배경
Feel-Archive 프로젝트에서 타임캡슐 기능을 테스트하던 중, 설정한 시간이 되어도 메일 알림이 오지 않는 문제가 발생했다.
오전 1시53분에 타임캡슐을 작성했고, 오전 2시에 열리도록 설정했는데 한참을 기다려도 안오길래 DB를 직접 확인해보니 open_at 컬럼에 오전 11시로 저장되어 있었다. 정확히 9시간 차이... 타임존 문제임을 확신했다.
원인
먼저 서버 환경을 확인했다.
# EC2 서버 시간 확인
date
# Sat Mar 7 17:45:35 UTC 2026
# Docker 컨테이너 내부 JVM 타임존 확인
java -XshowSettings:property -version 2>&1 | grep timezone
# default timezone = Etc/UTC
JVM은 UTC로 동작 중이었다. AWS SSM에 등록된 JDBC URL은 다음과 같았다.
jdbc:mysql://feel-mysql:3306/feelarchive?serverTimezone=Asia/Seoul
왜 open_at만 9시간 차이가 났을까?
흥미로운 점은 created_at은 정상적으로 저장되고 있었다는 것이다.

created_at이 정상처럼 보인 이유
created_at → 저장값 01:53 / 기대값 01:53 (정상)
open_at → 저장값 11:00 / 기대값 02:00 (9시간 차이)
created_at은 JPA @CreationTimestamp을 사용중이었고, 그래서 생성 시간은 처음에 생성될 때 UTC 16:53 생성되었을 것이다.
근데 JDBC URL에서 타임존을 한국으로 설정했기때문에 저장할 때 +9시간을 해서 25:53분 즉 1:53분으로 저장되어 있어서 정상적으로 저장된걸로 보였던 것이다.
open_at이 9시간 틀린 이유
프론트: KST 02:00 → "2025-03-05T02:00:00" 전송 (타임존 정보 없음)
서버(JVM UTC): "이거 UTC 02:00이구나" 해석
JDBC 드라이버: UTC → KST 변환 (+9)
DB 저장: 11:00 (오답)
핵심 원인은 JDBC 드라이버가 JVM 타임존(UTC)과 serverTimezone(Asia/Seoul)이 다른 것을 보고 자동으로 변환을 수행했기 때문이다.
DATETIME vs TIMESTAMP
MySQL 컬럼 타입도 이해가 필요하다.
DATETIME → 입력값 그대로 저장. 타임존 인식 없음.
범위 1000~9999년.
TIMESTAMP → 내부적으로 UTC(Unix timestamp)로 저장. 타임존 인식.
범위 1970~2038년.
현재 엔티티는 DATETIME 컬럼 + LocalDateTime을 사용 중이었다. DATETIME은 타임존을 인식하지 못하기 때문에 JDBC 드라이버가 변환을 담당하는 구조였다.
해결
두 가지 방향을 고민했다.
1. JVM/JDBC 모두 Asia/Seoul로 맞추기 → DB에 KST로 저장
2. DB는 UTC로 통일, 응답 시 KST 변환
DB에는 항상 UTC로 저장하는 것이 표준 관행이라서 타임존 설정이 바뀌어도 데이터의 의미가 불변하기때문에 2번을 선택했다.
1. JDBC serverTimezone 변경 serverTimezone=Asia/Seoul → serverTimezone=UTC로 설정
2. JVM 타임존 명시적 설정 SSM에 등록된 JVM 옵션을 수정했다. -Duser.timezone=UTC로 명시적으로 선언
3. 프론트에서 타임캡슐 openAt 시간을 줄 때 KST 시간을 주지만 Offset을 포함해서 전송하도록 수정
- 한국 전용 서비스임을 가정했기때문에 offset은 9시간으로 고정했다.
그래서 백엔드쪽 DTO에서도 LocalDateTime -> OffsetDateTime으로 타입 변환을 해주고 서버쪽에서 받은 시간을 UTC로 변환해서 저장해주도록 했다.
DB 저장은 UTC로, 응답 시에는 서버에서 KST로 변환해서 반환하도록 했다.
느낀점
시간 관련 버그는 개발 환경에서는 잘 발생하지 않다가 배포 환경에서 갑자기 터지는 경우가 많다. 개발 환경에서는 로컬 시간 값을 사용하기에 잘 맞아떨어져보이기 때문이다.
항상 DB에는 UTC 시간을 저장하고 서버쪽에서 변환하도록 하자.
'Feel-Archive' 카테고리의 다른 글
| [Feel-Archive] 스케줄러 블로킹을 해결하기 위한 비동기 처리 - 타임캡슐 알림 개발 (1) (0) | 2026.03.18 |
|---|---|
| [Feel-Archive] 성능 테스트(1) - 1GB 메모리에서 모니터링 구축하기 (0) | 2026.03.12 |
| [Feel-Archive] N+1 감지 시스템 구축기 (0) | 2026.03.12 |