서버 시간은 맞는데 응답만 9시간 틀렸던 타임존 버그

가입 시각을 응답으로 내려 주는데, 프런트에서 자꾸 9시간 전으로 표시된다는 제보가 왔습니다. 서버 시간은 정확했고 DB에 찍힌 값도 맞았습니다. 딱 9시간, 그러니까 한국과 UTC의 시차만큼이 어긋나 있었습니다.

어디서 9시간이 새는가

추적해 보니 DB에는 UTC 기준으로 시각이 저장돼 있었는데, 애플리케이션은 이 값을 그냥 한국 시각인 것처럼 다뤘습니다. LocalDateTime은 이름 그대로 타임존 정보가 없는 타입입니다. 같은 2026-07-01T00:00이라도 이게 UTC인지 KST인지 그 자체로는 알 수 없죠. 그 모호함을 코드 곳곳에서 각자 다르게 해석하면서 9시간이 붙었다 떨어졌다 한 것이었습니다.

기준을 하나로 고정

먼저 JVM과 커넥션의 타임존을 명시적으로 통일했습니다. 그리고 경계를 넘나드는 시각 값은 타임존을 품은 타입으로 다루도록 정리했습니다.

spring:
  jackson:
    time-zone: Asia/Seoul
# jdbc url
jdbc:mysql://.../db?serverTimezone=Asia/Seoul

서버 내부 로직에서 특정 시점을 표현할 땐 InstantOffsetDateTime을 쓰고, 화면에 보여줄 때만 사용자 타임존으로 변환하도록 바꿨습니다.

OffsetDateTime joinedAt = member.getJoinedAt()
        .atZone(ZoneId.of("Asia/Seoul"))
        .toOffsetDateTime();

배운 것

시간 버그는 대부분 계산이 틀려서가 아니라 기준이 여러 개여서 생겼습니다. 저장은 UTC, 표시는 사용자 타임존, 이 원칙 하나를 팀 규칙으로 못 박고 나서야 9시간 유령이 사라졌습니다.