본문으로 건너뛰기
개발 머꼬
개발 노트Python
hohyeon.dev16

저장된 시각이 9시간씩 어긋나고 비교에서 TypeError까지 난 이유

  • #Common Pitfall
  • #Engineering Note
  • #Python

문제 발생

만료 시각을 검사하는 코드가 어떤 경로에서는 9시간 어긋나고, 어떤 경로에서는 아예 예외로 죽었습니다.

from datetime import datetime

created = datetime.utcnow()          # 타임존 없음
expires = row["expires_at"]          # DB 드라이버가 tz-aware로 돌려줌

if expires < datetime.utcnow():      # TypeError
    ...
TypeError: can't compare offset-naive and offset-aware datetimes

원인 분석

두 종류의 datetime이 섞였습니다. Python 문서는 이 구분을 이렇게 설명합니다 — aware 객체는 시간대와 서머타임 정보를 충분히 갖고 있어 다른 aware 객체를 기준으로 자신을 위치시킬 수 있으며, 해석의 여지가 없는 특정 순간을 나타냅니다. 반대로 naive 객체는 자신을 명확히 위치시킬 정보가 없고, 그것이 UTC인지 지역 시각인지 다른 시간대인지는 전적으로 프로그램에 달려 있습니다.

utcnow()가 정확히 이 함정입니다. UTC 기준의 시각을 담지만 tzinfoNone인 naive 객체를 돌려줍니다. 그래서 로컬 시각과 섞이면 조용히 9시간이 어긋나고, aware 객체와 만나면 예외가 납니다. 문서는 예외 조건도 명확히 적습니다 — 한쪽이 aware이고 다른 쪽이 naive이면 뺄셈에서 TypeError가 발생하고, naive와 aware datetime 사이의 순서 비교도 TypeError를 발생시킵니다.

그래서 표준 라이브러리는 방향을 정리했습니다 — datetime.utcnow()는 3.12부터 deprecated이며 datetime.now()UTC를 넘겨 쓰라고 안내합니다. utcfromtimestamp()도 같습니다.

해결 방안

  1. 경계에서 aware로 만듭니다.
from datetime import datetime, timezone

created = datetime.now(timezone.utc)
  1. 저장은 UTC, 표시만 지역 시간으로 통일합니다. DB에는 UTC aware 값을 넣고, 화면에 그릴 때만 astimezone(ZoneInfo("Asia/Seoul"))을 씁니다. 애플리케이션 내부에서 지역 시각으로 계산하기 시작하면 서머타임이 있는 지역에서 바로 무너집니다.

  2. naive가 들어오는 지점을 찾습니다. DB 드라이버 설정, 외부 API의 2026-01-01T00:00:00처럼 오프셋 없는 문자열이 흔한 출처입니다. 파싱 직후 replace(tzinfo=timezone.utc)로 붙일지, 정말 UTC가 맞는지 확인합니다 — 아니라면 그 자리에서 틀린 값이 됩니다.

  3. 예외를 반갑게 여깁니다. TypeError는 버그가 아니라 의미가 다른 두 값을 섞지 못하게 막아주는 안전장치입니다. 한쪽에 .replace(tzinfo=None)을 붙여 오류만 없애면, 9시간 어긋난 값이 조용히 흘러갑니다.

  4. 테스트에 타임존을 넣습니다. 서버가 UTC로 도는 CI에서는 로컬 시각과 UTC가 같아 이 버그가 재현되지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.