이 글에서 얻는 것

  • “시간”을 Instant(절대 시각)과 LocalDateTime(지역 시각)로 구분해, UTC 저장/표시 변환을 안전하게 설계할 수 있습니다.
  • DST(서머타임) 때문에 생기는 대표 함정(존재하지 않는 시각, 두 번 존재하는 시각)을 이해하고 스케줄/마감 처리에서 사고를 줄일 수 있습니다.
  • Locale/i18n(메시지/포맷/통화)을 백엔드 계약 관점에서 정리하고, API에서 무엇을 고정해야 하는지 기준이 생깁니다.

0) 시간 문제는 대부분 “정의가 섞여서” 생긴다

백엔드에서 다루는 시간은 크게 3종류입니다.

  • Instant(절대 시각): “지구상 한 점에서의 실제 순간” (UTC 기준 타임스탬프)
  • Local date/time(지역 시각): “그 지역 달력 기준 시각” (타임존 규칙이 필요)
  • Duration/Period: “기간”(1시간/1일/1개월) — 달력 기반 기간은 DST 영향을 받을 수 있음

이걸 섞으면 사고가 납니다.

1) 원칙: 저장/전송은 UTC, 표시는 사용자 타임존

실무에서 가장 안전한 기본값:

  • DB 저장: UTC 기반(Instant, timestamp with time zone 개념)
  • API 전송: ISO8601 + offset/zone 정보를 명확히
  • UI 표시: 사용자 타임존/로케일에 맞게 변환

그리고 서버/DB의 기본 타임존도 UTC로 고정하는 편이 안전합니다.

2) DST(서머타임)가 터지는 지점

DST가 있는 지역에서는 “지역 시각”이 다음을 겪습니다.

  • 존재하지 않는 시각: 새벽 2시가 3시로 점프(그 2시는 존재하지 않음)
  • 두 번 존재하는 시각: 2시가 두 번 반복(어느 2시인지 애매함)

그래서 스케줄/마감은 아래 중 하나로 “정의”를 고정해야 합니다.

  • “절대 시각(UTC) 기준”인지
  • “지역 달력 기준(매일 9시)”인지

예: “매일 9시에 알림”은 지역 달력 기준입니다. 이 경우 DST가 바뀌면 UTC 시각이 변합니다.

3) 마감/기간 계산의 함정

  • “오늘 자정까지”는 사용자 타임존에 따라 다릅니다.
  • “1일 뒤”는 24시간 뒤가 아닐 수 있습니다(DST로 23/25시간이 될 수 있음).

기간을 정의할 때:

  • 초/분/시간 단위면 Duration(절대 시간)
  • 월/년 단위면 Period(달력)

처럼 구분하는 편이 좋습니다.

4) 국제화(i18n): API는 ‘표준 포맷’을 유지하라

백엔드의 역할:

  • 저장/전송은 표준(ISO8601, 숫자는 raw value)
  • 표시 포맷은 UI/클라이언트에 맡긴다

예외:

  • 이메일/푸시 메시지처럼 서버가 사용자에게 직접 렌더링하는 경우엔 locale별 템플릿/메시지 번들이 필요합니다.

5) 운영/테스트: 시간은 반드시 시뮬레이션한다

시간 관련 버그는 재현이 어렵습니다. 그래서 테스트 전략이 중요합니다.

  • Clock 주입으로 “현재 시간”을 테스트에서 고정
  • DST 전환 날짜를 포함한 케이스(두 번 존재/존재하지 않는 시각)
  • 타임존이 다른 사용자(예: 서울/LA)의 “오늘/이번 주” 범위가 달라지는 케이스

연습(추천)

  • “매일 9시 알림”을 설계해보기: 저장은 어떤 값으로 할지(UTC? local time + zone?)와 DST 때 동작을 정의해보기
  • “오늘 00:00~23:59” 조회 API를 사용자 타임존 기준으로 구현하고, 서버/DB는 UTC인 상태에서 올바르게 동작하는지 테스트해보기
  • DST 전환일(해당 지역)을 골라, 존재하지 않는 시각/두 번 존재하는 시각을 처리하는 테스트를 작성해보기