타임존과 서머타임 입문: IANA 이름, 없는 시각, tzdata
CST·IST 가 모호한 이유, 서머타임으로 사라지는 02:30 을 8개 언어가 어떻게 처리하는지, zdump 로 보는 1987~88년 한국 서머타임, PHP 가 KST 를 +08:30 으로 읽는 이유까지 실행해 확인합니다.
시리즈: 날짜·시간·타임존 강좌 2/6 (초급). 오프셋을 타임존이라 생각하고 저장해 온 분, 한국에 살아서 서머타임(일광 절약 시간, DST)을 고민해 본 적이 없는 분을 위한 글입니다. 다 읽고 나면 올바른 타임존 이름을 고르고, 건너뛰거나 반복되는 시각을 내 언어가 어떻게 처리하는지 예측하고, zdump 로 전환 시점을 읽고, 런타임의 tz 데이터베이스 버전을 확인할 수 있습니다.
한국 개발자가 서머타임 버그를 만나는 건 대개 해외가 얽힐 때입니다. 지금의 Asia/Seoul 은 일 년 내내 +09:00 이라 국내 서비스에서는 문제가 드러나지 않습니다. 그런데 미국·유럽 사용자가 생기거나 서버를 해외 리전에 두면 일 년에 두 번 "존재하지 않는 시각" 과 "두 번 오는 시각" 이 찾아옵니다. 02:30 에 맞춘 알림이 03:30 에 울리고, "01:00~02:00 집계" 가 두 시간 분량이 되고, -05:00 으로 저장한 회의가 여름에 한 시간 밀립니다. 이 글은 그 순간 8개 런타임이 무엇을 하는지 실제 출력으로 보여 주고, 같은 순간인데 Mac 한 대 위의 두 프로그램이 한 시간 어긋나는 사례까지 확인합니다. 실행 환경은 Apple Silicon Mac(M1 Pro), 시스템 tz 데이터베이스 2026d. Python 3.9.6 zoneinfo, PHP 8.5.6, Java 26, .NET 10, Swift 6.4, Dart 3.11, JavaScriptCore jsc, sqlite 3.51 을 모두 TZ= 를 명시해서 돌렸습니다.
타임존은 오프셋이 아니라 "규칙집"
1회에서 "순간" 과 "벽시계 시각" 을 나눴습니다. 둘을 잇는 것이 타임존이고, 그 정체는 이름 붙은 오프셋의 역사입니다. 이름은 IANA tz 데이터베이스(tzdata, Olson 데이터베이스라고도 함)에서 옵니다. 일 년에 여러 번 갱신되는 공개 데이터입니다. 이름은 지역/도시 형태로 Asia/Seoul, America/New_York, Asia/Kolkata 같은 식이고, 이 기계의 Python 3.9 에는 598개가 있었습니다.
시계나 화면에 보이는 KST·EST·IST 는 약어이고 표시용일 뿐입니다. 2026-01-15 와 2026-07-15 에 각 약어를 쓰는 타임존을 zoneinfo 로 세어 봤습니다.
CST: 44 zones, offsets [-6.0, -5.0, 8.0]
IST: 7 zones, offsets [1.0, 2.0, 5.5]
KST: 3 zones, offsets [9.0]
CST -6.0 ['America/Bahia_Banderas', 'America/Belize', 'America/Chicago']
CST -5.0 ['America/Havana', 'Cuba']
CST 8.0 ['Asia/Chongqing', 'Asia/Chungking', 'Asia/Harbin']
IST 1.0 ['Eire', 'Europe/Dublin']
IST 2.0 ['Asia/Jerusalem', 'Asia/Tel_Aviv', 'Israel']
IST 5.5 ['Asia/Calcutta', 'Asia/Kolkata']
KST 는 Asia/Seoul, 별칭 ROK, 그리고 Asia/Pyongyang 세 곳이 씁니다. "CST" 는 미국 중부·쿠바·중국, "IST" 는 아일랜드·이스라엘·인도입니다. 도구마다 처리가 달랐습니다.
| 입력 | 실제 결과 |
|---|---|
Python ZoneInfo("CST") |
ZoneInfoNotFoundError 'No time zone found with key CST' |
Java ZoneId.of("CST") |
ZoneRulesException: Unknown time-zone ID: CST |
jsc Intl.DateTimeFormat(…, {timeZone: "CST"}) |
RangeError: invalid time zone: CST |
jsc new Date("2026-03-04 12:00 CST") |
Invalid Date |
PHP new DateTimeImmutable("2026-03-04 12:00 IST") |
+02:00 (이스라엘, 인도가 아님) |
PHP new DateTimeImmutable("2026-03-04 12:00 KST") |
+08:30 |
PHP new DateTimeImmutable("2026-03-04 12:00 JST") |
+09:00 |
마지막 두 줄이 한국 개발자에게 중요합니다. PHP 는 "KST" 를 +09:00 이 아니라 +08:30 으로 읽었습니다. PHP 의 약어 표 DateTimeZone::listAbbreviations()['kst'] 첫 항목이 Asia/Seoul 30600, 즉 한국이 1954~61년에 썼던 +08:30 이기 때문입니다(아래 zdump 출력에서 확인). 로그나 외부 API 에서 "2026-03-04 12:00 KST" 같은 문자열을 받아 PHP 로 파싱하면 30분 어긋난 순간이 저장됩니다. 거부해 주는 쪽이 친절한 셈입니다.
Python 의 ZoneInfo("EST") 는 존재하지만 서머타임 없는 고정 -05:00 입니다. 7월 1일에도 -05:00 EST 였고 America/New_York 은 -04:00 EDT 였습니다.
규칙: 저장과 입력은 IANA 이름으로. 약어는 출력에만.
처음 5분: zdump 로 전환 시점 읽기
zdump 는 macOS 와 리눅스에 들어 있고, 타임존의 오프셋이 바뀌는 순간을 보여 줍니다.
zdump -v -c 2026,2027 America/New_York | grep 2026
America/New_York Sun Mar 8 06:59:59 2026 UT = Sun Mar 8 01:59:59 2026 EST isdst=0 gmtoff=-18000
America/New_York Sun Mar 8 07:00:00 2026 UT = Sun Mar 8 03:00:00 2026 EDT isdst=1 gmtoff=-14400
America/New_York Sun Nov 1 05:59:59 2026 UT = Sun Nov 1 01:59:59 2026 EDT isdst=1 gmtoff=-14400
America/New_York Sun Nov 1 06:00:00 2026 UT = Sun Nov 1 01:00:00 2026 EST isdst=0 gmtoff=-18000
두 줄씩 "전환 1초 전" 과 "전환 순간" 입니다. 3월 8일은 01:59:59 다음이 03:00:00 이라 02:00~02:59 가 없습니다(갭). 11월 1일은 01:59:59 EDT 다음이 01:00:00 EST 로 돌아가 01:00~01:59 가 두 번 있습니다(겹침).
이제 Asia/Seoul 을 봅니다.
zdump -v -c 1950,2027 Asia/Seoul | grep -E " (19|20)[0-9][0-9] UT"
Asia/Seoul Sat Mar 20 15:00:00 1954 UT = Sat Mar 20 23:30:00 1954 KST isdst=0 gmtoff=30600
...
Asia/Seoul Wed Aug 9 15:30:00 1961 UT = Thu Aug 10 00:30:00 1961 KST isdst=0 gmtoff=32400
Asia/Seoul Sat May 9 16:59:59 1987 UT = Sun May 10 01:59:59 1987 KST isdst=0 gmtoff=32400
Asia/Seoul Sat May 9 17:00:00 1987 UT = Sun May 10 03:00:00 1987 KDT isdst=1 gmtoff=36000
Asia/Seoul Sat Oct 10 16:59:59 1987 UT = Sun Oct 11 02:59:59 1987 KDT isdst=1 gmtoff=36000
Asia/Seoul Sat Oct 10 17:00:00 1987 UT = Sun Oct 11 02:00:00 1987 KST isdst=0 gmtoff=32400
Asia/Seoul Sat May 7 16:59:59 1988 UT = Sun May 8 01:59:59 1988 KST isdst=0 gmtoff=32400
Asia/Seoul Sat May 7 17:00:00 1988 UT = Sun May 8 03:00:00 1988 KDT isdst=1 gmtoff=36000
Asia/Seoul Sat Oct 8 16:59:59 1988 UT = Sun Oct 9 02:59:59 1988 KDT isdst=1 gmtoff=36000
Asia/Seoul Sat Oct 8 17:00:00 1988 UT = Sun Oct 9 02:00:00 1988 KST isdst=0 gmtoff=32400
tz 데이터에는 1954~61년의 +08:30(gmtoff=30600)과 1987~88년 서머타임(KDT, +10:00)이 남아 있고, 1988년 10월 이후로는 전환이 한 줄도 없습니다. 생년월일이나 오래된 기록을 다루는 시스템에서는 지금도 영향을 줍니다. 파이썬으로 보면 1987-10-11 02:30 은 두 번 있었고, 1988-05-08 02:30 은 없었습니다.
Asia/Seoul 1987-10-11T02:30:00+10:00 fold=0 KDT -> UTC 1987-10-10 16:30 -> back 02:30 KDT
Asia/Seoul 1987-10-11T02:30:00+09:00 fold=1 KST -> UTC 1987-10-10 17:30 -> back 02:30 KST
Asia/Seoul 1988-05-08T02:30:00+09:00 fold=0 KST -> UTC 1988-05-07 17:30 -> back 03:30 KDT
Asia/Seoul 1988-05-08T02:30:00+10:00 fold=1 KDT -> UTC 1988-05-07 16:30 -> back 01:30 KST
존재하지 않는 새벽 2시
America/New_York 의 2026-03-08 02:30 을 코드는 어떻게 처리할까요. 먼저 파이썬입니다.
# ~/demo/l2_gap.py
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
z = ZoneInfo("America/New_York")
for fold in (0, 1):
d = datetime(2026, 3, 8, 2, 30, tzinfo=z, fold=fold)
rt = d.astimezone(timezone.utc).astimezone(z)
print(d.isoformat(), f"fold={fold}", d.tzname(), "-> UTC", f"{d.astimezone(timezone.utc):%H:%M}", "-> back", f"{rt:%H:%M %Z}")
2026-03-08T02:30:00-05:00 fold=0 EST -> UTC 07:30 -> back 03:30 EDT
2026-03-08T02:30:00-04:00 fold=1 EDT -> UTC 06:30 -> back 01:30 EST
예외가 없습니다. 파이썬은 뉴욕의 어떤 시계에도 나타난 적 없는 시각을 태연하게 만들고, UTC 를 왕복시켜야 비로소 03:30 으로 바뀌는 게 보입니다. fold 는 Python 3.6 에서 들어온 속성으로, 모호한 시각의 두 가지 읽기 중 하나를 고릅니다. 갭에서는 fold=0 이 전환 전, fold=1 이 전환 후 오프셋입니다.
다른 런타임에도 같은 질문을 했습니다.
| 런타임 | 2026-03-08 02:30 New York (갭) | 2026-11-01 01:30 New York (겹침) |
|---|---|---|
Python 3.9 zoneinfo |
02:30-05:00 그대로, 에러 없음 |
fold=0 → -04:00 EDT, fold=1 → -05:00 EST |
Java 26 ZonedDateTime.of |
03:30-04:00 |
01:30-04:00(앞쪽). withLaterOffsetAtOverlap() 이면 -05:00 |
PHP 8.5 DateTimeImmutable |
03:30 -04:00 EDT |
01:30 -04:00 EDT |
jsc new Date(2026, 2, 8, 2, 30) |
07:30Z = 03:30 EDT |
05:30Z = EDT |
Swift Calendar.date(from:) |
07:30Z = 03:30 EDT |
05:30Z = EDT |
Dart DateTime(…)(TZ=America/New_York) |
03:30 EDT |
01:30 EDT |
.NET 10 TimeZoneInfo |
IsInvalidTime = True, ConvertTimeToUtc 는 예외 |
IsAmbiguousTime = True, 06:30Z = EST |
sqlite datetime(…,'utc')(TZ=America/New_York) |
07:30 |
05:30 |
.NET 의 예외 메시지는 이렇습니다.
ArgumentException: The supplied DateTime represents an invalid time. For example, when the clock is adjusted forward, any time in the period that is skipped is invalid. (Parameter 'dateTime')
자바는 직접 물어볼 수도 있습니다. 02:30 의 getValidOffsets 는 [], getTransition 은 Transition[Gap at 2026-03-08T02:00-05:00 to -04:00] 이었습니다.
대부분의 라이브러리는 갭 길이만큼 "앞으로 밀어" 주므로 알림 용도라면 대개 기대한 대로입니다. 판단을 강요하는 건 .NET 뿐, 불가능한 값을 조용히 통과시키는 건 파이썬뿐이었습니다.
두 번 오는 새벽 1시
겹침에서는 실제로 있는 두 순간 중 하나를 골라야 합니다. 대부분 앞쪽(아직 서머타임인 쪽)을 골랐고, .NET 은 표준시, 파이썬은 fold 로 지정하게 합니다.
의외였던 건 PHP 입니다. 같은 코드인데 타임존에 따라 고르는 쪽이 달랐습니다.
America/New_York 2026-11-01 01:30 -> 2026-11-01 01:30:00 -04:00 EDT UTC 05:30
Europe/Madrid 2026-10-25 02:30 -> 2026-10-25 02:30:00 +01:00 CET UTC 01:30
Europe/London 2026-10-25 01:30 -> 01:30 +00:00 GMT
America/Chicago 2026-11-01 01:30 -> 01:30 -05:00 CDT
미국 타임존은 서머타임(앞쪽), 유럽 타임존은 표준시(뒤쪽). 같은 마드리드 시각을 자바는 +02:00, TZ=Europe/Madrid 의 jsc 도 00:30Z(+02:00)로 봤습니다. PHP 서비스와 자바 서비스가 각각 "마드리드 10월 25일 02:30" 을 변환하면 한 시간 차이 나는 순간이 저장됩니다. 모호한 로컬 시각은 어느 쪽인지 명시적으로 고른 뒤 저장하세요.
흔한 실수: 타임존 대신 고정 오프셋 저장
뉴욕 사용자가 1월에 매주 10:00 회의를 등록하고, 앱은 2026-01-15T10:00:00-05:00 을 저장한 뒤 7일씩 더합니다. 26주 뒤입니다.
# ~/demo/l2_fixed.py (발췌)
fixed = datetime(2026, 1, 15, 10, 0, tzinfo=timezone(timedelta(hours=-5)))
named = datetime(2026, 1, 15, 10, 0, tzinfo=ZoneInfo("America/New_York"))
f2, n2 = fixed + timedelta(days=182), named + timedelta(days=182)
America/New_York fixed 2026-07-16T10:00:00-05:00 = 11:00 local | named 2026-07-16T10:00:00-04:00
Europe/Madrid fixed 2026-07-16T10:00:00+01:00 = 11:00 local | named 2026-07-16T10:00:00+02:00
고정 오프셋이면 여름 내내 11:00 이 되고, 타임존 이름이면 10:00 그대로입니다. 국내 사용자만 있다면 +09:00 이든 Asia/Seoul 이든 결과가 같지만, 그건 지금 한국에 서머타임이 없다는 우연에 기댄 것뿐입니다. 해결: 반복 일정이나 미래 일정은 벽시계 시각과 Asia/Seoul 같은 타임존 이름을 함께 저장합니다.
갭과 겹침을 직접 판별하기
파이썬은 불평하지 않으니 판별 함수를 한 번 만들어 둡니다.
# ~/demo/l2_check.py
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
def classify(naive, zone):
"""타임존 안의 벽시계 시각이 'ok' / 'gap' / 'overlap' 중 무엇인지 돌려준다"""
z = ZoneInfo(zone)
a = naive.replace(tzinfo=z, fold=0)
b = naive.replace(tzinfo=z, fold=1)
if a.utcoffset() == b.utcoffset():
return "ok"
back = a.astimezone(timezone.utc).astimezone(z).replace(tzinfo=None)
return "overlap" if back == naive else "gap"
America/New_York 2026-03-08 02:30:00 gap
America/New_York 2026-11-01 01:30:00 overlap
America/New_York 2026-03-08 12:00:00 ok
Europe/Madrid 2026-03-29 02:30:00 gap
America/Santiago 2026-09-06 00:00:00 gap
Asia/Tokyo 2026-03-08 02:30:00 ok
칠레(America/Santiago)는 자정에 전환하므로 2026-09-06 은 00:00 이 없고 01:00 에 시작합니다. 자바의 LocalDate.of(2026,9,6).atStartOfDay(ZoneId.of("America/Santiago")) 는 2026-09-06T01:00-03:00[America/Santiago] 를 돌려줬습니다. "하루는 반드시 00:00 에 시작한다" 고 가정한 코드는 여기서 깨집니다.
tz 데이터베이스는 바뀐다: 같은 순간, 두 가지 답
각국은 서머타임 규칙을 바꾸고, 때로는 몇 주 전에야 발표합니다. tz 데이터베이스는 새 릴리스(2026a, 2026b…)로 따라가고, 런타임마다 자체 사본을 가질 수 있습니다. 이 Mac 에서는 이랬습니다.
| 런타임 | 데이터 출처 | 보고된 버전 |
|---|---|---|
macOS, zdump, Python zoneinfo |
/usr/share/zoneinfo |
2026d(+VERSION 파일) |
| Swift Foundation | 시스템 | TimeZone.timeZoneDataVersion = 2026d |
| PHP 8.5.6 | 번들 | timezone_version_get() = 2026.1 |
| Java 26 | JDK 번들 | ZoneRulesProvider.getVersions = 2025b |
2024~2029년의 전환을 파이썬(시스템)·PHP·자바에서 전부 뽑아 비교했더니, 그 기간에 전환이 있는 타임존 중 자바와 시스템은 205개 중 10개, PHP 와 시스템은 201개 중 11개가 달랐습니다. America/Vancouver, America/Edmonton, Africa/Casablanca, Europe/Chisinau 등입니다. 고정된 순간 2026-11-15T12:00:00Z 를 America/Vancouver 로 바꿔 봤습니다.
python 3.9 zoneinfo: 2026-11-15T05:00:00-07:00
python 3.12 zoneinfo: 2026-11-15T05:00:00-07:00
php 8.5: 2026-11-15T04:00:00-08:00 PST
java 26: 2026-11-15T04:00-08:00[America/Vancouver]
jsc: 05:00 PDT
swift: 2026-11-15T05:00:00-07:00
date(1): 2026-11-15T05:00 MST
.NET 10: 2026-11-15T05:00:00.0000000-07:00
같은 기계, 같은 순간인데 한 시간 차이입니다. 2026d 에는 2026-11-01 밴쿠버의 표준시 복귀가 없고(zdump 로 보면 약어만 PDT 에서 MST 로 바뀌고 오프셋은 -07:00 유지), PHP 와 자바의 옛 사본은 여전히 -08:00 으로 돌아갑니다. 약어마저 jsc 는 PDT, date 는 MST 로 제각각입니다. 여기서는 데이터가 보여 주는 것만 적었습니다. 제도 자체는 공식 발표로 확인하세요.
할 일: 각 런타임이 tzdata 를 어디서 가져오는지 알아 두고, OS 패키지, PHP(번들 timezonedb 또는 PECL 확장), JDK(또는 Oracle TZUpdater), 파이썬 tzdata 패키지를 정기 점검 때 함께 갱신합니다. 여러 서비스가 로컬 시각을 맞춰야 한다면 변환은 한 곳에서만 하세요.
작은 측정: 타임존은 느린가
"속도 때문에" 진짜 타임존 대신 고정 오프셋을 저장하는 팀도 있습니다. Python 3.9.6, Apple Silicon Mac(M1 Pro)에서 UTC aware datetime 200,000개(seed 7)를 astimezone() 으로 변환해 5회 중앙값을 냈습니다.
timezone.utc 62 ns/op
fixed -05:00 215 ns/op
ZoneInfo New_York 243 ns/op
ZoneInfo Tokyo 224 ns/op
ZoneInfo Madrid 248 ns/op
ZoneInfo.no_cache load 54.7 us/op
ZoneInfo cached 65 ns/op
진짜 타임존은 고정 오프셋보다 4~15% 비쌀 뿐이고(두 번째 실행은 248~263 ns), 차이는 수십 나노초입니다. 비싼 건 타임존 파일 읽기로, ZoneInfo.no_cache 는 약 55 µs, 캐시가 되는 보통의 ZoneInfo("…") 호출(65 ns)의 약 840배였습니다. 진짜 타임존을 쓰고, ZoneInfo 는 평범하게(캐시가 되도록) 만드세요.
솔직한 한계와 버전 메모
zoneinfo와fold는 PEP 615(Python 3.9)와 PEP 495(3.6)에 적힌 대로 동작했습니다. 실행한 버전은 3.9.6 과 3.12.14 뿐입니다.- Dart
DateTime은 UTC 와 프로세스의 로컬 타임존만 압니다.timezone패키지 없이는 "America/New_York" 을 지정할 수 없습니다(설치·실행 안 함). 표의 Dart 행은 프로세스 전체를TZ=America/New_York으로 돌린 결과입니다. - jsc 에는
Temporal이 없어서(undefined)disambiguation옵션으로 명시하는 방식은 시험하지 못했습니다. 브라우저와 Node.js 는 실행하지 않았습니다. - .NET 은 macOS 에서 돌렸으므로
TimeZoneInfo가 IANA ID 를 읽습니다. Windows 에서는 Windows ID 를 쓰며, 여기서TryConvertIanaIdToWindowsId("Asia/Seoul")는Korea Standard Time을 돌려줬습니다. - PHP 의 겹침 처리와 KST 해석은 8.5.6 에서 관찰한 것입니다. 문서화된 동작은 찾지 못했으니 기대지 마세요.
- 전환 비교는 2024~2029년만, 파이썬 쪽은 1시간 간격 스캔과 이분 탐색으로 구했습니다.
연습 문제
- "America/New_York 의 2026-11-01 01:30" 이 뜻할 수 있는 두 UTC 순간은? 파이썬에서 각각을 고르려면?
- 1988-05-08 02:30 Asia/Seoul 을
classify()에 넣으면 무엇이 나올까요? zdump 출력으로 이유를 설명하세요. - 외부 시스템이
"2026-03-04 12:00 KST"를 보내고 PHP 8.5.6 이 파싱했습니다. 어떤 오프셋이 붙었고, 대신 무엇을 보내 달라고 해야 하나요?
정답
fold=0이면05:30Z(EDT,-04:00),fold=1이면06:30Z(EST,-05:00).datetime(2026, 11, 1, 1, 30, tzinfo=ZoneInfo("America/New_York"), fold=1)처럼 지정합니다.gap. zdump 에Sun May 8 01:59:59 1988 KST다음 줄이Sun May 8 03:00:00 1988 KDT라서 02:00~02:59 가 없었습니다. 파이썬에서 왕복시키면 03:30 KDT 로 바뀝니다.+08:30. 약어 대신2026-03-04T12:00:00+09:00같은 ISO 8601 오프셋이나Asia/Seoul이라는 IANA 이름을 보내 달라고 하세요.
다음 글
자주 묻는 질문
한국에는 서머타임이 없는데 왜 신경 써야 하나요?
해외 사용자와 해외 리전 서버 때문입니다. America/New_York 에서는 2026-03-08 의 02:00~02:59 가 존재하지 않고 2026-11-01 의 01:00~01:59 는 두 번 있습니다. 또 tz 데이터베이스에는 1987~88년 한국 서머타임(KDT, +10:00)이 기록돼 있어서 옛 날짜를 다루는 코드에 영향을 줍니다.
PHP 에서 '12:00 KST' 를 파싱했더니 +08:30 이 나옵니다. 왜죠?
PHP 의 약어 표에서 kst 의 첫 항목이 Asia/Seoul 의 옛 오프셋 30600초(+08:30, 1954~61년 사용)이기 때문입니다. PHP 8.5.6 에서 new DateTimeImmutable('2026-03-04 12:00 KST') 는 +08:30 이었습니다. 약어 대신 Asia/Seoul 을 쓰세요.
같은 컴퓨터의 두 프로그램이 현지 시각을 1시간 다르게 보여 주는 이유는?
런타임마다 tz 데이터베이스 사본을 따로 가질 수 있기 때문입니다. 이 Mac 에서는 시스템이 2026d, PHP 가 2026.1, Java 가 2025b 였고, 2026-11-15T12:00Z 의 America/Vancouver 를 Python·Swift·.NET 은 05:00 -07:00, PHP 와 Java 는 04:00 -08:00 으로 보여 줬습니다.