글로벌 서비스를 고려할 때, 타임존 이슈를 어떻게 처리해야 할까?
타임존을 지지고 볶아보면서 삽질을 해보자.
목표
글로벌 서비스를 개발한다면 시간 데이터 처리를 어떻게 해야할까? 다양한 타임존에서 일관된 시간 데이터를 처리하기 위해서는 Java의 시간 관련 타입과 데이터베이스의 시간 관련 자료형에 대한 이해가 필요하다.
LocalDateTime vs. ZonedDateTime
데이터베이스 시간 타입 (DATETIME, TIMESTAMP)의 특성
다양한 타임존 설정이 시간 데이터 처리에 미치는 영향
이에 대한 정리가 필요하다고 느껴 직접 코드를 짜고 테스트를 진행해보고자 한다.
테스트 테이블 설계 전, 시간 관련 타입은 어떤 것을 사용해야할까?
MySQL에서는 DATETIME
타입과 TIMESTAMP
타입을 주로 사용하게 된다. 두 타입은 어떤 차이가 존재할까?
어떤 상황에서 어떤 타입을 사용해야할지 고민해보자.
DATETIME
8 bytes
형식:
YYYY-MM-DD HH:MM:SS
현재 타임존의 시간으로 저장하고, 저장된 시간이 그대로 반환됨
유효 범위: 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
타임존 변환 없이 입력된 값 그대로 저장 및 반환
TIMESTAMP
4 bytes
형식:
YYYY-MM-DD HH:MM:SS
내부적으로 UTC로 저장되며, 값을 읽거나 쓸 때 세션의 타임존에 맞게 자동 변환되어 반환된다.
유효 범위: 1970-01-01 00:00:01.000000 ~ 2038-01-19 03:14:07.499999
항상 UTC로 저장하고, 세션 타임존에 맞게 변환해서 반환
실무에서는 어떻게 데이터 타입을 정하게 되는걸까?
서비스의 규모 (국내 한정 / 글로벌)
확장성 고려 (데이터 동기화, 서비스 유지 기간)
데이터 타입의 크기 (8bytes vs. 4bytes)
이러한 측면들을 고려해서 기본적으로 TIMESTAMP
를 사용하여 관리하거나 DATETIME
으로 시작해 규모가 커졌을 때 데이터 마이그레이션을 진행하는 등 팀에서 협의 후 설계를 진행한다고 한다. 또한, 데이터 타입의 크기가 초기엔 유의미하지 않더라도 데이터가 많이 쌓일수록 용량 차이가 생각보다 엄청난다고 한다.
따라서, 두 타입을 모두 테스트해보고자 한다.
테스트 테이블
테스트용 테이블 구조는 다음과 같다.
테스트 환경은 모두 Asia/Seoul(KST) 를 기반으로 진행해보자.
DB Connection (Spring)
DB Server (MySQL)
Application Server (JVM, OS)
1. 타입별 저장 테스트
위 테이블을 기준으로 아래와 같이 엔티티를 구성해준다.
이 엔티티를 기준으로 다음과 같이 테스트를 진행해보았다.
결과를 표로 정리를 해보자.
lDatetime
LocalDateTime
으로 저장된 값
2025-04-03T21:43:11
zDatetime
ZonedDateTime
으로 저장된 값
(UTC로 변환되어 저장)
2025-04-03T12:43:11Z
lDatetimeDefault
DATETIME DEFAULT CURRENT_TIMESTAMP
(기본값, LocalDateTime 저장)
2025-04-03T21:43:11
zDatetimeDefault
DATETIME DEFAULT CURRENT_TIMESTAMP
(기본값, UTC로 변환된 ZonedDateTime)
2025-04-03T21:43:11Z
lTimestamp
Timestamp
로 저장된 값
(LocalDateTime 기반)
2025-04-03 21:43:11.0
zTimestamp
Timestamp
로 저장된 값
(ZonedDateTime 기반, UTC 변환 후 Local 시간으로 저장)
2025-04-03 21:43:11.0
lTimestampDefault
TIMESTAMP DEFAULT CURRENT_TIMESTAMP
(기본값, LocalDateTime 기반)
2025-04-03 21:43:11.0
zTimestampDefault
TIMESTAMP DEFAULT CURRENT_TIMESTAMP
(기본값, ZonedDateTime 기반)
2025-04-03 21:43:11.0
위의 표를 보고 이상하게 느껴지는 부분이 있다.
왜 ZonedDateTime.now() 를 기반으로 만든 Timestamp의 값은 동일하지?
왜 ZonedDateTime.now() 값을 넣은 컬럼만 값이 다르지?
TIMESTAMP DEFAULT CURRENT_TIMESTAMP
의 경우 UTC로 저장이 되는게 아닌 DB 서버 타임에 맞춰지는거지?
애플리케이션 단에서 조회를 해서 다르게 나온것일까? DB에는 어떻게 저장이 되는지 보자.
모든 시간을 KST로 맞춰두었기 때문에 모두 동일하게 저장될 것으로 예상했는데 실제로 저장된 모습을 보니 다르다!
어떤 이유로 이러한 문제가 발생한걸까?
1. 왜 ZonedDateTime.now() 를 기반으로 만든 Timestamp의 값은 동일하지?
이 문제의 경우 Timestamp
로 변환하는 과정에서 발생한다.
Timestamp는 ZonedDateTime 처럼 Zone Id, Offset 정보를 저장할 수 없다.
Timestamp.valueOf(zonedNow.toLocalDateTime()))
으로 변환하는 시점에 해당 정보들은 날라간다.
아무 생각없이 코드를 작성하다보니 이걸 놓쳤다,,
결론
ZonedDateTime 타입을 Timestamp 타입으로 변환할 때 타임 존 정보는 날아간다!
2. 왜 ZonedDateTime.now() 값을 넣은 컬럼만 값이 다르지?
이 문제는 두 가지 측면을 봐야 한다.
MySQL의 DATETIME 특성
DATETIME 자료형은 시간대(Zone)와 오프셋(Offset) 정보를 저장하지 않는다.
입력된 값 그대로 저장하며, 조회시 그대로 반환한다.
JPA/Hibernate 동작 방식
ZonedDateTime 을 DATETIME 컬럼에 저장해야 한다.
이 때, UTC로 변환을 한다. (KST
2025-04-03T21:43:11+09:00
→ UTC2025-04-03T12:43:11Z
)
조회 시 저장된 UTC 값을
ZonedDateTime
객체로 반환하지만, 시간대와 오프셋 정보를 몰라 복원되지는 않는다.반환 결과: 2025-04-03T12:43:11Z (UTC)
SAVE 호출시 파라미터 바인딩이 어떻게 되는지 확인해보자.
TIMESTAMP_UTC 가 뭘까? 이 것 때문에 UTC 형태로 변환되어서 저장이 되는 것으로 보인다. 모두 KST(Asia/Seoul)로 변경해뒀는데 왜 이러한 일이 일어났을까?
hibernate.timezone.default_storage
옵션을 통해 시간대 관련 정보를 다루며 실제 DB 테이블에 저장하고 읽을 때 데이터를 어떤 식으로 연결시킬지 설정한다.
TimeZoneStorageType.java
별도의 설정이 없었기에 DEFAULT
동작을 따르고 있었다.
DB 레벨에서 시간대 정보를 관리할 수 있는 경우, 시간대 정보가 DB 테이블에 함께 저장 (
NATIVE
와 동일)DB 레벨에서 시간대 정보를 관리할 수 없는 경우, UTC 시간대로 연결해서 저장 (
NORMALIZE_UTC
와 동일)
MySQL에서는 시간대 정보를 관리할 수 있는 컬럼이 없기에 임의로 UTC 시간대로 변경해서 저장하게 된다면, 데이터 정합성의 문제는 물론 애플리케이션 로직에도 큰 영향을 미칠 수 있을 것이다.
위의 문서에서도 Hibernate 5 버전의 하위 호환성을 고려하는 경우 NORMALIZE
옵션을 사용하라고 제공되어있다.
NATIVE
timestamp with time zone
SQL 타입 사용
(PostgreSQL, Oracle)
O
NORMALIZE
시간대 저장하지 않음 (JVM 기본 시간대로 정규화)
X
NORMALIZE_UTC
시간대 저장하지 않음 (UTC로 정규화)
X
COLUMN
별도 컬럼에 시간대 정보 저장
O
AUTO
NATIVE 지원 시 NATIVE 사용, 아니면 COLUMN 사용
O
DEFAULT
NATIVE 지원 시 NATIVE 사용, 아니면 NORMALIZE_UTC
조건부 (DB에 따라)
이 문제를 해결하기 위해 NORMALIZE
옵션으로 변경하고 저장 테스트를 진행해보자.
저장 또한, UTC가 아닌 KST로 저장이 되었으며, 조회를 통해 값을 가져올 때도 Zone 정보를 함께 가져와준다!
결론
Hibernate 6.2 부터 시간대 관련 정보를 처리하는 방식이 달라졌다.
기존처럼 UTC 정규화가 필요한 경우
hibernate.timezone.default_storage
를NORMALIZE
로 사용하자.그렇지 않을 경우, 단순 UTC로 정규화한다.
3. TIMESTAMP DEFAULT CURRENT_TIMESTAMP
의 경우 UTC로 저장이 되는게 아닌 DB 서버 타임에 맞춰지는거지?
TIMESTAMP DEFAULT CURRENT_TIMESTAMP
의 경우 UTC로 저장이 되는게 아닌 DB 서버 타임에 맞춰지는거지?MySQL 공식문서를 살펴보자.
사실 CURRENT_TIMESTAMP와 NOW()는 동의어다. 즉, 동일하게 동작하는 것이다.
그렇다면 NOW() 는 어떤 방식으로 시간을 얻어올까?
The value is expressed in the session time zone. 값은 세션 시간대에 따라 달라집니다.
Now() 의 값은 session time zone 으로 나타내게 되는 것이다. 이로 인해 UTC 형태로 저장되는 것이 아닌 KST로 저장이 되고 있다.
이렇게 저장이 되는거면 UTC 기준으로 저장을 가정하는 TIMESTAMP 타입에 올바르게 값을 넣은 것인가?
반대로 Application Level 에서 TIMESTAMP 값을 만들어서 넣어준다면?
@CreationTimestamp
를 사용해서 저장해보면 어떨까?저장(SAVE) 시점에 UTC 형태로 저장이 될까?
조회(SELECT) 시점에 KST 형태로 변환이 될까?
그래도 전체적으로 KST 형태로 반환되네..? 잘 모르겠다.
VM, Connection, DB Server 모두 KST 로 맞춰두면 UTC를 사용해서 저장하는 TIMESTAMP 도 KST를 따라가는건지 아니면 내가 UTC로 볼 수 없는 환경을 구성해놓은건지
세션의 타임존을 변경해서 봐보자!
DB 내부적으로 UTC 형태로 저장을 하고 있었지만, 세션 타임존에 따라 변형해서 보여준 것이었다. 즉, 3번의 문제는 조회시 세션 타임존에 따라 변환해서 보여주는 문제로 인해 내가 오해하고 있던 것이었다.
진짜 취지에 맞게 계속해서 삽질을 하고 있다 😅
결론
TIMESTAMP 는 항상 UTC 형태로 저장한다.
세션 시간대에 따라 조회 시 자동으로 변환해서 보여준다.
2. 조회 테스트
날짜와 관련된 컬럼에 대해서 조회에 대한 테스트를 진행해보자.
조회를 진행할 때 DB 상의 데이터가 UTC 기준인 경우, 어떻게 조회가 될까?
Asia/Seoul, UTC 중 어떤 시간을 기준으로 조회해야할까?
테스트를 진행하기 전 예상하는 것은 DB Connection 타임존이 영향을 미칠 것으로 보인다.
테스트 데이터는 공통적으로 해당 데이터를 기준으로 테스트를 진행하고자 한다.
조회 테스트에 대한 흐름은 위의 흐름도를 따를 것이다. 여기서 궁금한 점은 타임존 세팅에 따라 해당 값을 못가져오는 경우도 발생할까?
모든 타임존이 같다면 정상 처리가 될 것 같다.
DB 서버만 타임존이 다르다면?
DB 서버와 DB Connection 타임존이 다르다면?
DB Connection 타임존만 다르다면?
JVM 시간은 별도의 영향이 없을까?
하나씩 테스트해보자.
2-1. LocalDateTime 기준으로 조회
DB Connection(serverTimezone): Asia/Seoul
DB Connection(serverTimezone): UTC
동일하게 호출했는데 DB Connection Timezone 설정이 달라졌다고 값이 나오질 않는다.
파라미터 바인딩을 보면 그대로 값을 넣은 것 같은데 왜 조회가 되지 않는걸까?
serverTimezone 설정으로 인해 DB 내부에 저장된 값들은 모두 UTC 기준으로 저장된 것으로 판단한 것인가? 따라서 VM(OS) 상에서의 시간인 KST(+09:00) 를 UTC로 컨버팅 하는 상황에서 요청 시간이 -9:00 가 되버린 것인가?
GET localhost:8080/api/timezone/local-datetime?datetime=2025-04-04T00:05:54
2025-04-04T00:05:54를 자바 애플리케이션이 읽음
자바 애플리케이션은 KST / DB Connection은 UTC
서로 타임존 설정이 달라 조회 쿼리를 날릴 때 'binding parameter (1:TIMESTAMP) <- [2025-04-04T00:05:54]' 라고 나와있지만, Instant 형태로 변환하면서 -9시간이 됨.
실제로 DB에서 찾는 값은 '2025-04-0315:05:54' 이 되어버림.
일치하는 값이 없음
예상대로 시간을 KST(UTC+09:00) 형태로 쏴주니까 가져오긴 한다. UTC -> KST로 변환한다고 +09:00 가 되어있다.
KST
KST
KST
O
KST
KST
UTC
O (DATETIME 특성)
KST
UTC
UTC
X (-09:00 기준 조회)
LocalDateTime
의 경우 별도의 타임존 설정이 없기에 조회 쿼리를 진행할 때 DB Connection 설정에 따라가는 것을 볼 수가 있다.
2-2. ZonedDateTime 기준으로 조회
그럼 ZonedDateTime에선 어떨까?
DB Connection: Asia/Seoul
안나온다.. Zone 정보를 UTC라고 명시를 해뒀는데도 왜이러지?
DB Connection이
Asia/Seoul
로 설정되어 있으므로, Hibernate는 이 값을 KST(UTC+9)로 변환하려고 시도변환 결과:
2025-04-04T09:05:54+09:00[Asia/Seoul]
그럼 반대로 9시간을 빼주면 정상적으로 값이 나오는 것일까?
값을 가져오긴 했는데 데이터 정합성이 다 깨져버린다.
WHERE 절에 사용할 때는 내부적으로 15:05:54Z -> 00:05:54로 변환
매칭되는 값을 그대로 뽑아와서 보여줌 (DATETIME 특징)
DB Connection: UTC
UTC로 변경해도 값이 이상하게 나온다.
위에서 언급했던 hibernate.timezone.default_storage
의 영향으로 인해서 발생하는 문제로 보인다.
hibernate.timezone.default_storage: NORMALIZE_UTC
로 변경해보자.
ZonedDateTime
은 별도의 타임존 정보가 존재하기에 해당 정보를 기반으로 가져오되, DB Connection 설정과 위에서 언급했던 hibernate.timezone.default_storage
를 통해 내부적으로 처리가 된다.
여기까지만 보더라도 모든 타임존을 하나로 동일하게 맞춰야하는 이유에 대해서 파악했을 것이다. 게다가 전체적으로 UTC로 통일하는 방식이 오류를 최대한 없애는데 가장 가까운 방법으로 보인다. (VM, DB, Hibernate 등 영향을 미치는게 많다.)
2-3. Timestamp 기준으로 조회
DB Connection: Asia/Seoul
DB Connection: UTC
Timezone은 위의 LocalDateTime
, ZonedDateTime
과는 다르게 VM 옵션을 제외한 모든 것을 UTC로 설정해둬도 문제가 발생한다.
VM 옵션으로 UTC 설정 (모든 설정을 UTC)
역시나 예상했던 대로, 모든 시간을 UTC로 맞추는게 오류가 없이 정상적으로 되는 것으로 보인다.
시간에 관련해서 문제를 일으킬 수 있는 부분이 너무 많다. 영향을 미치는게 너무 다양하게 존재한다. 이러한 문제를 해결하기 위해서는 팀 내에서 시작하기 전 '시간'을 크리티컬하게 다루는 서비스 뿐 만 아니라 어떤 시스템이더라도 정해놓고 시작을 하면 나중에 문제를 일으킬 일은 없을 것으로 보인다.
결론
MySQL을 사용한다는 가정하에 아래와 같은 결론을 내렸다.
서비스의 특성에 따라 적절한 시간 타입을 선택해야 한다. 그리고 데이터를 올바르게 처리하기 위해서는 다음과 같은 점을 고려하자.
모든 시간 관련 설정을 일관되게 유지하자. (클라이언트와 서버가 통신할 때에도)
UTC를 기준 타임존으로 사용하는 것은 글로벌 서비스에서 유리하다.
Hibernate 6.2 이상에서는 타임존 처리 방식이 변경되어 명시적인 설정이 필요하다.
시간 데이터 처리 정책을 프로젝트 초기에 명확하게 정하자.
시간 데이터 처리는 다양한 계층(JVM, DB Connection, DB Server, Hibernate)의 타임존 설정이 모두 영향을 줄 수 있으니 조심하자.
Last updated
Was this helpful?