🌀
f1v3-log
  • Welcome
  • 개발
    • SecurityContext를 새로 만들어야 할까?
    • OAuth2AuthorizationRequestResolver 커스터마이징
    • 동시성 문제를 해결해보자
    • MySQL은 어떻게 ID 값을 순차적으로 넣어주는 것일까? (Feat. Auto Increment Lock)
    • 외부 API 호출에 대한 고찰
      • HTTP Clients in Spring Boot
      • I/O와 트랜잭션 분리하기
      • 처리율 제한 장치 (Rate Limiter) 도입
      • 외부 API 의존성을 줄여보자
      • 캐시 레이어를 구성해보자 (Local Cache)
    • JPA Deep Dive
      • 결제 및 정산 시스템 기능 요구사항 분석
      • 글로벌 서비스를 고려할 때, 타임존 이슈를 어떻게 처리해야 할까?
      • Spring Data JPA - ID 생성 전략과 채번은 어떻게 되는걸까?
  • 회고
    • NHN Academy 인증과정 회고
    • DND 11기 회고
  • 독서
    • Effective Java 3/E
      • Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라
      • Item 2. 생성자에 매개변수가 많다면 빌더를 고려하라
      • Item 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
    • 객체지향의 사실과 오해
      • 1장. 협력하는 객체들의 공동체
      • 2장. 이상한 나라의 객체
      • 3장. 타입과 추상화
      • 4장. 역할, 책임, 협력
      • 5장. 책임과 메시지
      • 6장. 객체 지도
      • 7장. 함께 모으기
  • Real MySQL 8.0
    • 04. 아키텍처
    • 05. 트랜잭션과 잠금
    • 08. 인덱스
    • 09. 옵티마이저와 힌트
  • 생각정리
    • 기술에 매몰되지 말자.
  • 공부
    • 객체지향 5원칙(SOLID)
      • SRP (Single Responsibility Principle)
      • OCP (Open Closed Principle)
Powered by GitBook
On this page
  • 목표
  • 테스트 테이블
  • 1. 타입별 저장 테스트
  • 1. 왜 ZonedDateTime.now() 를 기반으로 만든 Timestamp의 값은 동일하지?
  • 2. 왜 ZonedDateTime.now() 값을 넣은 컬럼만 값이 다르지?
  • 3. TIMESTAMP DEFAULT CURRENT_TIMESTAMP 의 경우 UTC로 저장이 되는게 아닌 DB 서버 타임에 맞춰지는거지?
  • 2. 조회 테스트
  • 2-1. LocalDateTime 기준으로 조회
  • 2-2. ZonedDateTime 기준으로 조회
  • 2-3. Timestamp 기준으로 조회
  • 결론

Was this helpful?

  1. 개발
  2. JPA Deep Dive

글로벌 서비스를 고려할 때, 타임존 이슈를 어떻게 처리해야 할까?

타임존을 지지고 볶아보면서 삽질을 해보자.

목표

글로벌 서비스를 개발한다면 시간 데이터 처리를 어떻게 해야할까? 다양한 타임존에서 일관된 시간 데이터를 처리하기 위해서는 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로 저장하고, 세션 타임존에 맞게 변환해서 반환

실무에서는 어떻게 데이터 타입을 정하게 되는걸까?

  1. 서비스의 규모 (국내 한정 / 글로벌)

  2. 확장성 고려 (데이터 동기화, 서비스 유지 기간)

  3. 데이터 타입의 크기 (8bytes vs. 4bytes)

이러한 측면들을 고려해서 기본적으로 TIMESTAMP를 사용하여 관리하거나 DATETIME으로 시작해 규모가 커졌을 때 데이터 마이그레이션을 진행하는 등 팀에서 협의 후 설계를 진행한다고 한다. 또한, 데이터 타입의 크기가 초기엔 유의미하지 않더라도 데이터가 많이 쌓일수록 용량 차이가 생각보다 엄청난다고 한다.

따라서, 두 타입을 모두 테스트해보고자 한다.

테스트 테이블

테스트용 테이블 구조는 다음과 같다.

CREATE TABLE timezone_test
(
    id                 BIGINT PRIMARY KEY AUTO_INCREMENT,
    l_datetime         DATETIME,
    z_datetime         DATETIME,
    l_datetime_default DATETIME DEFAULT CURRENT_TIMESTAMP,
    z_datetime_default DATETIME DEFAULT CURRENT_TIMESTAMP,
    l_timestamp         TIMESTAMP,
    z_timestamp         TIMESTAMP,
    l_timestamp_default TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    z_timestamp_default TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

테스트 환경은 모두 Asia/Seoul(KST) 를 기반으로 진행해보자.

  • DB Connection (Spring)

  • DB Server (MySQL)

  • Application Server (JVM, OS)

1. 타입별 저장 테스트

위 테이블을 기준으로 아래와 같이 엔티티를 구성해준다.

/**
 * Timezone Test Entity
 *
 * @author Seungjo, Jeong
 */
@ToString
@Getter
@Entity
@Table(name = "timezone_test")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicInsert
public class Timezone {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private LocalDateTime lDatetime;
    private ZonedDateTime zDatetime;
    private LocalDateTime lDatetimeDefault;
    private ZonedDateTime zDatetimeDefault;
    private Timestamp lTimestamp;
    private Timestamp zTimestamp;
    private Timestamp lTimestampDefault;
    private Timestamp zTimestampDefault;

    @Builder
    public Timezone(LocalDateTime lDatetime, ZonedDateTime zDatetime,
                    Timestamp lTimestamp, Timestamp zTimestamp) {
        this.lDatetime = lDatetime;
        this.zDatetime = zDatetime;
        this.lTimestamp = lTimestamp;
        this.zTimestamp = zTimestamp;
    }
}

이 엔티티를 기준으로 다음과 같이 테스트를 진행해보았다.

@Test
void test1() {
        LocalDateTime localNow = LocalDateTime.now();
        ZonedDateTime zonedNow = ZonedDateTime.now(ZoneId.of("Asia/Seoul"))

        Timezone timezone = Timezone.builder()
                .lDatetime(localNow)
                .zDatetime(zonedNow)
                .lTimestamp(Timestamp.valueOf(localNow))
                .zTimestamp(Timestamp.valueOf(zonedNow.toLocalDateTime()))
                .build();
                
        // 이외의 값은 DEFAULT 처리로 저장되게 해보자.

        timezoneRepository.save(timezone);

        List<Timezone> actual = timezoneRepository.findAll();
        assertFalse(actual.isEmpty());

        actual.forEach(System.out::println);
}

Timezone
(
    id=1,
    lDatetime=2025-04-03T21:43:11, 
    zDatetime=2025-04-03T12:43:11Z, 
    lDatetimeDefault=2025-04-03T21:43:11, 
    zDatetimeDefault=2025-04-03T21:43:11Z, 
    lTimestamp=2025-04-03 21:43:11.0, 
    zTimestamp=2025-04-03 21:43:11.0, 
    lTimestampDefault=2025-04-03 21:43:11.0, 
    zTimestampDefault=2025-04-03 21:43:11.0
)

결과를 표로 정리를 해보자.

컬럼 명
설명
저장된 값

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

위의 표를 보고 이상하게 느껴지는 부분이 있다.

  1. 왜 ZonedDateTime.now() 를 기반으로 만든 Timestamp의 값은 동일하지?

  2. 왜 ZonedDateTime.now() 값을 넣은 컬럼만 값이 다르지?

  3. TIMESTAMP DEFAULT CURRENT_TIMESTAMP 의 경우 UTC로 저장이 되는게 아닌 DB 서버 타임에 맞춰지는거지?

애플리케이션 단에서 조회를 해서 다르게 나온것일까? DB에는 어떻게 저장이 되는지 보자.

[
  {
    "id": 1,
    "l_datetime": "2025-04-03 21:43:11",
    "z_datetime": "2025-04-03 12:43:11",
    "l_datetime_default": "2025-04-03 21:43:11",
    "z_datetime_default": "2025-04-03 21:43:11",
    "l_timestamp": "2025-04-03 21:43:11",
    "z_timestamp": "2025-04-03 21:43:11",
    "l_timestamp_default": "2025-04-03 21:43:11",
    "z_timestamp_default": "2025-04-03 21:43:11"
  }
]

모든 시간을 KST로 맞춰두었기 때문에 모두 동일하게 저장될 것으로 예상했는데 실제로 저장된 모습을 보니 다르다!

어떤 이유로 이러한 문제가 발생한걸까?

1. 왜 ZonedDateTime.now() 를 기반으로 만든 Timestamp의 값은 동일하지?

.zTimestamp(Timestamp.valueOf(zonedNow.toLocalDateTime()))

이 문제의 경우 Timestamp로 변환하는 과정에서 발생한다.

  • Timestamp는 ZonedDateTime 처럼 Zone Id, Offset 정보를 저장할 수 없다.

  • Timestamp.valueOf(zonedNow.toLocalDateTime())) 으로 변환하는 시점에 해당 정보들은 날라간다.

아무 생각없이 코드를 작성하다보니 이걸 놓쳤다,,

결론

ZonedDateTime 타입을 Timestamp 타입으로 변환할 때 타임 존 정보는 날아간다!

2. 왜 ZonedDateTime.now() 값을 넣은 컬럼만 값이 다르지?

ZonedDateTime zonedNow = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));

Timezone timezone = Timezone.builder()
        // ...
        .zDatetime(zonedNow)
        // ...

이 문제는 두 가지 측면을 봐야 한다.

MySQL의 DATETIME 특성

  • DATETIME 자료형은 시간대(Zone)와 오프셋(Offset) 정보를 저장하지 않는다.

  • 입력된 값 그대로 저장하며, 조회시 그대로 반환한다.

JPA/Hibernate 동작 방식

  • ZonedDateTime 을 DATETIME 컬럼에 저장해야 한다.

    • 이 때, UTC로 변환을 한다. (KST 2025-04-03T21:43:11+09:00 → UTC 2025-04-03T12:43:11Z )

  • 조회 시 저장된 UTC 값을 ZonedDateTime 객체로 반환하지만, 시간대와 오프셋 정보를 몰라 복원되지는 않는다.

    • 반환 결과: 2025-04-03T12:43:11Z (UTC)

SAVE 호출시 파라미터 바인딩이 어떻게 되는지 확인해보자.

logging:
  level:
    org.hibernate.orm.jdbc.bind: trace  # 파라미터 바인딩 값 출력
[Hibernate] 
    insert 
    into
        timezone_test
        (l_datetime, l_timestamp, z_datetime, z_timestamp) 
    values
        (?, ?, ?, ?)
        
binding parameter (1:TIMESTAMP) <- [2025-04-03T23:04:04.463445]
binding parameter (2:TIMESTAMP) <- [2025-04-03 23:04:04.463445]
binding parameter (3:TIMESTAMP_UTC) <- [2025-04-03T23:04:04.463475+09:00[Asia/Seoul]]
binding parameter (4:TIMESTAMP) <- [2025-04-03 23:04:04.463475]

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 옵션으로 변경하고 저장 테스트를 진행해보자.

spring:
  jpa:
    properties:
      hibernate:
        timezone.default_storage: NORMALIZE

Timezone
(
    id=11, 
    lDatetime=2025-04-04T00:05:54, 
    zDatetime=2025-04-04T00:05:54+09:00[Asia/Seoul], 
    lDatetimeDefault=2025-04-04T00:05:53, 
    zDatetimeDefault=2025-04-04T00:05:53+09:00[Asia/Seoul], 
    lTimestamp=2025-04-04 00:05:54.0, 
    zTimestamp=2025-04-04 00:05:54.0, 
    lTimestampDefault=2025-04-04 00:05:53.0, 
    zTimestampDefault=2025-04-04 00:05:53.0
)

저장 또한, UTC가 아닌 KST로 저장이 되었으며, 조회를 통해 값을 가져올 때도 Zone 정보를 함께 가져와준다!

결론

  • Hibernate 6.2 부터 시간대 관련 정보를 처리하는 방식이 달라졌다.

  • 기존처럼 UTC 정규화가 필요한 경우 hibernate.timezone.default_storage 를 NORMALIZE 로 사용하자.

  • 그렇지 않을 경우, 단순 UTC로 정규화한다.

3. 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 형태로 변환이 될까?

    @CreationTimestamp
    private LocalDateTime lDatetimeDefault;
    
    @CreationTimestamp
    private ZonedDateTime zDatetimeDefault;

    @CreationTimestamp
    private Timestamp lTimestampDefault;

    @CreationTimestamp
    private Timestamp zTimestampDefault;

그래도 전체적으로 KST 형태로 반환되네..? 잘 모르겠다.

VM, Connection, DB Server 모두 KST 로 맞춰두면 UTC를 사용해서 저장하는 TIMESTAMP 도 KST를 따라가는건지 아니면 내가 UTC로 볼 수 없는 환경을 구성해놓은건지

[
  {
    "id": 3,
    "l_datetime": "2025-04-04 14:14:29",
    "z_datetime": "2025-04-04 14:14:29",
    "l_datetime_default": "2025-04-04 14:14:29",
    "z_datetime_default": "2025-04-04 14:14:29",
    "l_timestamp": "2025-04-04 14:14:29",
    "z_timestamp": "2025-04-04 14:14:29",
    "l_timestamp_default": "2025-04-04 14:14:29",
    "z_timestamp_default": "2025-04-04 14:14:29"
  }
]

세션의 타임존을 변경해서 봐보자!

SET time_zone = '+00:00';
[
  {
    "id": 3,
    "l_datetime": "2025-04-04 14:14:29",
    "z_datetime": "2025-04-04 14:14:29",
    "l_datetime_default": "2025-04-04 14:14:29",
    "z_datetime_default": "2025-04-04 14:14:29",
    "l_timestamp": "2025-04-04 05:14:29",
    "z_timestamp": "2025-04-04 05:14:29",
    "l_timestamp_default": "2025-04-04 05:14:29",
    "z_timestamp_default": "2025-04-04 05:14:29"
  }
]

DB 내부적으로 UTC 형태로 저장을 하고 있었지만, 세션 타임존에 따라 변형해서 보여준 것이었다. 즉, 3번의 문제는 조회시 세션 타임존에 따라 변환해서 보여주는 문제로 인해 내가 오해하고 있던 것이었다.

진짜 취지에 맞게 계속해서 삽질을 하고 있다 😅

결론

  • TIMESTAMP 는 항상 UTC 형태로 저장한다.

  • 세션 시간대에 따라 조회 시 자동으로 변환해서 보여준다.

2. 조회 테스트

날짜와 관련된 컬럼에 대해서 조회에 대한 테스트를 진행해보자.

  • 조회를 진행할 때 DB 상의 데이터가 UTC 기준인 경우, 어떻게 조회가 될까?

  • Asia/Seoul, UTC 중 어떤 시간을 기준으로 조회해야할까?

테스트를 진행하기 전 예상하는 것은 DB Connection 타임존이 영향을 미칠 것으로 보인다.

테스트 데이터는 공통적으로 해당 데이터를 기준으로 테스트를 진행하고자 한다.

"id": 11,
"l_datetime":             "2025-04-04 00:05:54",
"z_datetime":             "2025-04-04 00:05:54",
"l_datetime_default":     "2025-04-04 00:05:53",
"z_datetime_default":     "2025-04-04 00:05:53",
"l_timestamp":             "2025-04-03 15:05:54",
"z_timestamp":             "2025-04-03 15:05:54",
"l_timestamp_default":     "2025-04-03 15:05:53",
"z_timestamp_default":     "2025-04-03 15:05:53"

조회 테스트에 대한 흐름은 위의 흐름도를 따를 것이다. 여기서 궁금한 점은 타임존 세팅에 따라 해당 값을 못가져오는 경우도 발생할까?

  1. 모든 타임존이 같다면 정상 처리가 될 것 같다.

  2. DB 서버만 타임존이 다르다면?

  3. DB 서버와 DB Connection 타임존이 다르다면?

  4. DB Connection 타임존만 다르다면?

  5. 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

  1. 2025-04-04T00:05:54를 자바 애플리케이션이 읽음

  2. 자바 애플리케이션은 KST / DB Connection은 UTC

  3. 서로 타임존 설정이 달라 조회 쿼리를 날릴 때 'binding parameter (1:TIMESTAMP) <- [2025-04-04T00:05:54]' 라고 나와있지만, Instant 형태로 변환하면서 -9시간이 됨.

  4. 실제로 DB에서 찾는 값은 '2025-04-0315:05:54' 이 되어버림.

  5. 일치하는 값이 없음

예상대로 시간을 KST(UTC+09:00) 형태로 쏴주니까 가져오긴 한다. UTC -> KST로 변환한다고 +09:00 가 되어있다.

JVM (OS)
DB Connection
DB Server (MySQL)
결과

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)

java -Duser.timezone=UTC Application.java

역시나 예상했던 대로, 모든 시간을 UTC로 맞추는게 오류가 없이 정상적으로 되는 것으로 보인다.

시간에 관련해서 문제를 일으킬 수 있는 부분이 너무 많다. 영향을 미치는게 너무 다양하게 존재한다. 이러한 문제를 해결하기 위해서는 팀 내에서 시작하기 전 '시간'을 크리티컬하게 다루는 서비스 뿐 만 아니라 어떤 시스템이더라도 정해놓고 시작을 하면 나중에 문제를 일으킬 일은 없을 것으로 보인다.

결론

MySQL을 사용한다는 가정하에 아래와 같은 결론을 내렸다.

서비스의 특성에 따라 적절한 시간 타입을 선택해야 한다. 그리고 데이터를 올바르게 처리하기 위해서는 다음과 같은 점을 고려하자.

  1. 모든 시간 관련 설정을 일관되게 유지하자. (클라이언트와 서버가 통신할 때에도)

  2. UTC를 기준 타임존으로 사용하는 것은 글로벌 서비스에서 유리하다.

  3. Hibernate 6.2 이상에서는 타임존 처리 방식이 변경되어 명시적인 설정이 필요하다.

  4. 시간 데이터 처리 정책을 프로젝트 초기에 명확하게 정하자.

시간 데이터 처리는 다양한 계층(JVM, DB Connection, DB Server, Hibernate)의 타임존 설정이 모두 영향을 줄 수 있으니 조심하자.

Previous결제 및 정산 시스템 기능 요구사항 분석NextSpring Data JPA - ID 생성 전략과 채번은 어떻게 되는걸까?

Last updated 2 months ago

Was this helpful?

라인의 기술 블로그 () 내용을 보며 이러한 이유를 파악했다. Spring 3.1 버전부터 Hibernate 6.2 버전을 사용하면서 시간대(Time Zone) 정보를 다루는 방식이 달라졌다는 것이다.

[LINE] - 실전! Spring Boot 3 마이그레이션
MySQL :: MySQL 8.4 Reference Manual :: 14.7 Date and Time Functions
Logo
https://docs.jboss.org/hibernate/orm/6.2/migration-guide/migration-guide.html
https://docs.jboss.org/hibernate/orm/6.4/javadocs/org/hibernate/annotations/TimeZoneStorageType.html