프로덕트

아직 프로덕트가 없습니다.

아티클

전체 보기
나그

나그

[TroubleShooting] 스프링 부트 2에서 Instant 사용하기 2탄 - DB에 올바른 시간 입력하기

들어가며... & 이전 이야기

부크부크에서 LocalDateTime를 Instant로 변경하기로 한 나그!

여러 문서를 보고 Instant로 변경에 성공한다.

하지만 DB에 UTC 적용시가 아닌 이상한 시간(타임존이 적용된 시간)이 입력되는데...

일단 화면을 볼까...

이전의 코드에 더해 시간 조회 기능도 만들었고, data.sql을 통해 DB(Mysql)에 CURRENT_TIMESTAMP로 DB의 현재 시간도 찍었다. (코드는 여기에 있다. https://github.com/nag0124/laboratory)

그리고 테스트를 해보면...

  • DateTimeTest.java

@Test
void postNGet() {
    // given
    RestAssured.post("/date-time"); // 일단 시간을 찍고

	// when & then
    RestAssured.get("/date-time") // 조회하서 로그를 띄워주어라
            .then().log().all();
}

빨간색 부분이 data.sql을 통해 DB의 현재 시간을 찍은 것이고, 노란색 부분이 스프링에서 찍은 시간이다.

여기에서 알 수 있는 점을 정리해보면, (참고로 스프링의 타임존은 KST(+09:00)고 DB의 타임존은 UTC이다.)

  1. Spring에서 Instant를 찍어도 DB에는 Spring의 타임존이 적용되어 저장된다. (Spring 7시 입력 -> DB에 16시 저장) 

  2. 시간을 DB에서 찍고 Spring에서 Instant로 가져오면 Spring의 타임존이 역으로 적용된다. (DB 7시 저장 -> Spring에 전날 22시 입력)

  3. Spring에서 DB로 시간을 저장하고, 저장된 시간을 Spring에서 읽으면 값에 변동이 없다.(Spring 7시 입력 -> DB에 16시 저장 -> DB의 16시 읽음 -> Spring에 7시로 입력)

어...? 그러니까 쓰는데는 지장이 없는거 아닌가...?

위의 3번의 상황을 그림으로 그려보면 아래와 같다.

  1. Spring에서 Instant.now()메서드를 통해 7시의 Instant 객체를 만든다.

  2. 이 객체는 DB에`TIMESTAMP` 타입으로 16시의 데이터를 저장된다.

  3. DB에서 16시의 TIMESTAMP 데이터를 전달한다.

  4. Spring에서 이 데이터를 7시의 Instant 객체로 변환한다.

한마디로 지금 쓰는 데는 지장이 없다. 

이상한 값이 저장되기는 하지만, Spring에서는 값을 읽고 쓸 때 올바른 UTC값이 들어간다.

그리고 지금은 DB에서 시간을 찍지도 않는다.

하지만 안정적인 시스템이냐고 물어보면 그건 아닌 것 같다.

스프링의 타임존이 바뀐다면...? DB에서 시간을 찍게 된다면...? DB에 저장된 값들을 사용하기 위해선 따로 처리를 해야 할 것이다.

결국 나는 결정을 해야 했다.

양념이냐 후라이드냐, 짜장이냐 짬뽕이냐, 그냥 쓸 것인가 문제를 해결할 것인가...

나는 변동과 확장에 안정적인 시스템을 만들기 위해 나는 문제를 더 파기로 했다.

문제가 어디에서 발생할까?

디버깅을 해보고 로그를 확인해보니 Spring과 DB 사이에 이상한 점이 있었다.

  • Spring 로그

Hibernate: 
    insert 
    into
        date_time
        (created_time) 
    values
        (?)
2024-05-22 17:13:12.070 TRACE 6378 --- [o-auto-1-exec-2] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [TIMESTAMP] - [2024-05-22T08:13:12.040609Z]

  • DB 로그

2024-05-22T08:13:09.845061Z	  229 Query	SELECT @@session.transaction_read_only
2024-05-22T08:13:09.846066Z	  229 Query	INSERT INTO date_time (created_time) VALUES (CURRENT_TIMESTAMP)
2024-05-22T08:13:11.915183Z	  229 Query	SET autocommit=0
2024-05-22T08:13:12.088538Z	  229 Query	insert into date_time (created_time) values ('2024-05-22 17:13:12.040609')
2024-05-22T08:13:12.129760Z	  229 Query	commit
2024-05-22T08:13:12.133393Z	  229 Query	SET autocommit=1
2024-05-22T08:13:13.276707Z	  229 Quit

Spring에서는 로깅 옵션을 주어 쿼리에 바인딩되는 값을 출력하도록 했고, DB에서는 generl.log를 저장하도록 옵션을 주었다.

로그의 시간이 다른건 스프링은 OS 시간(KST)을 찍고 있고, DB는 Docker로 UTC 타임존을 셋팅 했기 때문이다.

DB 로그의 4번째 줄이 실제 INSERT 쿼리인데, 쿼리의 값이 Spring의 로그에서 보여주는 값과는 다르다.

(스프링: 2024-05-22T08:13:12, DB: 2024-05-22 17:13:12)

한마디로, Spring과 DB의 통신 과정에서 타임존이 적용되는 문제가 발생한다.

Instant 클래스는 타임존 정보를 가지고 있지 않지만, DB에 저장될 때는 타임존이 적용되어서 저장이 된다.

해결해보자.(1편)

이 문제에 대해서 검색을 하고 여러 글들을 참고해서 3가지의 해결책을 찾을 수 있었다.(셋 다 성공한다.)

  1. spring.datasource.url에 serverTimezone=UTC을 추가하여 서버 타임존 설정하기

  2. spring.jpa.hibernate.jdbc.time_zone=UTC을 추가하여 Hibernate의 JDBC의 타임존을 UTC로 설정하기

  3. Java 어플리케이션을 실행할 때 -Duser.timezone=UTC로 어플리케이션의 타임존 자체를 변경하기

이렇게 문제를 해결할 수는 있었지만, 찜찜했다.

어느 부분이, 어떻게 문제인지 모르고 "아~ 이렇게 하면 되는구나~" 이렇게 넘어가는 느낌이었다.

그래서 원인을 파악하고자 더 파보기로 했다.

문제를 해결한 지금 다시 생각해보면, 나는 여기서 멈췄어야 했다.

아니야... 그거 아니야... 하지마!!!!! 멈춰!!!!!!!!!!!

더 파보자...

검색하다 Mysql Connector/J의 개발자 가이드의 이 문서를 찾았다.

잠깐! Mysql Connector/J란?
자바는 DB 사이의 연결을 위해 JDBC(Java DataBase Connectivity) API를 사용한다.
이 중 Mysql을 쓰기 위한 JDBC 구현체가 Mysql Connector/J이다.

https://dev.mysql.com/doc/connector-j/en/connector-j-time-instants.html

이 글에서는 Mysql Connector/J를 사용하여 Mysql 특정 시점을 DB에 어떻게 보존할 수 있는지를 알려준다.(원제: Preserving Time Instant)

그리고 이 글에서 얻은 내용을 바탕으로 문제를 파악, 해결할 수 있었다. (아래 내용은 거의 이 문서의 번역본이다.)

문제 심층 해부

이 글을 읽고 정리해본 지금 상황은 아래와 같다.

7시의 Instant 객체가 DB에 저장하는 과정을 살펴보면, 

  1. Instant를 DB에 저장하기 위해 16시의 Timestamp 객체로 변환된다. (Instant는 UTC이고 Timestamp는 KST로 설정되이기에 9시간이 더해진다.)

  2. Connector/J의 시간대가 Spring과 동일하기에 16시를 변환하지 않고 그대로 DB Session에 전달한다.

  3. DB의 Timezone과 Session Timezone이 같기 때문에 변환하지 않고 UTC 16시가 저장된다.

  4. DB에서 Spring으로 값을 보내주는 것은 이 과정의 역순이다.

이 과정에서 내가 파악한 문제의 핵심은 Connector/J(KST)에서 넘겨주는 시간 값을 DB Session(UTC)에서 변동하지 않고 값을 그대로 읽는다는 것이다.

문제 해결 방안 탐색

먼저 DateTime과 관련된 Mysql의 여러 옵션을 살펴봤다.

  • connectionTimezone: Connector/J의 타임존을 설정하는 속성

    • LOCAL(default): 스프링(원문에서는 JVM이라고 지칭)의 타임존을 사용한다.

    • SERVER: 서버(DB)의 타임존을 사용한다.

    • User Defined Timezone: 서버, 로컬 타임존을 사용하지 않고 자유롭게 지정할 수 있다.(ex: connectionTimezone=Europe/Moscow)

  • preserveInstants: 시간을 보존하기 위해 Connector/J와 Spring(JVM)의 사이에서 값을 변환하는 속성

    • TRUE(default): Connctor/J와 Spring의 타임존이 다르다면 값을 변환한다.

    • FALSE: Connctor/J와 Spring의 타임존이 다르더라도 값을 변환하지 않는다.

    • 만약 connectionTimeZone=LOCAL 옵션을 사용하고 있다면 이 옵션이 적용되지 않는다.

  • forceConnectionTimeZoneToSession: Session Timezone을 Connection Timezone으로 변환하는 옵션

    • FALSE(default): Session 타임존을 DB 타임존으로 설정한다.

    • TRUE: Session 타임존을 Connection 타임존으로 설정한다.

    • 만약 connectionTimeZone=SERVER 옵션을 사용하고 있다면 이 옵션이 적용되지 않는다.

    • 이 속성을 TRUE로 변경한다면, CURRENT_TIMESTAMP 같은 DB의 함수가 영향을 받을 수 있으니 주의해야 한다.

이 정보와 DB의 특성을 바탕으로 생각했을 때, 값을 변환할 수 있는 부분은 두 군데라고 생각했다.

  • Spring과 Connector/J 사이: preserveInstants 과 connectionTimeZone 속성을 설정해 변환할 수 있다.

  • DB Session과 Table 사이: DB session의 타임존이 DB 타임존과 다르면 DB에서 값을 변환하기에 이를 이용하면 된다.

안타깝게도 내가 찾아봤을 때 Connector/J에서 Session으로의 변환방법은 없었다.

해결해보자(2)

아래 방법들에서 사용한 테스트 코드는 아래와 같다.(전체 코드는 여기서 볼 수 있다. 링크)

@Test
    void postNGet() {
        // given
        RestAssured.post("/date-time"); // 미리 만들어 놓은 함수로 Spring의 timestamp를 찍는다.

        // when & then
        RestAssured.get("/date-time") // 찍힌 모든 Timestamp를 가져온다.
                .then().log().all();
    }

1. Spring과 Connector/J 사이에서 변환

  • application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/laboratory?preserveInstants=TRUE&connectionTimeZone=SERVER // preserveInstants 속성은 default가 TRUE이기에 넣어주지 않아도 된다.

  • 흐름도

  • DB 쿼리

UTC로 변환된 값이 들어갔다.

  • 테스트 결과

위의 창은 DB의 table을 조회한 것이고, 아래 json은 테스트 결과이다.

1번 레코드는 data.sql에서 CURRENT_TIMESTAMP로 찍은 값이고 2번 레코드가 Spring에서 생성해서 보낸 값이다.

시간이 잘 변환되고 잘 저장된다.

2. DB Session과 Table 사이에서 변환

  • application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/laboratory?preserveInstants=FALSE&forceConnectionTimeZoneToSession=TRUE // connection time zone이 LOCAL이기에 preserveInstants 속성은 안 넣어도 된다.

  • 흐름도

  • DB 쿼리

Session을 Asia/Seoul로 설정했고, INSERT 쿼리도 KST시간으로 들어갔다.

  • 테스트 결과

위의 창은 DB의 table을 조회한 것이고, 아래 json은 테스트 결과이다.

1번 레코드는 data.sql에서 CURRENT_TIMESTAMP로 찍은 값이고 2번 레코드가 Spring에서 생성해서 보낸 값이다.

시간이 잘 변환되고 잘 저장된다.

3.Spring과 Connector/J 사이에서 변환 + DB Session과 Table 사이에서 변환 (변환이 두개!)

Connection 타임존으로는 모스크바 타임존(Europe/Moscow)를 골랐다.

  • application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/laboratory?preserveInstants=TRUE&connectionTimeZone=Europe/Moscow&forceConnectionTimeZoneToSession=TRUE // preserveInstants 속성은 생략해도 된다.

  • 흐름도

그림과 같이 Spring(KST)에서 Connector/J(MSK)로, DB session(MSK)에서 DB table(UTC)로 변환이 2번 일어난다.

  • DB 쿼리

Session 타임존이 Europe/Moscow로 설정되었고, DB에 모스크바 타임존이 적용된 값이 잘 전달됐다.

  • 테스트 결과

잘 들어갔다.

결론

  1. Connector/J, DB Session, DB의 타임존을 일치시키면 특정 시점을 보존할 수 있다.(DB, Spring 등 어떤 시간대가 변동되어도 시점은 영향 받지 않는다)

  2. Connector/J와 DB Session 시간을 Spring(JVM)과 일치시키면 특정 시점을 보존할 수 있다.

  3. 변태같긴 하지만 3번 해결책과 같이 두 번 변환해서 시점을 보존할 수도 있다.

  4. 생각해보니까 Spring, DB의 타임존을 UTC말고 다른 타임존으로 설정하는 것 자체가 이상하다. (굳이...?)

  5. 아 참, 그리고 Instant외에도 OffsetDateTime, ZonedDateTime 같이 자바에서 타임존 정보가 있는 시간 클래스는 위의 해결책들을 사용하여 시점을 보존할 수 있다.

  6. 하루만에 글을 번역하고 실험하고 글을 쓰는 짓은 하지 말자.

++조금만 더...

이전에 찾았던 3가지 해결방법을 생각해보면 이런 결론이 나온다.

1. spring.datasource.url에 serverTimezone=UTC을 추가하여 서버 타임존 설정하기

  • Mysql 8.0.23 버전 이상에서 serverTimezone과 connectionTimeZone 속성은 같은 속성이다.(링크: )

  • 그렇지만 connectionTimeZone을 UTC로 하드코딩한다면 DB의 시간대가 UTC에서 다른 타임존으로 바뀌었을 때 시점을 보존할 수 없다.

  • 만약 이걸 쓴다면 connectionTimeZone=SERVER로 변경해서 쓰겠다.

2. spring.jpa.hibernate.jdbc.time_zone=UTC을 추가하여 Hibernate의 JDBC의 타임존을 UTC로 설정하기

  • 1번과 마찬가지의 이유로 UTC로 하드코딩은 위험할 수 있다.

  • 하드코딩 외에 hibernate의 JDBC 타임존을 DB 타임존으로 설정하는 기능은 없기에 차라리 1번 옵션을 쓰겠다.

3. Java 어플리케이션을 실행할 때 -Duser.timezone=UTC로 어플리케이션의 타임존 자체를 변경하기

  • EC2의 타임존이 UTC가 아니라면 괜찮을 것 같은데, EC2가 UTC말고 다른 타임존을 쓸 이유가 있을까...?

Reference

- https://dev.mysql.com/doc/connector-j/en/connector-j-time-instants.html(Preserving Time Inststans)

- https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-datetime-types-processing.html#cj-conn-prop_connectionTimeZone (JDBC URL의 시간 관련 속성)

- https://dev.mysql.com/doc/refman/8.4/en/time-zone-support.html#time-zone-upgrades (Mysql에서 어떻게 Timezone 속성을 관리하는가)

- https://chung-develop.tistory.com/143 (해결 방법 소개글)

3
0
나그

나그

[TroubleShooting] Spring Boot 2에서 Instant 사용하기 1탄

들어가며

부크부크에서는 LocalDateTime 클래스로 생성 시간, 수정 시간을 관리하고 있었다.

여기서 LocalDateTime은 서버 시간(Spring 타임존)의 영향을 받기에 유저, DB 등 특정 시간대에 영향을 받지 않는 시간을 저장해 타임존 의존성을 제거하고 싶었다.

타임존 의존성을 제거하여 유저가 영국에 있던 한국에 있던 상관 없이 유저의 위치에 알맞은 시간을 표시하고 싶었다. (다만 돌이켜보면 타임존이 꼭 고쳐야하는 문제는 아닌 것 같다. 현재 상황으로는 굳이 고치지 않아도 될 부분을 고친 것 같다.)

그래서 Instant라는, 특정 시점을 UTC 기준으로 저장하는 Java의 시간 클래스로 LocalDateTime을 대체했다.

이 과정에서 정말 많은 삽질이 있었는데, 이 과정에서 배운 걸 정리해보려고 한다.

Spring Boot 2의 JPA에서는 기본적으로 Instant를 지원하지 않는다.

미안하다... 왠지 모르겠는데 없다...

깃허브의 jarkartaee의 persistence 레포지토리에는 Instant와 관련하여 아래와 같은 이슈가 있다.

(이슈 링크: https://github.com/jakartaee/persistence/issues/163)

글을 쓰신 분은 JPA의 contributor이다.

여기서 중요한 부분을 번역해보면,

이슈 #63에서 java.time.Instant에 대한 지원에 관해 언급되어 있는데 최종 2.2 버전에는 없다. 나도 이게 왜 빠졌는지 모르겠다.
다만 Hibernate에서는 Instant를 이미 지원한다. 그리고 hibernate.jdbc.time_zone을 변경하여 데이터베이스의 타임존을 변경할 수 있다.

만약 JPA 2.2에서 특정 시점을 다루고 싶다면 다음과 같은 옵션이 있다.
1. java.util.Date 클래스 사용하기
2.OffsetDateTime 클래스 사용하기 - 그런데 이 방법은 offset을 골라야하는데, ZoneOffset.UTC를 offset으로 하면 offsetDateTime을 Instant처럼 사용하는 것이다. 다만 이 경우 DB에서 timestamp 칼럼에 offset을 저장하는 걸 지원하지 않을 수 있는데, 이럴 때는 @javax.persistence.TemporalTimeZoneColumn을 사용하여 칼럼을 2개로 쪼개 저장해라.
3. Instant를 사용하기 - JPA 2.2 버전 이전부터 사용하고 있겠지만, AttributeContributer와 함께 사용해라.


그래도 각각 옵션보다 Instant를 지원하는게 가장 좋은거 같다.

그래서 어떤 JPA 버전부터 Instant를 지원해 주는데...?

예... 23년 11월 8일 출시된 3.2.0 버전부터 지원한다.

한마디로 Spring Boot 2에서는 못쓴다.

이제 사용해 보자.

만약 이렇게 코드를 짜고 (소스코드는 여기에 다 여기에 있습니다, 커밋 별로 기능이 바뀝니다.)

  • DateTimeController.java

package nag.laboratory.datetime;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/date-time")
@RequiredArgsConstructor
public class DateTImeController {

    private final DateTimeService dateTimeService;

    @PostMapping
    public ResponseEntity<DateTimeResponse> createDateTime() {
        DateTimeResponse response = dateTimeService.createDateTime();

        return ResponseEntity.ok()
                .body(response);
    }

}
  • DateTimeService.java

package nag.laboratory.datetime;

import java.time.Instant;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class DateTimeService {

    private final DateTimeRepository dateTimeRepository;

    @Transactional
    public DateTimeResponse createDateTime() {
        DateTime dateTime = dateTimeRepository.save(new DateTime(Instant.now()));

        return new DateTimeResponse(dateTime.getCreatedTime());
    }

}
  • DateTimeRepository.java

package nag.laboratory.datetime;

import org.springframework.data.jpa.repository.JpaRepository;

public interface DateTimeRepository extends JpaRepository<DateTime, Long> {

}
  • DateTime.java

package nag.laboratory.datetime;

import java.time.Instant;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class DateTime {

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

    @Column(columnDefinition = "TIMESTAMP")
    private Instant createdTime;

}
  • DateTimeResponse.java

package nag.laboratory.datetime;

import java.time.Instant;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public class DateTimeResponse {

    private final Instant createdTime;

}
  • DateTimeTest.java

package nag.laboratory;

import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;

import io.restassured.RestAssured;
import io.restassured.response.Response;
import nag.laboratory.datetime.DateTime;
import nag.laboratory.datetime.DateTimeResponse;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // RestAssured를 사용하기 위해 스프링을 띄웁니다.
public class DateTimeTest {

    @LocalServerPort
    private int port;

    @BeforeEach 
    public void setUp() {
    	// RestAssured에 스프링 포트를 설정합니다.
        RestAssured.port = port;
    }

    @Test
    void post() {
        // when
        Response response = RestAssured
                .given().log().all()
                .when()
                .post("/date-time")
                .then().log().all()
                .extract().response();

        // then
        assertThat(response.jsonPath().getString("createdTime")).isNotNull();
    }

}

위의 테스트 코드를 실행하면... 테스트가 통과한다.

???????????? 뭐죠? 왜죠? 왜 통과하죠?

Hibernate에서는 Instant를 지원한다.

여러분들은 테스트가 통과하지 않을 거라 예상하셨겠지만, 실제로는 통과한다.

이 테스트가 성공하는 이유는 Hibernate에서 Instant를 지원하기 때문이다.

위의 표는 Hibernate 5와 JPA 2.2의 시간 객체 지원 여부를 보여주는 표다.

Hibernate와 JPA는 공통으로 LocalDate, LocalTime, LocalDateTime, OffSetTime(UTC 시간에 시간 오프셋이 적용된 시간 ex: 16:10:14.368917+09:00), 'OffsetDateTime'(ex: 2024-05-20T16:11:31.266075+09:00)을 지원한다.

HIbernate에서만 Duration(시간의 크기), ZonedDateTime(`OffsetDateTIme`에 타임존 위치 정보가 추가된 클래스), Instant를 지원한다.

Hibernate가 JPA의 구현체이기 때문에, JPA가 지원하지 않는대도 Hibernate에서 지원하면 쓸 수 있다.

그리고 내가 쓰고 있는 Spring Boot 2.7.18 에서 Hibernate 5.6.15 Final 버전을 지원하니까 Hibernate를 통하여 Instant를 사용할 수 있다.

생성시간을 자동으로 관리해보자.

이제 Instant를 자동으로 관리하도록 코드를 짜보자.

1. JPA Auditing을 사용하기

JPA Auditing은 Spring Data JPA에서 제공해주는 누가, 언제 엔티티를 생성, 변경했는지 자동으로 추적해주는 기능이다.

이 기능을 사용하면 상속을 통하여 내가 원하는 엔티티에 자동으로 생성 시간을 넣어 줄 수 있다.

코드는 위의 코드에서 일부분만 변경하면 된다.

아래는 변경된 부분 중 핵심 부분이다.

  • BaseTimeEntity.java

package nag.laboratory.datetime;

import java.time.Instant;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import lombok.Getter;

@MappedSuperclass // 엔티티 간의 공통 정보를 정의하기 위해 쓰인다. BaseTimeEntity를 상속 받으면 createdTime이 자동으로 엔티티에 추가된다.
@EntityListeners(AuditingEntityListener.class) // 이벤트의 종류에 따라 자동으로 이벤트를 처리한다.
@Getter
public abstract class BaseTimeEntity {

    @CreatedDate // 생성 시간을 넣어준다.
    private Instant createdTime;

}
  • LaboratoryApplication.java

package nag.laboratory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing // 어노테이션을 추가해야 JPA Auditing을 사용할 수 있다.
public class LaboratoryApplication {

    public static void main(String[] args) {
        SpringApplication.run(LaboratoryApplication.class, args);
    }

}

2. Hibernate의 @CreationTimestamp 사용하기

Hibernate에서 제공 해주는 @CreationTimestamp를 사용해도 JPA Auditing과 똑같이 생성시간을 자동화해서 넣어줄 수 있다.

JPA Auditing과 비교하면 어노테이션 하나만 추가하면 되기에 간단하다.

BaseTimeEntity라는 클래스를 생성 하지않고 어노테이션 하나만 추가하면 된다.

  • DateTime.java

package nag.laboratory.datetime;

import java.time.Instant;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import org.hibernate.annotations.CreationTimestamp;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class DateTime {

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

    // 어노테이션 하나면 된다.
    @CreationTimestamp
    private Instant createdTime;

}

마무리하며

글을 요약해보면 3줄이 나온다.

  1. Spring Boot 2에서 Instant를 사용할 수 있는 건 Hibernate 덕분이다.

  2. JPA Auditing과 @CreationTimestamp를 통해 생성 시간을 자동으로 넣어줄 수 있다.

  3. JPA Auditing은 상속을 통하여 필드를 입력하고, @CreationTimestamp는 필드에 어노테이션을 붙여 자동으로 입력한다.

그리고 무엇보다 아직 끝난 건 아니다.

저렇게 JPA Auditing, @CreationTimstamp를 활용해도 DB에 UTC 적용 시간이 저장되지 않는 이슈가 있다.

저... 선생님...? 왜 그러시죠? 불만이 있으시면 말로하세요...

그리고 이 이슈에 관해서는 다음 글에서 다루겠다. (솔직히 저 이슈를 파다보니까 이 글을 작성하게 되었다. 이럴 줄 알았으면  LocalDateTime쓸 걸...)

Reference

- https://stackoverflow.com/questions/49309076/why-jpa-does-not-support-java-time-instant (JPA에서 왜 Instant를 지원해주지 않는가?)

- https://github.com/jakartaee/persistence/issues/163 (JPA에서 Instant를 지원해주지 않는 것에 대한 이슈)

- https://docs.oracle.com/en/java/javase/11/docs/api/index.html (오라클 Javadoc, Instant, OffsetTime, ZonedDateTime 등 시간 클래스 참고)

- https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/ (위의 JPA, HIbernate 관계 그림 출처)

- https://thorben-janssen.com/hibernate-jpa-date-and-time/ (Hibernate와 JPA의 시간 객체 지원 차이)

2
0

포스트

아직 포스트가 없습니다.