파이썬 datetime timezone 비교 오류는 시간대 정보가 없는 naive 객체와 시간대 정보가 있는 aware 객체를 한 연산에 섞을 때 주로 발생합니다. API와 예약 작업에서는 내부 계산을 UTC aware datetime으로 통일하고, 한국어 화면이나 보고서에 보여 줄 때만 Asia/Seoul로 변환하는 방식이 안전합니다. 이 글에서는 오류를 구분하는 기준과 astimezone() 및 replace(tzinfo=...)를 선택하는 순서를 정리합니다.
먼저 확인할 것: naive와 aware의 차이
datetime.now()처럼 시간대를 지정하지 않고 만든 객체는 보통 naive입니다. 객체 안에 UTC인지 KST인지 판단할 시간대 정보가 들어 있지 않다는 뜻입니다. 반면 datetime.now(timezone.utc)처럼 tzinfo가 연결된 객체는 aware입니다.
from datetime import datetime, timezone
local_without_zone = datetime.now()
utc_with_zone = datetime.now(timezone.utc)
print(local_without_zone.tzinfo) # None
print(utc_with_zone.tzinfo) # UTC
정확한 판별은 tzinfo만 확인하는 것보다 utcoffset()까지 보는 편이 낫습니다. Python 문서의 기준은 tzinfo가 None이 아니고 utcoffset()도 None이 아니면 aware라는 것입니다.
def is_aware(value: datetime) -> bool:
return value.tzinfo is not None and value.utcoffset() is not None
왜 비교에서 TypeError가 발생할까
두 객체가 모두 aware라면 서로 다른 시간대라도 같은 UTC 기준으로 비교할 수 있습니다. 그러나 naive 객체와 aware 객체는 같은 기준으로 해석할 수 없으므로 순서 비교에서 TypeError가 발생합니다.
from datetime import datetime, timezone
created_at = datetime.now() # naive
current_utc = datetime.now(timezone.utc) # aware
# TypeError: naive와 aware를 섞은 비교
# print(created_at < current_utc)
여기서 created_at에 무조건 UTC를 붙이는 것이 정답은 아닙니다. 그 값이 실제로 어느 지역의 시각으로 입력되었는지 먼저 확인해야 합니다. 시간대 정보를 잃은 문자열이나 데이터베이스 값이라면, 입력 계약이 정해져 있을 때만 그 계약에 맞게 처리해야 합니다.
UTC 기준으로 생성하고 비교하기
새로 얻는 현재 시각은 가능한 한 aware UTC로 생성합니다. datetime.now(timezone.utc)는 UTC aware datetime을 반환하며, datetime.utcnow()는 naive UTC datetime을 반환합니다. Python 공식 문서에서는 utcnow()가 Python 3.12부터 deprecated되었고 UTC 표현에는 aware datetime 사용을 권장합니다.
from datetime import datetime, timezone
now_utc = datetime.now(timezone.utc)
deadline_utc = datetime(2026, 10, 12, 9, 0, tzinfo=timezone.utc)
if now_utc >= deadline_utc:
print("마감 시각이 지났습니다.")
이미 다른 시간대의 aware 값이 들어왔다면 aware 상태를 유지한 채 비교하면 됩니다. 비교 연산이 UTC 기준으로 처리되므로, 표시 지역이 다르다는 이유만으로 먼저 수동 계산을 할 필요는 없습니다.
UTC를 한국시간으로 표시하기: astimezone()
한국시간으로 보여 주거나 날짜를 기준으로 보고서를 만들 때는 ZoneInfo("Asia/Seoul")를 사용합니다. astimezone()은 같은 순간을 대상 시간대로 변환하므로, UTC 시각의 의미를 보존합니다.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
now_utc = datetime.now(timezone.utc)
now_kst = now_utc.astimezone(ZoneInfo("Asia/Seoul"))
print(now_utc.isoformat())
print(now_kst.isoformat())
ZoneInfo는 IANA 시간대 데이터에 기반한 tzinfo 구현입니다. 실행 환경에 시간대 데이터가 없으면 ZoneInfoNotFoundError가 발생할 수 있으므로, 배포 환경에 필요한 시간대 데이터가 준비되어 있는지도 확인해야 합니다.
replace(tzinfo=…)가 변환이 아닌 이유
다음 코드는 겉보기에는 UTC를 KST로 바꾸는 것처럼 보이지만, 실제로는 시각의 숫자를 그대로 둔 채 시간대 정보만 바꿉니다.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
value_utc = datetime(2026, 10, 12, 0, 0, tzinfo=timezone.utc)
# 시간대 변환이 아니라 Asia/Seoul 정보를 부착하는 동작
misleading = value_utc.replace(tzinfo=ZoneInfo("Asia/Seoul"))
# 같은 순간을 한국시간으로 변환
correct = value_utc.astimezone(ZoneInfo("Asia/Seoul"))
replace(tzinfo=...)는 시간대 정보가 없는 값의 입력 계약이 명확할 때, 그 값이 어느 시간대의 벽시계 시각인지 표시하는 용도로만 제한해 사용하는 편이 좋습니다. 이미 UTC인 값을 KST로 표시하려는 상황이라면 astimezone()이 맞습니다.
문자열과 예약 작업에서의 점검 순서
문자열을 받는 API나 예약 데이터에서는 아래 순서로 확인하면 원인을 좁히기 쉽습니다.
- 입력 문자열에
Z또는+09:00같은 offset이 포함되어 있는지 확인합니다. - 파싱한 결과가 aware인지
is_aware()같은 검사로 확인합니다. - 내부 비교 대상도 aware UTC인지 확인하고, naive 값을 섞지 않습니다.
- 한국 날짜나 시각을 판단해야 할 때만
astimezone(ZoneInfo("Asia/Seoul"))로 변환합니다. - 실행 환경에
Asia/Seoul시간대 데이터가 있는지 확인합니다.
특히 예약 작업의 날짜 경계는 서버의 표시 시간과 별개로 결정됩니다. “한국시간으로 오늘인가”를 판단하려면 먼저 UTC 기준 값을 KST로 변환한 뒤 날짜를 읽어야 합니다. 반대로 단순히 로그가 UTC로 보이는 문제라면 데이터 비교 오류와 같은 것으로 단정하지 말고, 로그 표시 설정과 데이터의 tzinfo를 분리해 확인해야 합니다.
적용 전 최종 체크
- 새 현재 시각은
datetime.now(timezone.utc)로 생성했는가? - 비교에 참여하는 모든 datetime이 aware인가?
- 문자열 입력의 offset 또는 시간대 계약을 확인했는가?
- UTC를 KST로 표시할 때
astimezone()을 사용했는가? replace(tzinfo=...)를 변환 함수처럼 사용하지 않았는가?- 예약 작업의 기준 날짜와 화면 표시 날짜를 구분했는가?
이 기준을 지키면 오류 메시지만 없애는 데 그치지 않고, API 시각·예약 실행 시각·사용자에게 보여 주는 한국시간을 서로 다른 역할로 관리할 수 있습니다.