저장된 시각이 9시간씩 어긋나고 비교에서 TypeError까지 난 이유
문제 발생
만료 시각을 검사하는 코드가 어떤 경로에서는 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 기준의 시각을 담지만 tzinfo는 None인 naive 객체를 돌려줍니다. 그래서 로컬 시각과 섞이면 조용히 9시간이 어긋나고, aware 객체와 만나면 예외가 납니다. 문서는 예외 조건도 명확히 적습니다 — 한쪽이 aware이고 다른 쪽이 naive이면 뺄셈에서 TypeError가 발생하고, naive와 aware datetime 사이의 순서 비교도 TypeError를 발생시킵니다.
그래서 표준 라이브러리는 방향을 정리했습니다 — datetime.utcnow()는 3.12부터 deprecated이며 datetime.now()에 UTC를 넘겨 쓰라고 안내합니다. utcfromtimestamp()도 같습니다.
해결 방안
- 경계에서 aware로 만듭니다.
from datetime import datetime, timezone
created = datetime.now(timezone.utc)-
저장은 UTC, 표시만 지역 시간으로 통일합니다. DB에는 UTC aware 값을 넣고, 화면에 그릴 때만
astimezone(ZoneInfo("Asia/Seoul"))을 씁니다. 애플리케이션 내부에서 지역 시각으로 계산하기 시작하면 서머타임이 있는 지역에서 바로 무너집니다. -
naive가 들어오는 지점을 찾습니다. DB 드라이버 설정, 외부 API의
2026-01-01T00:00:00처럼 오프셋 없는 문자열이 흔한 출처입니다. 파싱 직후replace(tzinfo=timezone.utc)로 붙일지, 정말 UTC가 맞는지 확인합니다 — 아니라면 그 자리에서 틀린 값이 됩니다. -
예외를 반갑게 여깁니다.
TypeError는 버그가 아니라 의미가 다른 두 값을 섞지 못하게 막아주는 안전장치입니다. 한쪽에.replace(tzinfo=None)을 붙여 오류만 없애면, 9시간 어긋난 값이 조용히 흘러갑니다. -
테스트에 타임존을 넣습니다. 서버가 UTC로 도는 CI에서는 로컬 시각과 UTC가 같아 이 버그가 재현되지 않습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.