시간을 설계할 때 먼저 “세계에서 일어난 순간”, “사용자가 의도한 지역 달력 시각”, “한 프로세스 안에서 흐른 시간”을 서로 다른 값으로 모델링해야 합니다. 이 세 의미를 하나의 timestamp 덧셈으로 처리하면 DST와 재시작, 지역 규칙 변경에서 예약과 timeout이 서로 다른 오류를 만듭니다.
구현 선택은 저장 모델에서 드러나야 합니다. “매일 09:00”은 다음 날짜와 America/New_York를 저장한 뒤 해당 날짜의 규칙으로 instant를 다시 계산하고, “24시간 간격”은 기준 instant와 duration을 저장합니다. 두 작업을 같은 nextRunAt += 24h 코드로 합치지 않는 것이 핵심입니다.
현지 매일 09:00 예약과 24시간 간격 Timer
미국 New York의 2024-03-09 09:00은 UTC 14:00이고 DST 전환 뒤 2024-03-10 09:00은 UTC 13:00입니다. 현지 다음 날 같은 시각까지 경과 시간은 23시간입니다. 원래 instant에 24시간을 더하면 현지 10시가 됩니다. 반대로 가을 전환 구간의 하루는 25시간일 수 있습니다.
사용자가 원한 것이 매일 현지 9시인지 정확히 24시간 뒤인지 먼저 정합니다. 화면 표시·달력 반복·경과 timeout을 한 timestamp 필드와 연산으로 처리하지 않습니다.
절대 순간·달력 의도·로컬 경과의 구분
| 의미 | 표현 | 예 |
|---|---|---|
| 절대 순간 | UTC instant·명시 offset | 실제 결제 발생 시점 |
| 사용자 달력 의도 | local date/time·timezone ID·반복 규칙 | 매일 America/New_York 09:00 |
| 로컬 경과 | monotonic clock·duration | 요청 deadline·실행 timeout |
UTC timestamp는 각 사건을 같은 시간 기준으로 표현할 뿐, 여러 host가 만든 사건의 정확한 전역 순서를 자동으로 정하지 않습니다. host 사이의 clock skew(시계 오차)가 있으면 timestamp만 비교하지 말고 ID·version·인과 관계를 함께 사용합니다. monotonic 값은 한 실행 환경에서 경과 시간을 재는 값이므로 다른 host나 재시작 뒤의 보편 절대시각으로 쓸 수 없고, 장기 내구 예약에는 적절한 절대시각 또는 달력 의도를 기록해야 합니다.
그림을 글로 읽기
- 현지 매일 09:00 · timezone ID
- 날짜·tzdata·gap/fold 정책
- 해당 회차의 UTC instant
- 내구 회차 ID·실행·중복 억제
- 현지 매일 09:00 · timezone ID → 날짜·tzdata·gap/fold 정책: 달력 연산
- 날짜·tzdata·gap/fold 정책 → 해당 회차의 UTC instant: 모호성 해소
- 해당 회차의 UTC instant → 내구 회차 ID·실행·중복 억제: 회차 예약
gap·fold 시각과 모호성 처리
봄 전환일에 현지 02:30을 만들려고 하면 그 구간 자체가 없어 예약을 instant로 바꿀 수 없을 수 있고, 가을의 01:30은 서로 다른 offset으로 두 번 생길 수 있습니다. 이때 gap(없는 시각)은 건너뛸지 다음 유효 시각으로 이동할지, fold(두 번 생긴 시각)는 이른/늦은 offset을 고를지 두 회차를 모두 실행할지 먼저 정합니다. 그 뒤 사용하는 라이브러리가 기본으로 어느 보정을 하는지 읽고, 그 결과가 사용자 계약과 맞는지 확인합니다.
고정 offset -05:00만 저장하면 그 순간의 차이만 남아 미래 지역 법규나 DST 변화를 반영할 수 없습니다. 반복 예약을 저장할 때는 원래 현지 시각, 지역 ID, 반복 규칙, gap/fold 모호성 정책을 함께 보관하고, 필요하면 다음 회차의 계산된 instant도 따로 기록합니다. tzdata(지역별 시간 규칙 데이터)가 갱신되어 미래 instant가 달라질 수 있으므로 다시 계산할 시점과 사용자 안내, 이미 실행한 회차를 중복하지 않을 회차 ID 정책을 정합니다.
월말·윤년·정지 중 누락 정책
1월 31일에 한 달을 더할 때 2월 말로 조정할지 오류로 반환할지, 2월 29일 예약을 평년마다 어떻게 처리할지 먼저 정합니다. 서비스가 정지된 동안 여러 회차를 놓쳤다면 전부 보충할지, 가장 최신 한 번만 실행할지, 모두 skip할지를 업무 의미로 선택하고 각 회차 ID로 중복 실행을 막습니다. 날짜·시간 문자열에 서버 local timezone을 암묵적으로 적용하지 말고, 예약에 저장한 지역 ID를 사용합니다.
재현 연습으로 New York의 2024-03-09 09:00에서 현지 다음 날 회차를 계산하면 2024-03-10 09:00과 UTC 경과 23시간이 예상됩니다. 같은 시작 instant에 duration 24시간을 더하면 현지 10:00이 되어야 합니다. gap/fold 테스트에서는 없는 시각과 두 instant를 각각 정책에 맞게 거부·보정·선택하는지, 재시작 뒤 monotonic 값을 장기 예약의 키로 사용하지 않는지도 확인합니다.
시간 변환 결과와 경과 시간 검증
전환 전후·gap/fold·월말·윤년·다른 지역·tzdata 변경·재시작을 검사합니다. 이 노트는 시간 의미 설명이며 모든 라이브러리와 timezone 구현을 시험한 결과는 아닙니다. 특정 지역 예시는 그 날짜의 timezone 규칙에 대한 예입니다.