Skip to Content

5장. 타임존은 파일이다 — IANA tz 데이터베이스의 실체

만화로 보는 요약 — 먼저 읽어보세요

만화로 보는 요약 — 먼저 읽어보세요

도시별 규칙 파일 → JVM·OS·DB·브라우저의 서로 다른 tzdata 버전 → 오프셋이 아니라 지역과 이력을 고르는 설계.

면접 실전 질문: ① ZoneId.of(“Asia/Seoul”)은 실제로 무엇을 읽는가? ② tzdata 버전이 기기마다 다르면 어떤 문제가 생기는가? ③ 타임존 규칙 변경은 배포된 서버에 언제 반영되는가?


Asia/Seoul은 언어가 계산하는 값이 아니라 파일 한 줄이다 — 그리고 그 파일은 JDK와 OS와 DB와 브라우저에 따로따로, 서로 다른 버전으로 존재한다.

면접 실전 질문: ① ZoneId.of("Asia/Seoul")을 호출하면 실제로 무엇을 읽는가? ② tz 데이터베이스가 “버전”을 갖는다는 게 왜 문제가 되는가? ③ 정부가 표준시를 바꾸면 이미 배포된 서버의 타임존 계산은 언제, 어떻게 바뀌는가?


배경 — 계산이 아니라 파일

4장은 이 질문으로 끝났습니다. epoch 정수 하나와 “서울 기준 오후 3시”라는 화면 표시 사이를 잇는 규칙은 어디에 있으며, 그 규칙이 물리 법칙이 아니라 각국의 입법 기록이라면 컴퓨터 안에 대체 어떤 형태로 존재하느냐고. 2장은 그 답의 절반을 이미 줬습니다. 오프셋은 계산이 아니라 결정이고, ZoneId가 오프셋 하나가 아니라 “지역 이름 + 결정 이력의 데이터베이스”로 설계된 이유가 거기 있다고요.

이 장은 그 절반의 답을 실제로 열어봅니다. 많은 개발자가 ZoneId.of("Asia/Seoul")을 함수 호출로 생각합니다. 인자를 넣으면 어딘가에서 오프셋을 계산해 돌려준다고요. 실제로는 다릅니다. 그 호출은 계산이 아니라 조회입니다. 어딘가에 미리 적혀 있는 파일을 찾아 읽는 겁니다. 그 파일의 정체, 그 파일이 어떻게 관리되고, 어떻게 배포되고, 왜 여러 군데 흩어져 있는지 — 이게 이 장의 전부입니다.

스토리 — 한 사람의 취미에서 IANA의 표준으로

이 파일 더미의 시작은 뜻밖에 소박합니다. 1986년, **아서 데이비드 올슨(Arthur David Olson)**이라는 미국 국립보건원 연구자가 전 세계 타임존 규칙을 텍스트 파일로 정리하기 시작합니다. 얼마 지나지 않아 **폴 에거트(Paul Eggert)**가 합류해 범위를 크게 넓혔고, 1989년 이 코드와 데이터가 tzcode·tzdata라는 이름으로 처음 공개 릴리스됩니다. 그래서 이 데이터베이스는 지금도 종종 **“올슨 데이터베이스(Olson database)“**라 불립니다. 지금은 IANA(Internet Assigned Numbers Authority)가 공식 관리 주체이고, 에거트가 현재까지 편집자 역할을 맡고 있습니다.

데이터베이스 자체는 사람이 읽을 수 있는 규칙 텍스트입니다. “이 지역은 이 날짜부터 이 오프셋을 쓴다”는 문장들이 지역별 파일에 나열돼 있어요. 이 텍스트를 실제로 컴퓨터가 쓰는 이진 형식으로 바꾸는 도구가 zic(zone information compiler)입니다. zic이 뱉는 결과물이 TZif 바이너리 파일이고, 리눅스·macOS라면 이 파일들이 /usr/share/zoneinfo/Asia/Seoul 같은 경로에 실제로 존재합니다. ZoneId.of("Asia/Seoul")이 읽는 게 바로 이런 파일 — 또는 그 파일을 그대로 옮겨 담은 JDK 내부 사본입니다. JDK는 OS의 zoneinfo에 기대지 않고, 컴파일된 tzdata를 자체적으로 안에 챙겨 넣고 다닙니다. 4장에서 본 epoch 정수가 “언제”를 초 단위 정수로 근사했다면, 이 장의 tz 데이터베이스는 “어디서는 언제부터 몇 시였는가”를 파일로 근사한 셈입니다.

이 데이터베이스는 조용히 있지 않습니다. 규칙 텍스트가 바뀔 때마다 2024a, 2025b처럼 연도+알파벳으로 이름 붙은 새 버전이 릴리스됩니다. 어느 나라가 표준시나 DST 규정을 손보면, 때로는 시행까지 몇 주밖에 안 남은 시점에 새 버전이 나옵니다. 이 데이터베이스가 계산식이 아니라 입법을 뒤쫓는 파일이라는 증거입니다.

명명 규칙도 이 정치성을 피해 가려는 설계입니다. 항목은 Area/Location 형태 — Asia/Seoul, Europe/Madrid처럼 나라 이름이 아니라 그 지역을 대표하는 도시 이름을 씁니다. 나라 이름을 기준으로 삼지 않는 이유는 단순합니다. 나라는 갈라지고, 통일되고, 이름이 바뀝니다. 도시는 상대적으로 오래 남아요. 그래도 도시 이름 자체가 바뀌는 경우는 있어서, 하위 호환을 위해 **별칭(alias)**을 남겨둡니다. 인도의 Asia/Calcutta가 대표적인 예입니다. 도시의 공식 명칭이 콜카타(Kolkata)로 바뀌면서 새 정식 이름은 Asia/Kolkata가 됐지만, 기존 코드가 깨지지 않도록 Asia/Calcutta도 여전히 유효한 이름으로 남아 있습니다. 실제로 어떻게 동작하는지는 뒤에서 코드로 확인합니다.

이 데이터베이스가 순탄하게만 관리된 건 아닙니다. 2011년 9월, 점성술 소프트웨어 회사 Astrolabe가 올슨과 에거트를 상대로 소송을 겁니다. 데이터베이스 일부가 자사가 저작권을 가진 지도책의 정보를 침해했다는 주장이었어요. 그 여파로 2011년 10월 6일, 데이터베이스의 메일링 리스트와 FTP 배포가 실제로 중단됩니다. 전자프런티어재단(EFF)이 무료로 변호를 맡았고, Astrolabe는 2012년 2월 소송을 취하하며 “역사적 사실은 누구의 소유물도 아니다”라는 취지의 사과문까지 냈습니다. 이 사건을 계기로 데이터베이스의 공식 호스팅은 IANA(ICANN이 운영)로 넘어갔고, 2012년 2월 IETF가 RFC 6557로 유지보수 절차를 문서화했습니다. 짚어둘 대목은 이겁니다 — 전 세계 서버·휴대폰·브라우저가 의존하는 이 데이터가, 한때 소송 하나로 몇 주간 배포 자체가 멈췄던 적이 있다는 것.

핵심 — 버전은 몇 벌이고, 신선도는 누가 책임지는가

여기서부터가 실무의 문제입니다. 데이터베이스가 파일이라면, 그 파일의 사본이 몇 벌이나 있는가가 진짜 질문입니다. JVM은 자체 tzdata 사본을 갖고, OS는 /usr/share/zoneinfo를 따로 갖고, 데이터베이스는 또 자기 타임존 테이블을 따로 갖고(MySQL의 mysql_tzinfo_to_sql이 그 예), 브라우저는 각자 엔진에 자기 사본을 심어둡니다. 정부가 규칙 하나를 바꾸면, 이 넷 전부가 갱신돼야 실제로 정확해집니다. 하나라도 낡으면, 그 하나만 과거 규칙으로 계산합니다. 타임존 버그의 실체는 대개 알고리즘이 틀린 게 아니라, 이 중 하나의 사본이 새 법을 아직 못 받은 것입니다.

JDK 자신도 이 문제를 압니다. 그래서 정규 릴리스 주기(분기별)를 기다리지 않고 tzdata만 먼저 갱신할 수 있도록 오라클이 별도로 TZUpdater라는 도구를 배포합니다. 이건 JDK/JRE 설치본 안의 tzdata를 IANA의 최신 데이터로 다시 컴파일해 넣는 유틸리티예요. 그리고 JDK 안에서 이 데이터를 실제로 쥐고 있는 클래스가 java.time.zone.ZoneRulesProvider입니다. 이 provider가 “지금 이 JVM이 로드한 tzdata 버전이 뭔지”를 그대로 알려줍니다.

import java.time.zone.ZoneRulesProvider; import java.util.NavigableMap; public class TzVersion { public static void main(String[] args) { NavigableMap<String, ?> versions = ZoneRulesProvider.getVersions("UTC"); System.out.println("ZoneRulesProvider.getVersions(\"UTC\") 키 목록 = " + versions.keySet()); System.out.println("가장 최신 버전 = " + versions.lastKey()); System.out.println("java.version = " + System.getProperty("java.version")); } }
ZoneRulesProvider.getVersions("UTC") 키 목록 = [2026a] 가장 최신 버전 = 2026a java.version = 21.0.11

2026a라는 값이 그대로 이 장의 요점입니다. 이건 상수가 아니라, 이 글을 실행한 시점, 이 JDK 21 이미지가 담고 있던 tzdata 버전입니다. 같은 코드를 다른 날, 다른 JDK 배포판, 다른 OS에서 돌리면 다른 문자열이 나올 수 있어요. 코드는 그대로인데 답이 달라진다는 것 — 이게 알고리즘 문제가 아니라 데이터 신선도 문제라는 뜻입니다.

이제 그 파일 안에 실제로 뭐가 적혀 있는지 열어볼 차례입니다. ZoneRules는 특정 지역의 오프셋이 역사적으로 어떻게 바뀌어왔는지, 그 전이(transition) 전체를 갖고 있습니다. Asia/Seoul로 직접 확인해봤습니다.

import java.time.ZoneId; import java.time.zone.ZoneOffsetTransition; import java.time.zone.ZoneRules; import java.util.List; public class SeoulTransitions { public static void main(String[] args) { ZoneRules rules = ZoneId.of("Asia/Seoul").getRules(); List<ZoneOffsetTransition> transitions = rules.getTransitions(); System.out.println("Asia/Seoul 전체 전이(transition) 개수 = " + transitions.size()); for (ZoneOffsetTransition t : transitions) { System.out.printf("%s %s -> %s (%s)%n", t.getDateTimeBefore(), t.getOffsetBefore(), t.getOffsetAfter(), t.isGap() ? "GAP" : "OVERLAP"); } } }

전체 28개 중 흐름을 보여주는 줄만 추립니다(중간 반복분은 생략했습니다).

Asia/Seoul 전체 전이(transition) 개수 = 28 1908-04-01T00:00 +08:27:52 -> +08:30 (GAP) 1912-01-01T00:00 +08:30 -> +09:00 (GAP) 1948-06-01T00:00 +09:00 -> +10:00 (GAP) 1948-09-13T00:00 +10:00 -> +09:00 (OVERLAP) ... (1949~1951년, 매년 여름 같은 패턴 3회 반복) 1954-03-21T00:00 +09:00 -> +08:30 (OVERLAP) 1955-05-05T00:00 +08:30 -> +09:30 (GAP) 1955-09-09T00:00 +09:30 -> +08:30 (OVERLAP) ... (1956~1960년, 매년 여름 같은 패턴 5회 반복) 1961-08-10T00:00 +08:30 -> +09:00 (GAP) 1987-05-10T02:00 +09:00 -> +10:00 (GAP) 1987-10-11T03:00 +10:00 -> +09:00 (OVERLAP) 1988-05-08T02:00 +09:00 -> +10:00 (GAP) 1988-10-09T03:00 +10:00 -> +09:00 (OVERLAP)

Asia/Seoul이라는 파일 한 줄 뒤에 이만한 역사가 들어 있습니다. 1908년, 대한제국은 +08:27:52라는 지역 태양시에서 +08:30이라는 반올림된 표준시로 넘어갑니다. 1912년, 일제 강점기에 일본 표준시에 맞춰 +09:00으로 바뀝니다. 그런데 여기서 끝이 아니에요. 1948년부터 1951년까지, 정부 수립을 전후한 그 혼란기에도 한국은 여름마다 +10:00으로 올라가는 서머타임을 이미 시행하고 있었습니다. 전환 기록이 남긴 첫 날짜는 1948년 6월 1일 — 정부 수립(8월 15일)보다 앞선 미군정기입니다. 1954년, 이승만 정부가 표준시 자체를 다시 +08:30으로 되돌렸다가, 1955년부터 1960년까지는 그 +08:30을 기준으로 또 여름마다 +09:30 서머타임을 돌렸고, 1961년 박정희 군정이 표준시를 지금의 +09:00으로 재확정합니다. 그리고 1987년과 1988년, 서울올림픽을 앞두고 다시 한번 여름 두 해 동안 +10:00 서머타임을 시행한 뒤 완전히 접습니다. 표준 오프셋만 세 번(+08:30 → +09:00 → +08:30 → +09:00) 바뀌었고, 서머타임은 최소 세 시기에 걸쳐 시행됐다 폐지되기를 반복한 겁니다. 2장이 “타임존은 법령을 모아둔 데이터베이스”라고 말했을 때, 이게 바로 그 말의 실물입니다. +09:00이라는 오늘의 한 줄 뒤에는 이런 정치사 전체가 접혀 들어가 있습니다.

GAPOVERLAP이라는 두 표시도 눈여겨봐 둘 만합니다. 오프셋이 커지는 전이는 그 순간 어떤 지역 시각이 존재하지 않게 되고(GAP), 오프셋이 작아지는 전이는 어떤 지역 시각이 두 번 나타나게 됩니다(OVERLAP). 이 GAP과 OVERLAP이 실제로 코드에서 어떤 사고를 일으키는지는 다음 장의 주제입니다.

마지막으로 별칭이 실제로 어떻게 동작하는지 확인해봤습니다.

import java.time.ZoneId; public class AliasCheck { public static void main(String[] args) { ZoneId calcutta = ZoneId.of("Asia/Calcutta"); ZoneId kolkata = ZoneId.of("Asia/Kolkata"); System.out.println("ZoneId.of(\"Asia/Calcutta\") = " + calcutta); System.out.println("정규화(normalized) = " + calcutta.normalized()); System.out.println("두 ZoneId의 getRules() 동일? " + calcutta.getRules().equals(kolkata.getRules())); System.out.println("두 ZoneId 자체가 equals()? " + calcutta.equals(kolkata)); } }
ZoneId.of("Asia/Calcutta") = Asia/Calcutta 정규화(normalized) = Asia/Calcutta 두 ZoneId의 getRules() 동일? true 두 ZoneId 자체가 equals()? false

Asia/Calcutta를 넣으면 이름표는 Asia/Calcutta 그대로 유지됩니다. Java가 이걸 자동으로 Asia/Kolkata로 바꿔 부르지 않아요. 그런데 실제 규칙(getRules())은 Asia/Kolkata와 완전히 같은 객체로 취급됩니다 — 두 이름이 tz 데이터베이스 안에서 같은 파일을 가리키는 두 개의 이름표이기 때문입니다. 대신 ZoneId끼리의 equals()는 이름 문자열을 비교하므로 false가 나옵니다. “규칙은 같지만 이름은 다르다”는 이 어긋남을, 코드로 직접 확인하기 전엔 짐작만 하고 넘어가기 쉬운 지점입니다.

이 모든 관찰을 하나로 묶으면 이렇습니다. 타임존을 다루는 일은 더 똑똑한 알고리즘을 짜는 문제가 아니라, 여러 군데 흩어진 파일 사본의 신선도를 관리하는 문제입니다. JVM, OS, 데이터베이스, 브라우저 — 이 넷이 각자 다른 시점에 tzdata를 받아 각자 다른 버전으로 굳어 있다면, 코드 자체는 완벽해도 답은 넷 다 다를 수 있습니다.

정리

  • ZoneId.of("Asia/Seoul")은 계산이 아니라 조회다. 실제로 읽는 건 IANA tz 데이터베이스가 zic으로 컴파일한 TZif 바이너리(OS라면 /usr/share/zoneinfo, JDK라면 자체 내장 사본)다.
  • 이 데이터베이스는 1986년 아서 데이비드 올슨이 시작해 폴 에거트가 확장했고(“올슨 데이터베이스”), 지금은 IANA가 관리 주체다. 규칙이 바뀔 때마다 2024a, 2026a처럼 버전이 릴리스된다 — 실측한 이 JDK 21 이미지의 tzdata 버전은 **2026a**였다(관측 시점·이미지에 따라 달라질 수 있다는 게 이 장의 요지다).
  • 명명 규칙 Area/Location은 나라가 아니라 도시를 기준으로 삼는다 — 나라는 갈라지고 이름이 바뀌지만 도시는 비교적 안정적이기 때문이다. Asia/Calcutta 같은 옛 이름은 하위 호환 별칭으로 남는다. 실측 결과 별칭의 ZoneId 이름표는 유지되지만(equals()false), 실제 규칙(getRules())은 Asia/Kolkata와 동일한 객체다.
  • 2011년 Astrolabe v. Olson 소송으로 데이터베이스 배포가 몇 주간 중단됐다가, 2012년 2월 취하됐고, 이후 공식 호스팅이 IANA(ICANN)로 넘어가 RFC 6557로 유지보수 절차가 문서화됐다.
  • Asia/Seoul의 실제 transition 28개를 덤프해보면, 표준 오프셋만 +08:30 ↔ +09:00 사이를 세 번 오갔고(1908·1912·1954·1961), 서머타임도 19481951년, 19551960년, 1987~1988년 세 시기에 걸쳐 시행·폐지가 반복됐다. 오늘의 +09:00 한 줄 뒤에는 이 정치사 전체가 접혀 있다.
  • 결론: 타임존 버그의 실체는 대개 알고리즘 오류가 아니라 JVM·OS·DB·브라우저가 각자 다른 tzdata 버전을 들고 있어서 생기는 신선도 문제다. 오라클의 TZUpdater와 ZoneRulesProvider는 그 신선도를 JDK 릴리스 주기와 무관하게 맞추기 위한 장치다.

생각해볼 질문: 방금 본 transition 출력에는 GAPOVERLAP이라는 두 표시가 계속 반복됐습니다. 오프셋이 갑자기 커지면 어떤 지역 시각은 아예 존재하지 않게 되고, 갑자기 작아지면 어떤 지역 시각은 두 번 나타납니다. 그렇다면 2020-05-05T02:30:00 같은 값이 하필 그 없는 시각, 혹은 두 번 오는 시각 한가운데 떨어진다면 — 여러분의 코드는 그 값을 어떤 순간으로 해석해야 할까요?

6장 · 없는 시각, 두 번 오는 시각 — DST의 두 얼굴

Last updated on