Java 8 Date(Time) 와 JPA 그리고 스프링 부트
악평(?)이 자자하던 Java 날짜와 시간Date and Time 라이브러리[1]는 Java 8 버전부터 완전히 새로워졌다. 이 글은 새로워진 Java 날짜와 시간(이하 Java8 날짜와 시간)을 스프링 부트Spring Boot+ JPAJava Persistence API(Hibernate) 환경에서 사용하는 방법을 다룬다.
기대와 다른 결과
예시 코드는 스프링 부트 1.5 으로 작성하였고 JPA (spring-boot-starter-data-jpa) 의존성을 추가하였다.[2]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> //... <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.14.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> //... </dependencies> </project>
데이터베이스에 저장될 Member 엔티티Entitiy는 아래와 같다. createdTimeAt, createdDateAt에 Java8 날짜와 시간 객체인 LocalDate, LocalDateTime을 사용하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
//... import java.time.LocalDate; import java.time.LocalDateTime; @Entity public class Member { @Id @GeneratedValue private Long id; private LocalDateTime createdTimeAt; private LocalDate createdDateAt; public Member() { createdDateAt = LocalDate.now(); createdTimeAt = LocalDateTime.now(); } // ... }
JPA로 Member 엔티티 데이터베이스에 저장하면 Java8 LocalDate와 LocalDateTime는 JDBC Type인 Date와 Timestamp로 변환 되어 저장될까?
코드를 실행하면 실제 데이터베이스에는 기대했던 Date와 Timestamp가 아닌 Binary로 저장된다.
하이버네이트에서 자동 생성한 DDLData Definition Language을 확인해 보면[3] 데이터 타입을 Binary로 생성하고 있다.
JPA 2.2 명세Specification
JPA는 명세일 뿐이고 실제로 명세를 구현하는 구현체는 따로 있다. 스프링 부트는 기본적으로 JPA 구현체를 하이버네이트를 사용한다. Java 8 날짜와 시간은 JPA 2.2 명세[3]에 추가되었으며 하이버네이트는 5.3부터 JPA 2.2를 지원한다.
현재 스프링 부트 최근 릴리즈(2.0.2)는 하이버네이트 5.2(JPA 2.1)를 그리고 이전 릴리즈인 1.5는 하이버네이트 5.0(JPA 2.1)을 사용한다.
JPA |
Hibernate |
Spring Boot |
---|---|---|
2.2 | 5.3 | - |
2.1 | 5.2 | 2.0 RELEASE |
2.1 | 5.0 | 1.5 RELEASE |
표. JPA/Hibernate/Spring Boot 버전 비교
스프링 부트 + JPA 환경에서 Java 8 날짜와 시간을 사용하려면 스프링 부트가 하이버네이트 5.3(JPA 2.2)을 지원하기를 기다려야 하는 걸까?
결론부터 말하자면 JPA 2.2를 지원하지 않더라도 사용할 수 있다.
명세는 명세일 뿐
관점을 좁혀서 하이버네이트만 살펴보자. 하이버네이트는 5.0부터 JPA 명세와 상관없이 Java 8 날짜와 시간을 지원한다. 다만 버전에 따라 사용법이 조금 다르다.
하이버네이트 5.0, 5.1의 경우 Java 8 날짜와 시간을 사용하기 위해서는 classpath에 hibernate-java8 라이브러리를 추가해 주어야 한다.
아래는 스프링 부트가 아닌 하이버네이트만 사용하는 프로젝트에 hibernate-java8 라이브러리 의존성을 추가한 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> //... <properties> <hibernate.version>5.0.12.Final</hibernate.version> </properties> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-java8</artifactId> <version>${hibernate.version}</version> </dependency> //... </dependencies> </project>
이렇게 hibernate-java8 라이브러리 의존성을 추가 후 위의 Member 엔티티를 JPA로 저장하면 아래와 같이 Date와 Timestamp로 저장되는 것을 확인할 수 있다.
하이버네이트 5.2부터 hibernate-java8 의존성 추가 필요 없이 Java 8 날짜와 시간을 사용할 수 있다.
코드는 아래와 같다.[5]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <properties> <hibernate.version>5.2.8.Final</hibernate.version> </properties> <dependencies> <!-- JPA --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> //... </dependencies> //... </project>
왜 하이버네이트 5.0, 5.1는hibernate-java8 라이브러리 추가해야 하고 5.2는 필요 없는 것일까?
하이버네이트 5.0과 5.1의 최소 Java 버전은 6이고, 5.2의 최소 Java 버전은 8이다. 따라서 필자의 추측으로는 하이버네이트 5.0, 5.1는 Java 8을 사용하지 않는(6,7 버전) 프로젝트를 위해서 따로 분리한 것으로 보인다.
그렇다면 스프링 부트 + JPA 에서는?
스프링 부트 1.5
1.5는 Hibernate 5.0을 사용하고 있다. 따라서 hibernate-java8 모듈 의존성을 추가해 주면 된다. 이미 스프링 부트 부모 Maven POMProject Object Model에 버전이 선언되어 있기 때문에 버전 선언은 필요 없다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> //... <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-java8</artifactId> </dependency> //... </dependencies> </project>
스프링 부트 2.0
2.0은 Hibernate 5.2를 사용하고 있다. 따라서 추가 설정 필요 없이 JPA 엔티티에 Java8 날짜와 시간을 그냥 쓰면 된다.
1 2 3 4 5 6 7 8 9 10 11 12
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> //... <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> //... </dependencies> </project>
왜 스프링 부트에는 기본적으로 hibernate-java8 의존성이 추가되어 있지 않나?
이 역시 하이버네이트와 마찬가지로 Java 버전 때문일 것이라고 추정된다. 스프링 부트 1.5는 최소 Java 7이고 2.0은 Java 8이다.
One more thing...
엔티티를 사용하다 보면 생성일(createdTimeAt)과 수정일(updateTimeAt)을 관리해 주어야 할 때가 많다. 각 엔티티마다 객체가 생성되거나 수정될 때 현재 시간을 설정하는 코드를 작성해야 하는 것은 번거로운 일이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import java.time.LocalDate; import java.time.LocalDateTime; @Entity public class Member { @Id @GeneratedValue private Long id; private LocalDateTime createdTimeAt; private LocalDateTime updateTimeAt; public Member() { final LocalDateTime now = LocalDateTime.now(); createdTimeAt = now; updateTimeAt = now; } //... }
하이버네이트 5.2 이상을 사용하고 있다면 Java8 날짜와 시간에 CreationTimestamp과 UpdateTimestamp을 추가해 주면 시간 설정을 하이버네이트가 알아서 해준다.[6]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import java.time.LocalDate; import java.time.LocalDateTime; @Entity public class Member { @Id @GeneratedValue private Long id; @CreationTimestamp private LocalDateTime createdTimeAt; @UpdateTimestamp private LocalDateTime updateTimeAt; //... }
주석
[1] 자세한 Java의 날짜와 시간 라이브러리의 역사는 https://d2.naver.com/helloworld/645609 에서 확인할 수 있다.
[2] 스프링 부트 골격이 되는 코드는 SPRING INITIALIZR에서 생성하였다.
[3] 스프링 부트 설정 파일인 application.properties에 아래와 같이 추가하면 SQL 로그를 확인할 수 있다
1 2 3 4
spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.properties.hibernate.show_sql=true spring.jpa.properties.hibernate.use_sql_comments=true spring.jpa.properties.hibernate.format_sql=true
[4] Handy Improvements in JPA 2.2 - Java 8 date and time support
[5] 하이버네이트 5.0, 5.1과 5.2에 의존성 모듈이 다른 이유는 5.2 이전에는 JPA support 모듈이 분리되어 있었지만 5.2 부터 core 모듈로 합쳐졌기 때문이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> //... <dependencies> <!-- Hibernate 5.0, 5.1 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate.version}</version> </dependency> <!-- Hibernate 5.2 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> </dependencies> //... </project>
[6] 하이버네이트 5.0, 5.1의 경우 LocalDate와 LocalDateTime을 CreationTimestamp/UpdateTimestamp에서 지원하지 않는다.