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 이상부터 사용가능한 방법으로, 아마 가장 간단한 방법일 것 같다.
아래와 같이 @EntityScan에 Jsr310JpaConverters.class를 지정해주기만 하면 된다. 다만, Jsr310JpaConverters.class를 사용하지 않았다면 굳이 지정해 주지 않아도 자동 설정으로 처리될 basePackages도 명시적으로 지정해줘야만 엔티티를 로딩할 수 있다는 단점이 있다.
@EnableJpaAuditing @EntityScan( basePackageClasses = {Jsr310JpaConverters.class}, // basePackageClasses에 지정 basePackages = {"homo.efficio.toy.member.domain"}) // basePackages도 추가로 반드시 지정해줘야 한다 @SpringBootApplication publicclassMemberApplication{
엔티티에도 날짜/시간형 필드에 @Temporal(TemporalType.TIMESTAMP)를 붙여주지 않아도 된다. 사실은 붙이고 싶어도 붙일 수가 없다. Java EE API 문서에 보면 @Temporal은 java.util.Date이나 java.util.Calendar에만 붙일 수 있게 되어 있다.
Jsr310JpaConverters 클래스는 사실 다음에 설명할 Attribute Converter를 활용하는 방법을 Spring에서 구현해서 쓰기 편하게 Wrapping 해준 Jsr310Converters 클래스를 JPA에서 사용할 수 있게 해주는 클래스다.
Attribute Converter를 활용하는 방법
JPA 2.1 부터 Attribute Converter라는 기능이 도입되었다. AttributeConverter 클래스를 상속받는 자체 Converter를 만들면, Java8의 날짜/시간 데이터 타입을 JPA에서 인식할 수 있는 타입으로 자동으로 변환되게 할 수 있다. 아래는 java.time.LocalDateTime에 대한 Converter다.
@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 애노테이션을 지정해준다.
@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을 사용하지 않는다면 이 방법으로 해결하면 된다. 다만, 타이핑 양은 좀 되지만 복붙신공이면 될 일이고.. ㅋㅋ