Skip to content

필사 모드: 시간과 날짜의 기술적 지옥 완전 해설 — TimeZone, DST, 윤초, Temporal API, NTP 끝장 가이드 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — "2024년 10월 27일 02:30"이 존재하지 않을 수 있다

유럽에서 서머타임이 끝나는 날, 새벽 03:00이 되면 시계가 **02:00으로 되돌아간다**. 즉, 같은 날 02:30이 **두 번** 나온다. 그날 02:30에 쿠폰을 발급한 이벤트가 있다면, 그 쿠폰은 어느 02:30의 것인가?

반대로, 3월 마지막 일요일 유럽은 02:00에서 **03:00으로 건너뛴다**. 02:30이라는 시각 자체가 **존재하지 않는다**. 사용자가 "02:30에 예약"이라고 저장하면 그 예약은 언제 실행될까?

이런 질문들은 단순한 UX 이슈가 아니다. **금융 거래의 order 정렬, 로그의 시간순 분석, 분산 시스템의 이벤트 ordering, 임금 계산** 같은 실전 시스템이 이 미묘함에 깨진다. Jon Skeet(Noda Time 저자)의 유명한 말: **"Time is a joke. Seriously."**

이 글은 시간과 날짜를 다루는 기술적 기반을 처음부터 설명한다. UTC가 뭐고 GMT와 어떻게 다른지, Unix time은 왜 윤초를 무시하는지, IANA 타임존 데이터베이스가 왜 자주 업데이트되는지, DST 전환을 어떻게 안전하게 처리하는지, `new Date()`의 악명 높은 함정들, JavaScript `Temporal` API가 왜 10년 만의 혁신인지, 분산 시스템에서 NTP/PTP로 시계를 맞추는 방법, 그리고 Google Spanner의 TrueTime까지.

Unicode 글에서 "문자열은 어려운 문제다"라고 했다면, **시간은 그보다 더 어렵다**. 문자열은 적어도 한 컴퓨터 안에서 결정론적이지만, 시간은 지구의 자전 속도 변화, 국가의 정치적 결정, OS의 clock drift까지 얽혀 있다.

1. 시간의 3가지 개념 — Instant, Local, Civil

시간을 프로그램에서 다룰 때 먼저 구분해야 할 세 가지:

1.1 Instant (순간)

"지금 이 순간"이라는 절대적 시점. 지구 어느 곳에서 관측하든 같은 값이다. **Unix timestamp**(1970-01-01T00:00:00Z부터 경과한 초)가 대표적 표현이다.

예: `1760000000` (2025-10-09T07:33:20Z에 해당하는 초 단위 epoch)

1.2 Civil Date/Time (민간 시각)

"2026년 4월 15일 13:30"처럼 **달력과 시계에서 읽는 값**. 타임존 없이는 모호하다.

1.3 Zoned Date/Time (타임존이 붙은 민간 시각)

"2026-04-15 13:30 Asia/Seoul". 이제 Instant로 변환 가능하다.

1.4 핵심 규칙

- **DB/로그/정렬**: **Instant**로 저장 (UTC 시점)

- **UI 표시**: 사용자 타임존으로 **Zoned**로 변환

- **반복 일정**("매월 1일 9시")은 Civil + TimeZone으로 저장

"UTC로 저장하면 된다"는 말은 반은 맞고 반은 틀리다. "2026년 12월 25일 00:00에 알림"이라는 일정을 서울 사용자가 세팅했다면, UTC로 환산해 저장하면 DST 변화/타임존 변경 시 **날짜가 바뀐다**. 이런 경우는 원본 civil+tz를 보존해야 한다.

2. UTC, GMT, TAI, UT1 — 시간 표준의 혼란

2.1 GMT (Greenwich Mean Time)

영국 그리니치의 평균 태양시. 19세기 선박 항해를 위해 표준화. 법적으로는 영국의 일부 공식 문서에서 여전히 쓰이지만, 기술 문서에서 "GMT"는 **거의 항상 UTC의 구어적 동의어**로 쓰인다.

2.2 UTC (Coordinated Universal Time)

국제 표준. **원자시(atomic time) 기반**으로 매우 정밀하지만, 지구 자전과 맞추기 위해 **윤초(leap second)**를 삽입한다. UTC = TAI - (누적 윤초 수).

2.3 TAI (International Atomic Time)

전 세계 약 400개 원자시계의 평균. **윤초 없음**. 2025년 기준 UTC보다 **37초** 앞서 있다. GPS time은 UTC가 아니라 TAI 기반이고 1980년 기준 오프셋을 가진다.

2.4 UT1

지구 자전에 실제로 맞춘 시간. UTC는 UT1과 0.9초 이상 차이 나면 안 된다는 규약으로 윤초가 추가된다.

2.5 실전 정리

- **프로그램에서 "UTC"라 부르는 것**: 대부분 `UTC = epoch + 초` (윤초 무시)

- **엄밀한 UTC**: 윤초 있음

- 보통 사용에서는 차이가 무시 가능하지만, 금융 High-Frequency Trading, 통신 위성 동기화 등에서는 중요

3. Unix Timestamp — 단순함의 대가

3.1 정의

1970-01-01T00:00:00Z부터 경과한 초(또는 밀리초/나노초).

- POSIX 표준: **초 단위, 윤초 무시**

- 즉, 하루는 **정확히 86,400초**로 취급

3.2 윤초는 어디로 사라지는가

실제 지구 시간에서 윤초가 삽입되면 UTC는 `23:59:60`을 한 번 거친다. Unix time은 이를 어떻게 처리하나?

**두 가지 방식**:

1. **Slew (권장)**: 윤초 전후 수 시간에 걸쳐 시간을 천천히 늘려서 흡수. Google/AWS/Meta가 쓰는 방식. 어떤 순간도 "건너뛰지" 않음.

2. **Smear**: NTP 서버가 윤초 당일 86,400초가 아닌 86,401초 수준으로 slew. 실제 OS의 clock도 부드럽게 진행.

3. **Jump**: 23:59:59에서 00:00:00으로 다시 점프. 많은 시스템이 crash하거나 이중 이벤트 생성.

**역사적 사고**:

- 2012년 6월 30일 윤초: Linux 커널 버그로 Java 애플리케이션 다수가 CPU 100% 점유. Reddit, LinkedIn, Foursquare 다운

- 2015년 6월 30일 윤초: Twitter/Instagram 일시 장애

- 2017년 1월 1일 윤초: Cloudflare DNS가 윤초 계산 오류로 일부 장애

**미래**: 2022년 국제도량형총회(CGPM)가 **2035년까지 윤초 폐지**를 결정. 그 이후는 수십 년 단위로 "윤분" 등으로 큰 단위 조정.

3.3 2038년 문제

Unix time을 32비트 signed integer로 저장하면 **2038-01-19T03:14:07Z** 이후 오버플로우. 32비트 임베디드 시스템(IoT, 구식 데이터베이스 포맷)의 시한폭탄.

대응: 대부분 현대 OS/언어는 64비트로 전환했지만, **레거시 C/C++ 바이너리, 일부 DB 스키마, 파일 포맷**을 점검해야 한다. time_t를 명시적으로 size 확인.

3.4 0년 문제 vs 음수 epoch

`-62135596800`은 서기 1년 1월 1일 UTC. 더 이전 날짜(기원전)는 protocol에 따라 표현 가능/불가능. Java `Instant.MIN`은 -9999999-01-01, `Instant.MAX`는 9999999-12-31.

4. ISO 8601 — 문자열 표현 표준

4.1 기본 형식

2026-04-15 날짜

2026-04-15T13:30:00 로컬 시각

2026-04-15T13:30:00Z UTC

2026-04-15T13:30:00+09:00 서울 시각

2026-04-15T13:30:00.123456789Z 나노초 정밀도

2026-W16-3 주 기반 (ISO 주 16의 수요일)

P1Y2M10DT2H30M 기간 (1년 2개월 10일 2시간 30분)

2026-04-15/2026-04-20 간격

4.2 RFC 3339 — ISO 8601의 subset

IETF가 인터넷 프로토콜용으로 제한한 subset. `T` 소문자 허용, 타임존 오프셋 필수 등.

4.3 일상의 함정

- `2026-04-15T13:30:00` (타임존 없음) → 로컬? UTC? 파서마다 다름 (JavaScript `new Date()`는 로컬, Python `fromisoformat`는 naive)

- `Z` vs `+00:00` — 의미는 같으나 문자열 비교는 다름

- 소수점 이하 초의 정밀도가 시스템마다 다름 (ms vs μs vs ns)

- `+09` vs `+0900` vs `+09:00` — 파서 지원 범위 차이

**교훈**: 문자열로 주고받을 때 반드시 **타임존 오프셋 명시**. 파서는 ISO 8601 공식 라이브러리(Java `Instant.parse`, Python 3.11+ `fromisoformat`, JS `Temporal.Instant.from`) 사용.

5. 타임존 — 정치의 시간

5.1 타임존이 하는 일

UTC와의 오프셋을 결정한다. 하지만 오프셋은:

- 지역마다 다름

- 역사적으로 변함 (국가가 DST 도입/폐지, 오프셋 변경)

- 정치적 결정으로 **갑자기** 바뀔 수 있음

5.2 IANA Time Zone Database (tz database, zoneinfo, Olson DB)

1986년부터 Paul Eggert가 유지하는 **시간대 역사의 교과서**. `Asia/Seoul`, `America/New_York` 같은 이름을 키로, 해당 지역이 역사적으로 사용한 모든 오프셋/DST 규칙/변경 이력을 담고 있다.

예: `Asia/Seoul`은 1954년 3월 21일에 UTC+8:30에서 UTC+9로 변경. 1961년까지 여러 번 오프셋이 바뀌었고, 1987~1988년 DST가 있었다가 폐지. 지금 한국 API가 "1955년 1월 1일 00:00 Seoul"을 UTC로 변환하려면 이 역사를 알아야 한다.

5.3 tzdata 업데이트

매년 5~10회 새 버전 나온다. 최근 변경:

- 2024년: 이집트 DST 재도입

- 2023년: 멕시코 대부분 DST 폐지

- 2022년: 칠레 DST 날짜 변경

- 2022년: 요르단, 시리아가 영구 DST로 전환

**운영에서 중요**:

- 컨테이너 이미지의 tzdata 버전 체크 (alpine은 `tzdata` 패키지 별도 설치 필요)

- JVM은 `TZupdater`로 최신 tzdata 갱신 가능

- DB 서버 OS의 tzdata가 구식이면 앱이 잘못된 오프셋 사용

- Node.js는 ICU를 내장하지만 일부 slim 배포판은 small ICU만 → 타임존 제한됨 (`NODE_ICU_DATA` 설정)

5.4 abbreviation의 모호성

`CST` — 어느 국가?

- Central Standard Time (미국, UTC-6)

- China Standard Time (UTC+8)

- Cuba Standard Time (UTC-5)

`IST` — 더 심함:

- India Standard Time (UTC+5:30)

- Ireland Standard Time (UTC+1)

- Israel Standard Time (UTC+2)

**규칙**: 약어는 표시용으로만 사용. 프로그램에서는 **IANA 이름(`Asia/Seoul`, `America/Chicago`)**을 써라.

5.5 POSIX TZ 문자열

구식 형식이지만 여전히 살아있다: `EST5EDT,M3.2.0,M11.1.0`. 절대 쓰지 말고 IANA 이름으로.

6. DST — 실전 지뢰

6.1 "존재하지 않는 시간"

2024년 3월 31일 유럽, 02:00 → 03:00으로 점프. 02:30은 **존재하지 않는다**.

// JavaScript 잘 모르는 경우

new Date('2024-03-31T02:30:00+01:00').toISOString()

// "2024-03-31T01:30:00.000Z" — 실제로는 의도한 02:30이 아님

대응:

- 사용자 입력의 civil time이 존재하지 않으면 에러 또는 **forward shift**(다음 유효 시간으로)

- Java `ZonedDateTime.of(...)` + `ZoneRulesProvider` → `Gap` 처리 가능

- `Temporal.ZonedDateTime.from({..., disambiguation: 'earlier'/'later'/'reject'/'compatible'})`

6.2 "두 번 나타나는 시간"

2024년 10월 27일 유럽, 03:00 → 02:00으로 되돌아감. 02:30은 **두 번** 존재한다.

- 같은 "2024-10-27 02:30 Europe/Berlin" 문자열이 **두 개의 UTC 시점**에 해당

- 금융 로그는 UTC 기준으로 정렬해야 안전

6.3 달력 예약의 재앙

"매주 월요일 09:00 미팅"을 UTC로 변환해서 저장했다면:

- 서울(항상 UTC+9)은 UTC 00:00

- 뉴욕(DST 있음)은 UTC 14:00 ↔ UTC 13:00 (여름/겨울 다름)

**원본 civil+tz로 저장**하는 게 정답. DB 컬럼을 (`timestamp`, `timezone`, `recurrence_rule`)로 분리.

6.4 DST 폐지 추세

EU는 2019년 "2021년부터 DST 폐지" 결정했으나 각국 이견으로 연기. 미국은 2022년 "Sunshine Protection Act"로 영구 DST 법안이 상원 통과했으나 하원 계류. 터키/러시아/아이슬란드/중국 등은 이미 폐지.

**개발자 관점**: 폐지 결정은 **수개월 전에 갑자기** 확정되므로 tzdata 업데이트 체인을 항상 유지해야 한다.

7. 언어별 datetime API

7.1 JavaScript의 `Date` — 악명 높은 설계

1995년 Netscape가 Java `java.util.Date`를 빠르게 베꼈는데, 그 Java API는 이후 폐기됐다(Joda Time/java.time으로 교체). 하지만 JS는 **수십 년간 호환성 때문에 고치지 못했다**.

문제점:

- **가변(mutable)**: `date.setMonth(3)`이 원본을 바꿈

- **월이 0부터 시작**: `new Date(2026, 0, 15)` = 2026년 1월 15일

- **문자열 파싱 비일관**: `new Date("2024-03-15")`는 UTC, `new Date("2024/03/15")`는 로컬 — 브라우저마다 다름

- **타임존 처리 불가**: "로컬" 또는 "UTC" 둘 중 하나. 임의 IANA zone 지원 없음

- **정밀도 ms**: 나노초 불가

Moment.js, date-fns, Luxon, Day.js 같은 라이브러리가 이 갭을 메웠다.

7.2 Temporal — 2025년의 구세주

Stage 3 → 2024년 말 브라우저/Node 순차 구현. 주요 타입:

- **`Temporal.Instant`**: 나노초 정밀도 UTC 시점

- **`Temporal.ZonedDateTime`**: Instant + IANA timezone

- **`Temporal.PlainDate` / `PlainTime` / `PlainDateTime`**: 타임존 없는 civil

- **`Temporal.Duration`**: 기간

- **`Temporal.Calendar`**: 그레고리력/이슬람력/일본력/불기 등

- **`Temporal.Now`**: `Temporal.Now.instant()` 등

const now = Temporal.Now.zonedDateTimeISO('Asia/Seoul')

const oneHourLater = now.add({ hours: 1 })

const duration = now.until(oneHourLater) // Temporal.Duration

혁신 포인트:

- **불변(immutable)**: 모든 연산이 새 객체 반환

- **명시적 타임존 처리**: `disambiguation: 'reject' | 'earlier' | 'later' | 'compatible'`

- **나노초 지원**

- **윤일/윤년/각종 달력 정확히 처리**

7.3 Python

- **`datetime`**: 표준, `tzinfo`를 직접 넣거나 `zoneinfo` 모듈(Python 3.9+) 사용

- **`pytz`** (구식): `astimezone()` 버그와 `normalize()` 필요로 악명

- **`arrow`**: 더 친숙한 API

- **`pendulum`**: Ruby-style DSL

- **권장**: `datetime` + `zoneinfo` (stdlib)

from datetime import datetime

from zoneinfo import ZoneInfo

dt = datetime(2026, 4, 15, 13, 30, tzinfo=ZoneInfo("Asia/Seoul"))

dt.astimezone(ZoneInfo("America/New_York"))

7.4 Java

- **`java.util.Date`**: 구식, **쓰지 말 것**

- **`java.util.Calendar`**: 구식

- **`java.time`** (Java 8, JSR-310 / Joda 기반): 현대

- `Instant`, `LocalDateTime`, `ZonedDateTime`, `OffsetDateTime`

- 불변, 스레드 안전

- **Joda Time**: Java 7 이전 세대. Java 8+에서는 `java.time` 사용.

7.5 Go

- `time.Time` 내부에 wall clock + monotonic clock 둘 다 저장

- 타임존은 `time.LoadLocation("Asia/Seoul")`

- 포맷 문자열이 독특: `2006-01-02 15:04:05 -0700` (참조 날짜)

- 파싱: `time.Parse(layout, s)`

7.6 Rust

- `std::time::SystemTime`: OS 시계

- `std::time::Instant`: monotonic (경과 시간 측정용)

- Civil/zoned 처리는 `chrono` 또는 `jiff` (2024년 BurntSushi 발표) crate

- `jiff`는 Temporal-inspired API로 주목

8. Monotonic Clock vs Wall Clock

8.1 차이

- **Wall clock**: 달력 시간. NTP 동기화로 앞/뒤로 뛸 수 있음

- **Monotonic clock**: "부팅 이후 경과 시간"에 가까운 것. 절대 뒤로 안 감

8.2 왜 중요한가

**경과 시간 측정**에는 반드시 monotonic 사용:

// ❌ NTP sync가 뒤로 뛰면 음수가 나옴

const start = Date.now()

await doWork()

const elapsed = Date.now() - start

// ✅

const start = performance.now()

await doWork()

const elapsed = performance.now() - start

**타임아웃 / rate limiter / 벤치마크** 모두 monotonic clock 써야 한다.

8.3 언어별 monotonic 접근

- JS 브라우저: `performance.now()` (밀리초 + 소수점, 페이지 로드부터)

- JS Node: `process.hrtime.bigint()` (나노초)

- Go: `time.Now()`가 monotonic 포함, `time.Since(start)`는 항상 안전

- Java: `System.nanoTime()`

- Python: `time.monotonic()`, `time.monotonic_ns()`

9. NTP, PTP — 분산 시스템의 시계 동기화

9.1 NTP (Network Time Protocol)

- Layered "stratum" 구조:

- Stratum 0: 원자시계, GPS 수신기

- Stratum 1: Stratum 0에 직접 연결된 서버 (NIST, KRISS)

- Stratum 2: Stratum 1에서 받는 공개 서버 (time.google.com, time.cloudflare.com)

- Stratum 3+: 일반 서버

- UDP 123 포트

- 정확도: LAN 내 수 ms, 인터넷 거리 10~50 ms

- 대부분의 OS가 기본 탑재 (`chronyd`, `systemd-timesyncd`, `w32time`)

9.2 PTP (Precision Time Protocol, IEEE 1588)

- LAN 내 나노초~마이크로초 정확도

- 스위치가 timestamping hardware 지원 필요

- 금융 High-Frequency Trading, 5G 기지국 동기화에서 필수

- AWS는 2023년 "Time Sync Service" (PTP 기반, 마이크로초 정확도) 공개

9.3 Google TrueTime

Spanner DB가 전 세계적으로 일관된 트랜잭션을 하려고 만든 시스템. **시간을 단일 값이 아닌 [earliest, latest] 구간**으로 다룬다.

- 각 데이터센터에 GPS + 원자시계 설치

- TT.now()가 `{earliest, latest}` 반환 (보통 7ms 폭)

- 트랜잭션 commit 시 `latest`를 timestamp로 삼고, `latest` 경과까지 대기 → 글로벌 ordering 보장

비슷한 아이디어: CockroachDB의 HLC (Hybrid Logical Clock) — NTP + 논리 시계 결합.

9.4 실전 교훈

- NTP가 없거나 drift가 심한 서버는 분산 시스템에서 **배제**해야 함 (신뢰할 수 없는 timestamp)

- 시간 기반 token(JWT exp)이나 rate limiter는 5~10초 leeway를 둬야 stratification 에러에 견딤

- Kubernetes 노드는 모두 같은 NTP 서버 사용 권장

10. 달력 시스템 — 그레고리력이 전부가 아니다

10.1 주요 달력

- **그레고리력**: 1582년 도입, 현재 국제 표준

- **율리우스력**: 그레고리력 전. 일부 정교회가 여전히 사용

- **이슬람력(Hijri)**: 달 기반, 1년 약 354일. 종교적 일정 계산에 필수

- **히브리력**: 달-태양 결합, 윤달 있음

- **일본력**: 그레고리력 + 연호(令和, 2019~). API가 연호 전환에 대응해야 함

- **태국 불기**: 그레고리력 + 543년 오프셋 → 2025년 = 불기 2568년

- **페르시아력**: 이란 공식

- **중국력**: 달-태양, 24절기. 공식 달력은 그레고리이지만 전통 명절(춘절)은 음력

10.2 Unicode CLDR / ICU

다국어 달력 지원:

new Intl.DateTimeFormat('ja-JP-u-ca-japanese', { year: 'numeric', month: 'long', day: 'numeric' })

.format(new Date('2026-04-15'))

// "令和8年4月15日"

new Intl.DateTimeFormat('ko-KR-u-ca-buddhist').format(new Date('2026-04-15'))

// "불기 2569년"

10.3 윤년 규칙 (그레고리력)

- 4로 나눠지면 윤년

- 100으로 나눠지면 **평년**

- 400으로 나눠지면 **윤년**

2000년은 윤년(400), 1900년/2100년은 평년(100).

"매년 2월 29일 미팅"은 4년에 한 번만 생긴다. Outlook 등은 "이전 달 마지막 날" 또는 "다음 달 1일"로 fallback 옵션을 제공.

10.4 1752년 9월 3~13일의 공백

영국은 1752년에 율리우스력에서 그레고리력으로 전환하면서 **9월 2일 다음이 9월 14일**이다. 러시아는 1918년. 이 역사적 공백을 **정확히 모델링하는 라이브러리는 드물다** (Java `GregorianCalendar`, Noda Time 등이 일부 지원).

11. 데이터베이스의 시간 처리

11.1 PostgreSQL

- **`timestamp without time zone`**: civil time. 저장/조회 시 그대로.

- **`timestamp with time zone (timestamptz)`**: **내부는 UTC epoch**, 입출력 시 session의 `TIME ZONE` 설정으로 변환.

- **`date`**, **`time`**, **`interval`** 지원

- `AT TIME ZONE 'Asia/Seoul'`로 변환

**권장**: 특별한 이유 없으면 `timestamptz` 사용.

11.2 MySQL

- `DATETIME`: civil time, 타임존 변환 없음

- `TIMESTAMP`: 저장 시 세션 tz → UTC, 조회 시 UTC → 세션 tz (자동). 2038 문제 있음 (32비트).

- `DATE`, `TIME` 지원

**주의**: `TIMESTAMP`는 서버의 `time_zone` 세션 변수에 따라 결과가 달라진다. 커넥션마다 `SET time_zone = '+00:00'` 권장.

11.3 MongoDB

- BSON `Date`는 64비트 int milliseconds (UTC epoch)

- 타임존 없음 → 저장 전 UTC 변환 필수

11.4 DynamoDB, Redis

- 타임스탬프 문자열(ISO 8601 with offset) 또는 epoch ms 저장

- TTL은 Unix timestamp(초)

11.5 ClickHouse

- `DateTime`, `DateTime64`(서브초 정밀도)

- 각 컬럼이 타임존을 가질 수 있음 (`DateTime('Asia/Seoul')`)

- 집계 쿼리에서 `toTimeZone()` 함수로 변환

12. 로그와 메트릭의 timestamp

12.1 원칙: UTC로 저장, 로컬로 표시

- 서버 로그: 모두 UTC (TZ=UTC 설정)

- 대시보드(Grafana, Kibana): 사용자 브라우저 tz로 표시

- 분산 tracing: span의 timestamp는 UTC epoch ns

12.2 Correlation과 Clock Skew

마이크로서비스 A, B, C가 있을 때 A가 B를 호출하고 B가 C를 호출한다. 각 서버의 시계가 약간씩 다르면:

- B의 span이 A의 span보다 **먼저** 시작한 것처럼 보일 수 있음

- OpenTelemetry는 이를 완화하기 위해 span의 duration을 monotonic clock으로 측정 후 UTC로 환산

12.3 ISO 8601 또는 RFC 3339로 저장

2026-04-15T13:30:00.123456789Z

여기서 `Z`(= UTC)는 필수. TZ 없는 문자열은 파싱이 모호하다.

12.4 Kafka 등 이벤트 스트림

각 메시지는 **producer timestamp**(producer가 붙인 시각)와 **log append time**(broker가 저장한 시각) 두 가지를 가진다. 순서 정렬은 offset으로 하고 시각은 참고용.

13. 실전 버그 케이스 10선

13.1 "새벽 2시에 실행될 cron이 DST 날 실행 안 됨"

원인: 새벽 2:30이 존재하지 않는 시간대. cron 표현식은 로컬 시간 기준.

해결: UTC로 cron 돌리거나, `OnCalendar=UTC` 옵션 명시.

13.2 "JWT exp가 이상하게 빨리 만료됨"

원인: 서버와 클라이언트 시계 차이.

해결: 5~30초 leeway. NTP 동기화 강제.

13.3 "이메일의 'Received' 헤더 시간이 미래"

원인: 어느 중계 서버의 시계가 앞서 있음.

해결: 인프라 팀에 NTP 점검 요청. Spam filter는 이런 경우 의심 신호로 취급.

13.4 "생일 알림이 하루 전/후에 옴"

원인: UTC 기준 저장했는데 사용자가 다른 tz로 이동.

해결: 생일은 `PlainDate`로 저장 (시각 없음). 알림 발송 시 사용자 tz의 00:00에 매핑.

13.5 "1970-01-01 표시"

원인: `null` timestamp를 0으로 취급 + Date로 format.

해결: null 체크.

13.6 "2038년 테스트 실패"

원인: 32비트 time_t.

해결: 64비트로 컴파일 확인.

13.7 "duration이 음수"

원인: wall clock 사용. NTP가 뒤로 보정.

해결: monotonic clock (`performance.now`, `time.monotonic`).

13.8 "이벤트 로그가 뒤죽박죽"

원인: 서버 간 clock skew.

해결: OpenTelemetry의 Span Link + Logical Clock.

13.9 "사용자가 '오늘' 예약했는데 '내일'로 저장됨"

원인: 브라우저 tz ≠ 서버 tz. 서버에서 `new Date(...)`로 파싱하면 서버 tz 기준.

해결: 브라우저에서 `Temporal.PlainDate.from(...)` 또는 ISO 문자열 + tz 오프셋 명시.

13.10 "휴가 일수 계산이 DST 전환 때 0.96일"

원인: `(endMs - startMs) / (24 * 3600 * 1000)`에서 DST로 하루가 23 or 25시간이 됨.

해결: `ChronoUnit.DAYS.between(zonedStart, zonedEnd)` 같은 **civil-aware** 연산.

14. 체크리스트 — 시간 관련 시스템 설계

저장

- [ ] "Instant"와 "Civil+TZ"를 구분해 저장

- [ ] UTC 저장 우선, 반복 일정은 original civil+tz 보존

- [ ] DB timestamptz 사용 (MySQL은 연결 tz UTC 고정)

- [ ] ISO 8601 + timezone offset 문자열

- [ ] 나노초 정밀도 필요한지 확인 (부족하면 부가 컬럼)

변환

- [ ] 사용자 tz를 profile에 저장 (IANA 이름)

- [ ] UI 표시 시에만 사용자 tz로 변환

- [ ] 서버 로컬 tz에 의존하지 않기 (`TZ=UTC` 강제)

DST

- [ ] "존재하지 않는 시각" 핸들링 (reject / next valid)

- [ ] "두 번 나타나는 시각" 핸들링 (earlier / later)

- [ ] Test data에 DST 전환일 포함

동기화

- [ ] NTP 또는 chrony 구동 확인

- [ ] Clock skew monitoring (5초 이상 alerts)

- [ ] 경과 시간은 monotonic clock

라이브러리

- [ ] JS: `Temporal` (또는 Luxon) — `Date` 직접 사용 지양

- [ ] Java: `java.time` (Date/Calendar 지양)

- [ ] Python: `datetime + zoneinfo`

- [ ] Go: `time` (지원하지만 layout 포맷 주의)

- [ ] Rust: `jiff` 또는 `chrono`

운영

- [ ] `tzdata` 정기 업데이트

- [ ] Docker 이미지의 tzdata 버전 확인 (alpine)

- [ ] JVM의 `TZupdater` 적용 프로세스

- [ ] 신규 DST 정책 변경 뉴스 모니터링

15. 역사적 사건 아카이브

15.1 2007년 미국 DST 날짜 변경

미 정부가 DST 시작을 3주 앞당김. 많은 Outlook/Blackberry가 구 규칙대로 동작해서 미팅 1시간 어긋남. MS는 긴급 패치 배포.

15.2 2011년 사모아 시간대 변경

국제날짜변경선 넘어가면서 **2011년 12월 30일이 없었다**. 12월 29일 다음이 12월 31일. 뉴질랜드 거래 시간대 맞춤.

15.3 2016년 터키

당일 결정으로 DST 폐지. 구 Android 기기들이 자동 변경 안 되어 1시간 늦게 출근.

15.4 2018년 북한

UTC+8:30 → UTC+9로 변경(남북 통일 제스처). tzdata 업데이트.

15.5 2023년 그리니치 표준시의 죽음?

UK 정부가 브렉시트 후 타임존 정책 논의. 아직 변화 없음.

15.6 금융 High-Frequency Trading

나스닥 등은 **나노초** 정확도로 주문 순서 기록. PTP + 공유 atomic clock. 1 마이크로초 차이가 수백만 달러 거래 결과를 가름.

마무리 — "시간은 여러 개다"

본 글에서 반복한 관점 하나: **"시간"이라는 단일 개념은 없다**. Instant, Civil time, Zoned time, Monotonic, Logical time 등 여러 층위의 시간이 있고, 각자 다른 목적에 맞는다. 프로그램의 버그 대부분은 이 층위를 혼동할 때 발생한다.

**핵심 원칙 5가지**:

1. **저장은 UTC, 표시는 local** — 그리고 반복 일정은 원본 civil+tz 보존

2. **정렬·계산은 Instant로, UI는 ZonedDateTime으로**

3. **경과 시간은 Monotonic으로** (wall clock은 뛸 수 있다)

4. **타임존은 IANA 이름으로** (약어 금지)

5. **DST 전환일을 테스트 케이스에 포함**

Unicode처럼, 시간도 **인간 사회의 복잡성**이 소프트웨어에 스며든 영역이다. 정치적 결정으로 DST가 바뀌고, 종교 달력에 따라 휴일이 달라지고, 지구 자전이 느려져서 윤초가 삽입된다. "Just use UTC"로 해결되지 않는 현실에서, 개발자는 **시간의 레이어드 모델을 머릿속에 가져야** 한다.

다음 글에서는 **분산 시스템의 Consensus — Raft/Paxos, CAP 정리, 그리고 eventual consistency**를 파고든다. 이번 글에서 본 **시간의 불일치**가 분산 시스템의 핵심 난제와 어떻게 얽히는지(Lamport clock, Vector clock, Hybrid Logical Clock 등) 이어서 보자.

시간을 이해하는 건 단지 버그를 피하는 기술이 아니다. **"동시에 일어났다"가 무엇을 의미하는가**에 대한 철학적 질문이기도 하다. 상대성 이론이 말하듯, 분산 시스템에서 "전역 현재 시각"은 존재하지 않는다 — 우리는 그것의 근사치를 다룰 뿐이다.

현재 단락 (1/281)

유럽에서 서머타임이 끝나는 날, 새벽 03:00이 되면 시계가 **02:00으로 되돌아간다**. 즉, 같은 날 02:30이 **두 번** 나온다. 그날 02:30에 쿠폰을 발급한 ...

작성 글자: 0원문 글자: 13,660작성 단락: 0/281