Watch at Ease세상을 읽고, 일상의 답을 찾다.

기술 · 실용 가이드

Python ZoneInfo: 실제로 존재하는 DST 중복 시각을 거부하지 않고 빠진 시각 검사하기

UTC 변환 결과와 직접 비교하면 반복 구간의 두 정상 시각을 모두 무효로 판단할 수 있습니다. 현지 시각의 왕복 변환을 먼저 검사한 뒤, 중복 선택과 순간 키를 결정하세요.

런던의 일반 시각, 두 중복 시각, 공백 시각에 대한 왕복 검사 결과를 비교한 자체 제작 표.
자체 제작 기술 일러스트: 두 중복 시각은 존재하며 봄철 공백은 왕복 검사를 통과하지 못합니다. Watch at Ease

시간대 정보가 있는 datetime이라고 해서 그 현지 시계 값이 실제로 존재했다는 뜻은 아닙니다. Python은 봄철 시계를 앞으로 옮기며 생기는 공백에도 ZoneInfo datetime을 만들 수 있습니다. 반대 오류도 가능합니다. datetime을 자신의 UTC 변환 결과와 바로 비교하는 지름길은 반복 구간의 정상 시각을 존재하지 않는 것으로 판단할 수 있습니다.

세 가지 결정을 분리하세요. 현지 시계 값이 존재하는지, 사용자가 두 번 중 어느 쪽을 뜻했는지, 선택한 순간을 어떻게 저장하고 비교할지입니다. 아래 검사는 반복 구간의 두 정상 시각을 모두 보존하고, 빠진 현지 시각을 몰래 이동시키지 않고 거부합니다.

1. 정상 시각을 거부하는 검사를 알아보기

pytz-deprecation-shim 마이그레이션 가이드는 dt.astimezone(timezone.utc) == dt를 쓰는 작은 표준 라이브러리 존재 검사 함수를 제공합니다. 근처에 있는 dateutil 절차는 별도의 방법입니다. fold를 바꾸면 UTC 오프셋이 달라지는 정상 중복 시각에서는 이 동등 비교 지름길이 실패합니다. PEP 495는 이런 시간대 간 동등 비교가 의도적으로 False가 되도록 규정합니다. 따라서 False만으로 공백 시각이라고 결론낼 수 없습니다.

CPython 3.12.14의 제한된 검증에서 Europe/London의 2024-10-27 01:30은 fold=0일 때 UTC 00:30, fold=1일 때 UTC 01:30으로 변환됐습니다. 둘 다 실제 순간입니다. 각각 자신의 UTC 변환 결과와 직접 비교하면 모두 False가 나왔습니다. 이를 보조 함수의 회귀 사례로 다루세요. ZoneInfo가 중복 시각을 표현할 수 없다는 증거가 아닙니다.

2. 왕복 후의 현지 시계 필드를 비교하기

tzinfo가 ZoneInfo 인스턴스인 datetime dt에 대해 먼저 유효한 시간대 정보를 요구하세요. dt.tzinfo가 존재하고 dt.utcoffset()이 None이 아니어야 합니다. back = dt.astimezone(timezone.utc).astimezone(dt.tzinfo)를 계산한 뒤 exists = back.replace(tzinfo=None) == dt.replace(tzinfo=None)를 계산합니다. 이는 왕복 변환 이후 원래의 현지 날짜와 시계 필드를 비교하는 방식입니다. 원래 값을 다른 시간대의 값과 직접 비교하지 않습니다.

exists가 False이면 입력을 거부하거나 별도로 문서화한 제품 정책을 적용하세요. dt를 자동으로 back으로 바꾼 뒤 검증했다고 부르면 안 됩니다. 존재하지 않던 입력을 변경한 것입니다. 이미 dateutil을 사용하는 앱이라면 문서에 있는 datetime_exists(dt)와 datetime_ambiguous(dt)를 이용해 자체 구현의 유지보수를 피할 수 있습니다. 실제 배포하는 구현에 동일한 검증 사례를 실행하세요.

3. 존재를 확인한 뒤 중복을 해소하기

exists가 True인 뒤 dt.replace(fold=1-dt.fold).utcoffset()이 dt.utcoffset()과 다르면 그 ZoneInfo 값은 중복 시각입니다. 앞선 회차(fold=0)나 뒤의 회차(fold=1)를 묻거나, 기존 선택을 보존하거나, 명시적으로 선택하세요. fold 설정 자체는 검증이 아닙니다. 이 존재 검사는 해당 현지 시계 값이 발생했는지를 묻습니다. 반복 구간 밖에 주어진 모든 fold 표시까지 엄격하게 검증하는 기능은 아닙니다.

상위 시스템 기록이 이미 UTC나 오프셋으로 순간을 지정했다면 그 정보를 보존하세요. 오프셋 없는 현지 문자열에서 다시 추측할 필요가 없습니다. ZoneInfo는 기존 순간을 변환할 때 fold를 적절히 설정합니다. 사용자가 입력하는 현지 예약 시간은 결과 순간뿐 아니라 시간대 키와 중복 처리 정책도 저장하세요. 보정값을 한 시간으로 고정하지 마세요. 전환 폭과 규칙은 선택한 시간대 데이터에 달려 있습니다.

4. 네 가지 회귀 사례를 유지하기

ZoneInfo('Europe/London')를 사용해 일반 시각인 2024-10-26 01:30, 2024-10-27 01:30의 두 fold 값, 존재하지 않는 2024-03-31 01:30을 검사했습니다. 왕복 존재 검사 결과는 차례대로 True, True, True, False였습니다. 존재 검사를 통과한 뒤에만 중복을 판단한 결과는 False, True, True, False였습니다. 공백 값은 UTC에서 돌아오자 현지 02:30이 되어 원래 시계 필드가 보존되지 않았음을 드러냈습니다.

이것은 과거 날짜의 검증 사례이며 앞으로의 모든 전환을 예측하지 않습니다. 앱이 쓰는 Python 버전, 시간대 키, 시간대 데이터 출처를 기록하고 실제 서비스 지역의 사례를 추가하세요. ZoneInfo는 시스템 시간대 데이터를 읽으며 필요하면 공식 tzdata 패키지를 사용합니다. 데이터 패키지가 없거나 시간대 키가 잘못된 것은 별도의 초기화 문제입니다. DST 공백이 아니므로 사용자 입력 오류로 바꾸어 거부하면 안 됩니다.

5. 검증한 다음 순간 키를 사용하기

입력이 존재하고 회차도 결정했다면, 업무상 식별 대상이 순간일 때 dt.astimezone(timezone.utc)를 사용하세요. 검증에서 동일한 ZoneInfo 객체를 공유하는 런던의 두 중복 값을 집합에 넣으면 현지 키는 하나가 됐습니다. 먼저 UTC로 변환하면 두 개가 됐습니다. 현지 값끼리 뺀 결과는 0초였지만 UTC 값끼리 뺀 결과는 3,600초였습니다. 비교 의미가 다르므로 저장 방식을 서로 같은 것으로 취급할 수 없습니다.

마지막 변환은 이벤트 순서, 순간 기준 중복 제거, 실제 경과 시간 계산에 쓰입니다. 반복되는 현지 시간 일정은 의도적으로 시계 기준 의미가 필요할 수 있으므로 원래 현지 규칙을 보존하세요. 지금 키를 변경해도 이전 현지 키 충돌로 덮어쓴 이벤트를 재구성할 수는 없습니다. 원본 기록이 남아 있다면 그곳에서 복구하세요. 일반 시각, 두 중복 회차, 공백 시각이 모두 의도한 경로로 처리되어야 수정을 승인할 수 있습니다.

존재 여부 확인, 중복 회차 선택, UTC 순간 키 생성 순서를 보여 주는 자체 제작 흐름도.
자체 제작 기술 일러스트: 존재, 중복 선택, 순간 식별은 별개의 판단입니다. Watch at Ease