본문 바로가기
CHONNOM Study/JPA Testing

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

by Ramos 2025. 4. 5.

테스트 환경

  • Java 21, Spring Boot 3.4.4, Spring Data JPA/QueryDSL
  • MySQL 8.0.36

TODO

  • Java의 타입과 MySQL/PostgreSQL의 타입 매핑 및 관리포인트를 생각해본다.
    • JPA 엔티티 매핑 시 LocalDateTime vs ZonedDateTime
    • 서버의 타임존 셋팅 처리 방법
      • MySQL Connection 시 timezone 처리도 고려한다.
      • JVM Option이 별도로 필요한지 체크해본다.
      • MySQL 서버 자체의 타임존 설정이 필요한지 체크해본다. (Ubuntu Server, MySQL 애플리케이션)
    • LocalDateTime.now() 처리 후 저장하는 방식은 항상 유효한 값일까? 타임 데이터를 저장하는 시점에 대해서.
      • @EnableJpaAuditing, @CreatedDate@LastModifiedDate
  • 쿼리 시 어떤 것을 기준으로 타임을 처리해야 올바른 데이터를 가져올 수 있을까?
    • JPA로 쿼리 호출 시 타임 데이터를 어떻게 호출하는지 확인해본다.

Test Case

  • LocalDateTime, ZonedDateTime 타입 각각으로 더미데이터를 insert 해준다.
    • _default 컬럼들은 DB에서 직접 현재시간을 기준으로 처리한다. Table 구성 시 DATETIME DEFAULT CURRENT_TIMESTAMP를 사용.
  • DB상의 더미 데이터가 UTC 기준인 경우, 해당 컬럼을 기준으로 날짜 조회 시(eq, between 등) 날짜 데이터가 Asia/Seoul, UTC 등 데이터 혹은 별도의 처리가 들어간 시간으로 조회해본다.
  • 추가적으로 ISO 8601 UTC String 포멧으로 Controller에서 입력받아 INSERT, SELECT 처리도 테스트해본다.

예시 테이블은 아래와 같다.

CREATE TABLE `timezone_test_ramos` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `l_datetime` datetime DEFAULT NULL,
  `z_datetime` datetime DEFAULT NULL,
  `l_datetime_default` datetime NULL DEFAULT CURRENT_TIMESTAMP,
  `z_datetime_default` datetime NULL DEFAULT CURRENT_TIMESTAMP,
  `l_timestamp` timestamp NULL DEFAULT NULL,
  `z_timestamp` timestamp NULL DEFAULT NULL,
  `l_timestamp_default` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `z_timestamp_default` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
package com.chonnom.jpatest.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.DynamicInsert;

/**
 * TimeZone 테스트를 위한 JPA Entity.
 *
 * @author HakHyeon Song
 */
@Getter
@Builder
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@DynamicInsert
@Table(name = "timezone_test_ramos")
public class TimeZoneTestEntity {

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

    @Column(name = "l_datetime")
    private LocalDateTime lDateTime;
    @Column(name = "z_datetime")
    private ZonedDateTime zDateTime;
    @Column(name = "l_datetime_default")
    private LocalDateTime lDateTimeDefault;
    @Column(name = "z_datetime_default")
    private ZonedDateTime zDateTimeDefault;

    @Column(name = "l_timestamp")
    private LocalDateTime lTimestamp;
    @Column(name = "z_timestamp")
    private ZonedDateTime zTimestamp;
    @Column(name = "l_timestamp_default")
    private LocalDateTime lTimestampDefault;
    @Column(name = "z_timestamp_default")
    private ZonedDateTime zTimestampDefault;
}

Case 1. Java의 LocalDateTime, ZonedDateTime과 MySQL의 DATETIME, TIMESTAMP를 비교해보자.

현재 MySQL의 타임존 설정은 KST를 기준으로 처리해둔 상태다. 이 후 해당 설정은 바꾸면서 케이스를 정리할 예정이다.

먼저 MySQL의 DATETIME, TIMESTAMP 타입을 비교해보자.

https://dev.mysql.com/doc/refman/8.4/en/datetime.html

위 공식 문서를 요약해보면 다음과 같다.

  • 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 UTC 부터 2038-01-19 03:14:07 UTC

결국, DATETIME은 그냥 저장이고 TIMESTAMP는 자동 변환 후 저장한다.

  • 참고로, 현재 JDBC Connection 옵션 중 url에 ?serverTimezone=UTC 설정을 하지 않은 상태다.
  • 1, 2번 데이터는 @DynamicInsert 처리를 하지 않았을 경우 DB 컬럼에서 default 값으로 채워넣지 않았던 상황이고 3, 4번 데이터는 해당 처리가 된 상황에 해당한다.
  • 하지만, _default 컬럼들이 KST 기준으로 들어가고 있다.

application.yaml 내 jdbc url 설정에 ?serverTimezone=UTC 를 추가해보자

spring:
  application:
    name: JPA-Test
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${IP주소}:{PORT}/jpa-test?serverTimezone=UTC
    username: ${username}
    password: ${password}
  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
    open-in-view: false
    show-sql: true
    hibernate:
      ddl-auto: none

logging:
  level:
    org.hibernate.orm.jdbc.bind: trace

내 PC 환경은 한국시간 4월 5일 17시 38분이었으나, DB에 들어간 데이터는 2025-04-05 08:38:32 로 9시간이 빼진 채로 들어갔다. 단 DB에서 default 처리를 했던 컬럼들에 대해선 DB 자체가 KST 타임존으로 설정되어 있으므로 한국 시간인 2025-04-05 17:38:32로 저장되어 있다.

 

다만, Spring Boot 자체에서 현재 별도로 타임존 설정 혹은 JVM 옵션으로 지정한 사항이 없기 때문에 로그가 어떻게 찍혔는지 확인은 필요하다.

Hibernate: insert into timezone_test_ramos (l_datetime,l_timestamp,z_datetime,z_timestamp) values (?,?,?,?)
2025-04-05T17:38:32.451+09:00 TRACE 31744 --- [JPA-Test] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (1:TIMESTAMP) <- [2025-04-05T17:38:32.355650]
2025-04-05T17:38:32.451+09:00 TRACE 31744 --- [JPA-Test] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (2:TIMESTAMP) <- [2025-04-05T17:38:32.355650]
2025-04-05T17:38:32.451+09:00 TRACE 31744 --- [JPA-Test] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (3:TIMESTAMP_UTC) <- [2025-04-05T17:38:32.355666+09:00[Asia/Seoul]]
2025-04-05T17:38:32.452+09:00 TRACE 31744 --- [JPA-Test] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (4:TIMESTAMP_UTC) <- [2025-04-05T17:38:32.355666+09:00[Asia/Seoul]]
Hibernate: select tzte1_0.id,tzte1_0.l_datetime,tzte1_0.l_datetime_default,tzte1_0.l_timestamp,tzte1_0.l_timestamp_default,tzte1_0.z_datetime,tzte1_0.z_datetime_default,tzte1_0.z_timestamp,tzte1_0.z_timestamp_default from timezone_test_ramos tzte1_0
TimeZoneTestEntity(id=1, lDateTime=2025-04-06T02:03:50, zDateTime=2025-04-05T08:03:50Z, lDateTimeDefault=null, zDateTimeDefault=null, lTimestamp=2025-04-06T02:03:50, zTimestamp=2025-04-05T08:03:50Z, lTimestampDefault=null, zTimestampDefault=null)
TimeZoneTestEntity(id=2, lDateTime=2025-04-06T02:05:25, zDateTime=2025-04-05T08:05:25Z, lDateTimeDefault=null, zDateTimeDefault=null, lTimestamp=2025-04-06T02:05:25, zTimestamp=2025-04-05T08:05:25Z, lTimestampDefault=null, zTimestampDefault=null)
TimeZoneTestEntity(id=3, lDateTime=2025-04-06T02:07:08, zDateTime=2025-04-05T08:07:08Z, lDateTimeDefault=2025-04-06T02:07:08, zDateTimeDefault=2025-04-05T17:07:08Z, lTimestamp=2025-04-06T02:07:08, zTimestamp=2025-04-05T08:07:08Z, lTimestampDefault=2025-04-06T02:07:08, zTimestampDefault=2025-04-05T17:07:08Z)
TimeZoneTestEntity(id=4, lDateTime=2025-04-06T02:08:12, zDateTime=2025-04-05T08:08:12Z, lDateTimeDefault=2025-04-06T02:08:12, zDateTimeDefault=2025-04-05T17:08:12Z, lTimestamp=2025-04-06T02:08:12, zTimestamp=2025-04-05T08:08:12Z, lTimestampDefault=2025-04-06T02:08:12, zTimestampDefault=2025-04-05T17:08:12Z)
TimeZoneTestEntity(id=5, lDateTime=2025-04-05T17:38:32, zDateTime=2025-04-05T08:38:32Z, lDateTimeDefault=2025-04-06T02:38:32, zDateTimeDefault=2025-04-05T17:38:32Z, lTimestamp=2025-04-05T17:38:32, zTimestamp=2025-04-05T08:38:32Z, lTimestampDefault=2025-04-06T02:38:32, zTimestampDefault=2025-04-05T17:38:32Z)
  • lDateTime : LocalDateTime , datetime → DB에 담긴 데이터보다 +9시간 후 반환
  • zDateTime : ZonedDateTime, datetime → DB에 담긴 데이터 그대로 반환
  • lTimestamp : LocalDateTime, timestamp → DB에 담긴 데이터보다 +9시간 후 반환
  • zTimestamp : ZonedDateTime, timestamp → DB에 담긴 데이터 그대로 반환

이제 ?serverTimezone=UTC jdbc url 옵션을 제거한 뒤 조회해보면 아래와 같은 조회 결과가 나타난다.

Hibernate: select tzte1_0.id,tzte1_0.l_datetime,tzte1_0.l_datetime_default,tzte1_0.l_timestamp,tzte1_0.l_timestamp_default,tzte1_0.z_datetime,tzte1_0.z_datetime_default,tzte1_0.z_timestamp,tzte1_0.z_timestamp_default from timezone_test_ramos tzte1_0
TimeZoneTestEntity(id=1, lDateTime=2025-04-05T17:03:50, zDateTime=2025-04-05T08:03:50Z, lDateTimeDefault=null, zDateTimeDefault=null, lTimestamp=2025-04-05T17:03:50, zTimestamp=2025-04-05T08:03:50Z, lTimestampDefault=null, zTimestampDefault=null)
TimeZoneTestEntity(id=2, lDateTime=2025-04-05T17:05:25, zDateTime=2025-04-05T08:05:25Z, lDateTimeDefault=null, zDateTimeDefault=null, lTimestamp=2025-04-05T17:05:25, zTimestamp=2025-04-05T08:05:25Z, lTimestampDefault=null, zTimestampDefault=null)
TimeZoneTestEntity(id=3, lDateTime=2025-04-05T17:07:08, zDateTime=2025-04-05T08:07:08Z, lDateTimeDefault=2025-04-05T17:07:08, zDateTimeDefault=2025-04-05T17:07:08Z, lTimestamp=2025-04-05T17:07:08, zTimestamp=2025-04-05T08:07:08Z, lTimestampDefault=2025-04-05T17:07:08, zTimestampDefault=2025-04-05T17:07:08Z)
TimeZoneTestEntity(id=4, lDateTime=2025-04-05T17:08:12, zDateTime=2025-04-05T08:08:12Z, lDateTimeDefault=2025-04-05T17:08:12, zDateTimeDefault=2025-04-05T17:08:12Z, lTimestamp=2025-04-05T17:08:12, zTimestamp=2025-04-05T08:08:12Z, lTimestampDefault=2025-04-05T17:08:12, zTimestampDefault=2025-04-05T17:08:12Z)
TimeZoneTestEntity(id=5, lDateTime=2025-04-05T08:38:32, zDateTime=2025-04-05T08:38:32Z, lDateTimeDefault=2025-04-05T17:38:32, zDateTimeDefault=2025-04-05T17:38:32Z, lTimestamp=2025-04-05T08:38:32, zTimestamp=2025-04-05T08:38:32Z, lTimestampDefault=2025-04-05T17:38:32, zTimestampDefault=2025-04-05T17:38:32Z)
  • lDateTime : LocalDateTime , datetime → DB에 담긴 데이터 그대로 반환
  • zDateTime : ZonedDateTime, datetime → DB에 담긴 데이터 그대로 반환
  • lTimestamp : LocalDateTime, timestamp → DB에 담긴 데이터보다 그대로 반환
  • zTimestamp : ZonedDateTime, timestamp → DB에 담긴 데이터 그대로 반환

이 결과를 종합해보면, Java의 LocalDateTime은 DB에서 datetime, timestamp 타입 어떤 것을 매핑하더라도 DB connection을 연결한 session의 타임존이 어떠한지에 영향을 받는다. 반대로, ZonedDateTime은 타임존이 어떠한지에 영향을 받지 않고 일관적으로 UTC 기준으로 데이터를 저장하고 조회한다.

MySQL의 DATETIME은 시간대 정보를 포함하지 않기 때문에, Java에서 이를 단순한 LocalDateTime으로 다루면 JVM 기본 타임존이 반영되어 변환될 수 있다. 반면, TIMESTAMP는 내부적으로 UTC로 저장되고 세션 타임존에 따라 자동 변환되므로, Java에서 ZonedDateTime과 같은 시간대 정보를 명시적으로 매핑하면, DB connection 세션의 타임존 설정과 관계없이 UTC 기준으로 저장 및 조회를 보다 일관되게 할 수 있다.

즉, JDBC나 Hibernate 설정에서 별도의 타임존 옵션을 지정하지 않은 경우, ZonedDateTime 타입을 사용하면 올바른 시간대 정보를 보존할 수 있지만, 이 경우에도 데이터베이스 서버의 설정이나 드라이버의 동작 방식에 유의해야 한다. 추가적으로, 연결 문자열(JDBC URL)에서 serverTimezone=UTC 같은 옵션을 사용하는 것도 고려해볼 수 있다.

Case 2. ZonedDateTime 타입으로 매핑하여 UTC 기준 값으로 처리해보자. MySQL 서버의 타임존 설정은 UTC로 변경한다.

MySQL, Backend VM 등 뒷단의 모든 서버는 UTC 타임존으로 설정해서 배포하는게 일관적인 형태로 데이터를 유지할 수 있다. 앞선 테스트 결과를 토대로 ZonedDateTime 타입으로 컬럼을 매핑하는게 외부의 영향 없이 UTC 기준 값으로 처리하므로 보다 일관적으로 관리할 수 있다. 이 부분은 MySQL 환경이 생각보다 편리한 것 같다. (필자의 경험 상 PostgreSQL에선 이러지 않았음...)

 

DATETIME, TIMESTAMP 타입 중 어떤 것을 사용할 지는 다음 사항을 고려하는게 좋을 것 같다.

  • 서비스의 규모(국내 한정 or 글로벌)에 따라 정하긴 하지만 추 후 확장성까지 고려한다면 TIMESTAMP로 초기부터 두고 관리를 하거나 DATETIME으로 시작 후 규모가 커졌을 때 데이터 마이그레이션을 진행하는 방식으로 팀에서 협의 후 설계해야 할 것 같다.
  • 두 번째로 고려해야 하는 사항은, 해당 데이터 타입의 크기가 얼마나 되냐를 따진다. TIMESTAMP 타입과 DATETIME 타입이 각각 4bytes, 8bytes 인데 초기엔 유의미하진 않더라도 해당 테이블에 데이터가 많이 쌓일 수록 용량 차이가 생각보다 엄청나다.

이제 다시 테스트로 돌아오자. MySQL에 UTC 타임존으로 설정 해두고 jdbc-url에 serverTimezone=UTC를 설정한 뒤 데이터를 insert 해보면 아래와 같다.

ZonedDateTime.now()를 코드 상에서 처리하는게 옳을까?

DDL로 컬럼에 default current_timestamp을 두고 JPA 상의 @DynamicInsert 를 토대로 처리한 날짜 컬럼은 Insert 시 now() 쿼리를 사용한 것이라 DB 자체의 타임존 설정에 영향을 많이 받는다.

 

단, MySQL 서버를 UTC로 설정하고, 엔티티 컬럼에 DB의 default current_timestamp를 사용하면서 @DynamicInsert를 적용하면, 엔티티 생성 시점과 실제 DB에 기록되는 시간이 거의 일치하게 된다. 이 방식은 개발자가 직접 ZonedDateTime.now()와 같은 코드를 사용하여 타임스탬프를 지정할 필요를 줄여주며, 모든 타임스탬프가 UTC 기준으로 통일되어 시간대 관련 혼란을 방지할 수 있다.

 

LocalDateTime.now(), ZonedDateTime.now() 를 여러 라인에 작성한 코드에선 정말 일관성이 없는 결과가 나타나니 이런 문제를 회피할 수 있다.

 

결국 코드 상에서 LocalDateTime.now(), ZonedDateTime.now()를 작성해서 엔티티를 생성하고 xXXRepository.save(entity);를 남발하진 말자. DB 자체에서 타임존 설정이 모두 일관적으로 적용되었다면 insert 시 default now()를 DB가 처리하는게 더 정확한 데이터가 된다.

Case 3. 조회 쿼리에선 클라이언트로부터 날짜 데이터를 어떤 형식으로 받아와서 쿼리해야 할까?

이렇게까지 설정한 경우라면, 클라이언트와 서버간에선 ISO 8601 UTC String으로 소통하는게 옳다.

 

백단의 모든 날짜시간 데이터는 UTC 기준의 값으로 저장하고 조회하고 있음을 클라이언트 측은 반드시 알아야한다. 결국 화면을 그려주는 FE 개발자가 해당 UTC값을 클라이언트의 타임존을 토대로 보정해서 화면을 보여줄 수 있도록 처리해야 한다.

 

자, 그러면 이렇게 규칙이 정해졌다 가정하고 조회 쿼리를 테스트해보자. Spring Data JPA의 쿼리 메소드와 QueryDSL로 직접 조회했을 때, ZonedDateTime 타입으로 인해 값이 바뀌어서 쿼리가 되지 않는지도 확인이 필요하다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/timezone")
public class TimeZoneTestController {

    private final TimeZoneService timeZoneService;

    @GetMapping("/v1")
    public List<TestDto> test(@RequestParam ZonedDateTime start, @RequestParam ZonedDateTime end) {
        return timeZoneService.findV1(start, end);
    }

    @GetMapping("/v2")
    public List<TestDto> testV2(@RequestParam ZonedDateTime start, @RequestParam ZonedDateTime end) {
        return timeZoneService.findV2(start, end);
    }
}

 

ZonedDateTime 타입 그대로 ISO 8601 UTC String을 전달받아 Spring Data JPA와 QueryDSL 각각으로 쿼리를 해보자.

@Service
@RequiredArgsConstructor
public class TimeZoneService {

    private final TimeZoneRepository timeZoneRepository;

    @Transactional(readOnly = true)
    public List<TestDto> findV1(ZonedDateTime start, ZonedDateTime end) {
        return timeZoneRepository.findAllByzTimestampDefaultBetween(start, end)
                .stream()
                .map(TestDto::of)
                .toList();
    }

    @Transactional(readOnly = true)
    public List<TestDto> findV2(ZonedDateTime start, ZonedDateTime end) {
        return timeZoneRepository.findByzTimestampDefaultBetweenV2(start, end)
                .stream()
                .map(TestDto::of)
                .toList();
    }
}
public interface TimeZoneRepository extends JpaRepository<TimeZoneTestEntity, Long>, TimeZoneRepositoryCustom {

    List<TimeZoneTestEntity> findAllByzTimestampDefaultBetween(ZonedDateTime start, ZonedDateTime end);
}

@Repository
@RequiredArgsConstructor
public class TimeZoneRepositoryImpl implements TimeZoneRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public List<TimeZoneTestEntity> findByzTimestampDefaultBetweenV2(ZonedDateTime start, ZonedDateTime end) {
        QTimeZoneTestEntity timeZoneTestEntity = QTimeZoneTestEntity.timeZoneTestEntity;

        return queryFactory.selectFrom(timeZoneTestEntity)
                .where(timeZoneTestEntity.zTimestampDefault.between(start, end))
                .fetch();
    }
}

 

Query는 DB에서 DEFAULT로 default current_timestamp로 지정해둔 z_timestamp_default 컬럼을 기준으로 조회해본다. 앞서 MySQL에 UTC 타임존 설정을 해뒀기에 UTC 값으로 저장되어 있는 컬럼에 해당한다.

// v1 메소드 실행
Hibernate: select tzte1_0.id,tzte1_0.l_datetime,tzte1_0.l_datetime_default,tzte1_0.l_timestamp,tzte1_0.l_timestamp_default,tzte1_0.z_datetime,tzte1_0.z_datetime_default,tzte1_0.z_timestamp,tzte1_0.z_timestamp_default from timezone_test_ramos tzte1_0 where tzte1_0.z_timestamp_default between ? and ?
2025-04-05T21:15:29.851+09:00 TRACE 35344 --- [JPA-Test] [nio-8080-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter (1:TIMESTAMP_UTC) <- [2025-04-05T00:00Z]
2025-04-05T21:15:29.852+09:00 TRACE 35344 --- [JPA-Test] [nio-8080-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter (2:TIMESTAMP_UTC) <- [2025-04-05T23:59:59Z]

// v2 메소드 실행
Hibernate: select tzte1_0.id,tzte1_0.l_datetime,tzte1_0.l_datetime_default,tzte1_0.l_timestamp,tzte1_0.l_timestamp_default,tzte1_0.z_datetime,tzte1_0.z_datetime_default,tzte1_0.z_timestamp,tzte1_0.z_timestamp_default from timezone_test_ramos tzte1_0 where tzte1_0.z_timestamp_default between ? and ?
2025-04-05T21:19:10.040+09:00 TRACE 35344 --- [JPA-Test] [nio-8080-exec-5] org.hibernate.orm.jdbc.bind              : binding parameter (1:TIMESTAMP_UTC) <- [2025-04-05T00:00Z]
2025-04-05T21:19:10.041+09:00 TRACE 35344 --- [JPA-Test] [nio-8080-exec-5] org.hibernate.orm.jdbc.bind              : binding parameter (2:TIMESTAMP_UTC) <- [2025-04-05T23:59:59Z]

 

두 메소드의 실행 결과는 모두 동일했으며, Query를 호출할 때 전달받은 값 그대로 전달하여 별도의 변환 과정은 없었다.

 

결론

  • 타임존을 고려해서 날짜/시간 데이터를 관리할 땐, 표준화가 필요하다. 주로, UTC를 기준으로 처리하는게 좋다.
  • JPA와 MySQL 환경에서 Java의 데이터 타입과 MySQL의 데이터 타입을 적절하게 고려해야 한다.
    • 백단의 서버 모두 동일한 타임존으로 설정했다면, ZonedDateTime, TIMESTAMP 조합으로 매핑 후 처리하는 방법도 적절하단 생각이 든다.
    • 해당 DB 컬럼은 DDL로 구성할 때 default current_timestamp로 처리해두어 Java 코드에서 now()를 무분별하게 찍지 않도록 가이드 하는게 실제 엔티티 객체의 생성 시점과 DB에 실제 insert 된 시점이 불일치하는 현상을 최대한 막을 수 있다.
    • LocalDateTime 타입은 DB 서버 자체의 타임존이나 Connection을 맺은 클라이언트 세션의 타임존, JVM 자체 타임존 등의 영향을 많이 받는다. 이 타입을 기준으로 처리하고자 한다면 별도의 TimeZone Converter를 만들어서 로직으로 풀어내야 한다. 단, 버그 발생 요소가 다분할 수도 있겠다.