Spring Data JPA 에서 Java8 Date-Time(JSR-310) 사용하기   

Java8에는 시간 데이터를 더 편리하게 처리할 수 있게 해주는 LocalDate, LocalDateTime 등의 클래스들이 java.time 패키지에 추가되었다. 날짜/시간 차이 계산, 비교, 년/월/일/시/분/초 단위 별 추출 등 풍부한 기능을 제공해주므로 사용성이 아주 좋다. Joda-Time을 사용하고 있었다면, Java8에서는 java.time (JSR-310)으로 마이크레이션 하는 것이 좋다.

하지만, java.time (JSR-310)를 별다른 처리 없이 JPA를 이용해서 MySQL에 저장하면 버전에 따라서 아래와 같은 에러가 날 수도 있다.

1
2
3
...
Caused by: com.mysql.jdbc.MsqlDataTruncation: Data truncation: Incorrect dateme value: '\xAC\xED\x00\x05sr\x0Djava.time.Ser\x95]\x84\xBA\x1B"H\xB2\x0C

Spring Data JPA 에서 Java8 Date-Time(JSR-310) 사용하기   

Java8에는 시간 데이터를 더 편리하게 처리할 수 있게 해주는 LocalDate, LocalDateTime 등의 클래스들이 java.time 패키지에 추가되었다. 날짜/시간 차이 계산, 비교, 년/월/일/시/분/초 단위 별 추출 등 풍부한 기능을 제공해주므로 사용성이 아주 좋다. Joda-Time을 사용하고 있었다면, Java8에서는 java.time (JSR-310)으로 마이크레이션 하는 것이 좋다.

하지만, java.time (JSR-310)를 별다른 처리 없이 JPA를 이용해서 MySQL에 저장하면 버전에 따라서 아래와 같은 에러가 날 수도 있다.

1
2
3
...
Caused by: com.mysql.jdbc.MsqlDataTruncation: Data truncation: Incorrect dateme value: '\xAC\xED\x00\x05sr\x0Djava.time.Ser\x95]\x84\xBA\x1B"H\xB2\x0C\0\x00xpw\x07\x03\x00\x00\x07\xE0\x05\x1Fx' for column 'start_date' at row 1
...

또는 날짜/시간을 나타내는 컬럼이 MySQL에서 datetime 타입이 아니라 tinyblob 타입으로 생성되어서 아래와 같이 원치 않는 형식으로 저장되기도 한다.

어느 경우든, JPA와 Java8 Date/Time은 뭔가 조치를 취해주지 않으면 원하는 대로 쓸 수 없다.

데이터의 생성/수정 시각을 기록하는 JPA Auditing을 대상으로 그 조치 방법을 알아보자. 모든 엔티티가 상속해야 하는 BaseEntity라는 추상 클래스를 만들어서 이 클래스에 JPA Auditing을 적용하는 상황이다.

Jsr310JpaConverters.class를 활용하는 방법

Spring Data JPA 1.8 이상부터 사용가능한 방법으로, 아마 가장 간단한 방법일 것 같다.

 

아래와 같이 @EntityScanJsr310JpaConverters.class를 지정해주기만 하면 된다. 다만, Jsr310JpaConverters.class를 사용하지 않았다면 굳이 지정해 주지 않아도 자동 설정으로 처리될 basePackages도 명시적으로 지정해줘야만 엔티티를 로딩할 수 있다는 단점이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@EntityScan(
        basePackageClasses = {Jsr310JpaConverters.class},  // basePackageClasses에 지정
        basePackages = {"homo.efficio.toy.member.domain"}) // basePackages도 추가로 반드시 지정해줘야 한다
@SpringBootApplication
public class MemberApplication {

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

 

엔티티에도 날짜/시간형 필드에 @Temporal(TemporalType.TIMESTAMP)를 붙여주지 않아도 된다. 사실은 붙이고 싶어도 붙일 수가 없다. Java EE API 문서에 보면 @Temporaljava.util.Date이나 java.util.Calendar에만 붙일 수 있게 되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdDateTime;

    @LastModifiedDate
    @Column(name = "last_modified_at", updatable = true)
    private LocalDateTime lastModifiedDateTime;
}

위의 코드를 보면 BaseEntity에는 따로 무슨 조치를 취한 것 없이 깔끔하다.

Jsr310JpaConverters 클래스는 사실 다음에 설명할 Attribute Converter를 활용하는 방법을 Spring에서 구현해서 쓰기 편하게 Wrapping 해준 Jsr310Converters 클래스를 JPA에서 사용할 수 있게 해주는 클래스다.

Attribute Converter를 활용하는 방법

JPA 2.1 부터 Attribute Converter라는 기능이 도입되었다. AttributeConverter 클래스를 상속받는 자체 Converter를 만들면, Java8의 날짜/시간 데이터 타입을 JPA에서 인식할 수 있는 타입으로 자동으로 변환되게 할 수 있다.
아래는 java.time.LocalDateTime에 대한 Converter다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Date;

import static java.time.Instant.ofEpochMilli;
import static java.time.LocalDateTime.ofInstant;
import static java.time.ZoneId.systemDefault;

@Converter
public class LocalDateTimePersistenceConverter implements AttributeConverter<LocalDateTime, Date> {

    @Override
    public Date convertToDatabaseColumn(LocalDateTime localDateTime) {
        return Date.from(localDateTime.atZone(systemDefault()).toInstant());
    }

    @Override
    public LocalDateTime convertToEntityAttribute(Date date) {
        return ofInstant(ofEpochMilli(date.getTime()), systemDefault());
    }
}

위의 코드는 LocalDateTime에 대한 구현체만 들어있는데, 앞에서 언급한 org.springframework.data.convert.Jsr310Converters 클래스는 LocalDate, LocalTime, LocalDateTime, Instant, ZoneId 모두에 대한 변환 기능을 구현해서 제공해주며, Spring Data JPA 1.8 이상이라면 앞에서 살펴본 것처럼 Jsr310JpaConverters를 통해 Spring Data JPA에서 사용할 수 있다.

자체 Converter를 만들었다고 끝난 것이 아니다. 어느 데이터에 이 Converter를 적용할지 지정해줘야 한다. 따라서 아래와 같이 BaseEntity에서 Converter에 의한 자동변환이 필요한 데이터에 @Convert 애노테이션을 지정해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @CreatedDate
    @Convert(converter = LocalDateTimePersistenceConverter.class)  // <- @Converter를 지정 해줘야 한다.
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdDateTime;

    @LastModifiedDate
    @Convert(converter = LocalDateTimePersistenceConverter.class)  // <- @Converter를 지정 해줘야 한다.
    @Column(name = "last_modified_at", updatable = true)
    private LocalDateTime lastModifiedDateTime;
}

getter, setter를 변형해서 활용하는 방법

이 방법은 Jsr310Converters이나 AttributeConverter 등에 포함된 변환 로직을 그냥 getter, setter에 직접 심어버리는 방법으로 가장 직관적이고 간단하며, 외부 의존성도 적다. Spring을 사용하지 않는다면 이 방법으로 해결하면 된다. 다만, 타이핑 양은 좀 되지만 복붙신공이면 될 일이고.. ㅋㅋ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;

import static java.time.Instant.ofEpochMilli;
import static java.time.LocalDateTime.ofInstant;
import static java.time.ZoneId.systemDefault;
import static java.util.Objects.isNull;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @CreatedDate
    @Temporal(TemporalType.TIMESTAMP)  // java.util.Date이므로 @Temporal을 붙여준다.
    @Column(name = "created_at", updatable = false)
    private Date createdDateTime;

    @LastModifiedDate
    @Temporal(TemporalType.TIMESTAMP)  // java.util.Date이므로 @Temporal을 붙여준다.
    @Column(name = "last_modified_at", updatable = true)
    private Date lastModifiedDateTime;



    // 변환 로직을 직접 구현

    public LocalDateTime getCreatedDateTime() {
        return getLocalDateTimeFrom(createdDateTime);
    }

    public void setCreatedDateTime(final LocalDateTime createdDateTime) {
        this.createdDateTime = getDateFrom(createdDateTime);
    }

    public LocalDateTime getLastModifiedDateTime() {
        return getLocalDateTimeFrom(lastModifiedDateTime);
    }

    public void setLastModifiedDateTime(final LocalDateTime lastModifiedDateTime) {
        this.lastModifiedDateTime = getDateFrom(lastModifiedDateTime);
    }

    private LocalDateTime getLocalDateTimeFrom(Date date) {
        return isNull(date) ? null : ofInstant(ofEpochMilli(date.getTime()), systemDefault());
    }

    private Date getDateFrom(LocalDateTime localDateTime) {
        return isNull(localDateTime) ? null : Date.from(localDateTime.atZone(systemDefault()).toInstant());
    }
}

정리

Java8의 java.time (JSR-310) 데이터를 DB에 저장해야 한다면,

  • Spring Data JPA 1.8 이상이라면 Jsr310JpaConverters 클래스를 사용하자.

  • Spring Data JPA 1.8 미만이지만, JPA 2.1 이상이라면 Jsr310Converters 클래스를 사용해서 AttributeConverter를 상속하고 변환 로직을 구현해서 사용하자.

  • 이도 저도 아니라면 java.time (JSR-310)java.util.Date를 상호 변환할 수 있는 로직을 getter/setter 안에 직접 구현해서 사용하자.

더 읽을 거리

[출처] http://blog.eomdev.com/java/2016/01/11/jpa_04_entityMapping.html

 

\x00xpw\x07\x03\x00\x00\x07\xE0\x05\x1Fx'
for column 'start_date' at row 1
...

또는 날짜/시간을 나타내는 컬럼이 MySQL에서 datetime 타입이 아니라 tinyblob 타입으로 생성되어서 아래와 같이 원치 않는 형식으로 저장되기도 한다.

어느 경우든, JPA와 Java8 Date/Time은 뭔가 조치를 취해주지 않으면 원하는 대로 쓸 수 없다.

데이터의 생성/수정 시각을 기록하는 JPA Auditing을 대상으로 그 조치 방법을 알아보자. 모든 엔티티가 상속해야 하는 BaseEntity라는 추상 클래스를 만들어서 이 클래스에 JPA Auditing을 적용하는 상황이다.

Jsr310JpaConverters.class를 활용하는 방법

Spring Data JPA 1.8 이상부터 사용가능한 방법으로, 아마 가장 간단한 방법일 것 같다.

 

아래와 같이 @EntityScanJsr310JpaConverters.class를 지정해주기만 하면 된다. 다만, Jsr310JpaConverters.class를 사용하지 않았다면 굳이 지정해 주지 않아도 자동 설정으로 처리될 basePackages도 명시적으로 지정해줘야만 엔티티를 로딩할 수 있다는 단점이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@EntityScan(
        basePackageClasses = {Jsr310JpaConverters.class},  // basePackageClasses에 지정
        basePackages = {"homo.efficio.toy.member.domain"}) // basePackages도 추가로 반드시 지정해줘야 한다
@SpringBootApplication
public class MemberApplication {

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

 

엔티티에도 날짜/시간형 필드에 @Temporal(TemporalType.TIMESTAMP)를 붙여주지 않아도 된다. 사실은 붙이고 싶어도 붙일 수가 없다. Java EE API 문서에 보면 @Temporaljava.util.Date이나 java.util.Calendar에만 붙일 수 있게 되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdDateTime;

    @LastModifiedDate
    @Column(name = "last_modified_at", updatable = true)
    private LocalDateTime lastModifiedDateTime;
}

위의 코드를 보면 BaseEntity에는 따로 무슨 조치를 취한 것 없이 깔끔하다.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

Jsr310JpaConverters 클래스는 사실 다음에 설명할 Attribute Converter를 활용하는 방법을 Spring에서 구현해서 쓰기 편하게 Wrapping 해준 Jsr310Converters 클래스를 JPA에서 사용할 수 있게 해주는 클래스다.

Attribute Converter를 활용하는 방법

JPA 2.1 부터 Attribute Converter라는 기능이 도입되었다. AttributeConverter 클래스를 상속받는 자체 Converter를 만들면, Java8의 날짜/시간 데이터 타입을 JPA에서 인식할 수 있는 타입으로 자동으로 변환되게 할 수 있다.
아래는 java.time.LocalDateTime에 대한 Converter다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Date;

import static java.time.Instant.ofEpochMilli;
import static java.time.LocalDateTime.ofInstant;
import static java.time.ZoneId.systemDefault;

@Converter
public class LocalDateTimePersistenceConverter implements AttributeConverter<LocalDateTime, Date> {

    @Override
    public Date convertToDatabaseColumn(LocalDateTime localDateTime) {
        return Date.from(localDateTime.atZone(systemDefault()).toInstant());
    }

    @Override
    public LocalDateTime convertToEntityAttribute(Date date) {
        return ofInstant(ofEpochMilli(date.getTime()), systemDefault());
    }
}

위의 코드는 LocalDateTime에 대한 구현체만 들어있는데, 앞에서 언급한 org.springframework.data.convert.Jsr310Converters 클래스는 LocalDate, LocalTime, LocalDateTime, Instant, ZoneId 모두에 대한 변환 기능을 구현해서 제공해주며, Spring Data JPA 1.8 이상이라면 앞에서 살펴본 것처럼 Jsr310JpaConverters를 통해 Spring Data JPA에서 사용할 수 있다.

자체 Converter를 만들었다고 끝난 것이 아니다. 어느 데이터에 이 Converter를 적용할지 지정해줘야 한다. 따라서 아래와 같이 BaseEntity에서 Converter에 의한 자동변환이 필요한 데이터에 @Convert 애노테이션을 지정해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @CreatedDate
    @Convert(converter = LocalDateTimePersistenceConverter.class)  // <- @Converter를 지정 해줘야 한다.
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdDateTime;

    @LastModifiedDate
    @Convert(converter = LocalDateTimePersistenceConverter.class)  // <- @Converter를 지정 해줘야 한다.
    @Column(name = "last_modified_at", updatable = true)
    private LocalDateTime lastModifiedDateTime;
}

getter, setter를 변형해서 활용하는 방법

이 방법은 Jsr310Converters이나 AttributeConverter 등에 포함된 변환 로직을 그냥 getter, setter에 직접 심어버리는 방법으로 가장 직관적이고 간단하며, 외부 의존성도 적다. Spring을 사용하지 않는다면 이 방법으로 해결하면 된다. 다만, 타이핑 양은 좀 되지만 복붙신공이면 될 일이고.. ㅋㅋ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;

import static java.time.Instant.ofEpochMilli;
import static java.time.LocalDateTime.ofInstant;
import static java.time.ZoneId.systemDefault;
import static java.util.Objects.isNull;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @CreatedDate
    @Temporal(TemporalType.TIMESTAMP)  // java.util.Date이므로 @Temporal을 붙여준다.
    @Column(name = "created_at", updatable = false)
    private Date createdDateTime;

    @LastModifiedDate
    @Temporal(TemporalType.TIMESTAMP)  // java.util.Date이므로 @Temporal을 붙여준다.
    @Column(name = "last_modified_at", updatable = true)
    private Date lastModifiedDateTime;



    // 변환 로직을 직접 구현

    public LocalDateTime getCreatedDateTime() {
        return getLocalDateTimeFrom(createdDateTime);
    }

    public void setCreatedDateTime(final LocalDateTime createdDateTime) {
        this.createdDateTime = getDateFrom(createdDateTime);
    }

    public LocalDateTime getLastModifiedDateTime() {
        return getLocalDateTimeFrom(lastModifiedDateTime);
    }

    public void setLastModifiedDateTime(final LocalDateTime lastModifiedDateTime) {
        this.lastModifiedDateTime = getDateFrom(lastModifiedDateTime);
    }

    private LocalDateTime getLocalDateTimeFrom(Date date) {
        return isNull(date) ? null : ofInstant(ofEpochMilli(date.getTime()), systemDefault());
    }

    private Date getDateFrom(LocalDateTime localDateTime) {
        return isNull(localDateTime) ? null : Date.from(localDateTime.atZone(systemDefault()).toInstant());
    }
}

정리

Java8의 java.time (JSR-310) 데이터를 DB에 저장해야 한다면,

  • Spring Data JPA 1.8 이상이라면 Jsr310JpaConverters 클래스를 사용하자.

  • Spring Data JPA 1.8 미만이지만, JPA 2.1 이상이라면 Jsr310Converters 클래스를 사용해서 AttributeConverter를 상속하고 변환 로직을 구현해서 사용하자.

  • 이도 저도 아니라면 java.time (JSR-310)java.util.Date를 상호 변환할 수 있는 로직을 getter/setter 안에 직접 구현해서 사용하자.

더 읽을 거리

[출처] http://blog.eomdev.com/java/2016/01/11/jpa_04_entityMapping.html

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
38 Spring Batch 간단 정리 Sprong Batch 기초를 알아보자 file 졸리운_곰 2020.10.13 198
37 Spring Batch - ItemReader, ItemWriter, ItemProcessor 졸리운_곰 2020.10.13 370
36 spring batch» Spring Batch Tasklet Example file 졸리운_곰 2020.10.12 198
35 Spring Batch Example 2 - 간단한 Job만들기 file 졸리운_곰 2020.10.12 151
34 Spring batch Job 설정과 실행하기 file 졸리운_곰 2020.10.12 206
33 [Spring Batch] Configuring and Running a Job file 졸리운_곰 2020.10.12 121
32 [스프링] 스프링 배치 기본 개념 file 졸리운_곰 2019.12.24 244
31 Spring Batch 개념 정리 file 졸리운_곰 2019.12.24 257
30 스프링 배치 개발가이드 졸리운_곰 2019.12.24 321
29 Quartz + Spring Batch 조합! Quartz Scheduler 와 Spring Batch 의 궁합과 조합을 정리. file 졸리운_곰 2019.12.24 882
28 Spring Batch 간단 정리 file 졸리운_곰 2019.12.24 421
27 spring batch 소개 file 졸리운_곰 2019.12.24 208
26 Spring Batch - 소개 file 졸리운_곰 2019.12.24 207
25 Spring Batch Multithreading Example file 졸리운_곰 2019.01.31 285
24 Spring batch를 Parallel로 돌려보자 졸리운_곰 2019.01.31 399
23 Spring MVC 주요 애노테이션(Annotation)정리 file 졸리운_곰 2018.12.29 268
22 스프링/스프링부트 애노테이션(Annotation) 정리 졸리운_곰 2018.12.29 385
21 Spring Framework - annotation 정리 #1 졸리운_곰 2018.12.29 321
20 [Spring] Annotation 정리 졸리운_곰 2018.12.29 272
19 Maven 기초 사용법 졸리운_곰 2018.04.15 322
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED