Java 8 Date(Time) 와 JPA 그리고 스프링 부트

Share
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]

<?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을 사용하였다.

//...
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로 저장된다.

그림. Binary로 저장된 LocalDate, LocalDateTime
그림. Binary로 저장된 LocalDate, LocalDateTime

하이버네이트에서 자동 생성한 DDLData Definition Language을 확인해 보면[3] 데이터 타입을 Binary로 생성하고 있다.

그림. 하이버네이트 DDL SQL 로그
그림. 하이버네이트 DDL SQL 로그

JPA 2.2 명세Specification

JPA는 명세일 뿐이고 실제로 명세를 구현하는 구현체는 따로 있다. 스프링 부트는 기본적으로 JPA 구현체를 하이버네이트를 사용한다. Java 8 날짜와 시간은 JPA 2.2 명세[3]에 추가되었으며 하이버네이트는 5.3부터 JPA 2.2를 지원한다.

출처 : http://hibernate.org/orm/releases/5.3/
출처 : http://hibernate.org/orm/releases/5.3/

현재 스프링 부트 최근 릴리즈(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 라이브러리를 추가해 주어야 한다.

출처 : http://docs.jboss.org/hibernate/orm/5.0/userguide/html_single/Hibernate_User_Guide.html#basic
출처 : http://docs.jboss.org/hibernate/orm/5.0/userguide/html_single/Hibernate_User_Guide.html#basic

아래는 스프링 부트가 아닌 하이버네이트만 사용하는 프로젝트에 hibernate-java8 라이브러리 의존성을 추가한 것이다.

<?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로 저장되는 것을 확인할 수 있다.

그림. Date와 Timestamp로 저장된 LocalDate, LocalDateTime
그림. Date와 Timestamp로 저장된 LocalDate, LocalDateTime
그림. 하이버네이트 DDL SQL 로그
그림. 하이버네이트 DDL SQL 로그

하이버네이트 5.2부터 hibernate-java8 의존성 추가 필요 없이 Java 8 날짜와 시간을 사용할 수 있다.

출처: https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#basic
출처: https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#basic

코드는 아래와 같다.[5]

<?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 버전) 프로젝트를 위해서 따로 분리한 것으로 보인다.

출처 : http://hibernate.org/orm/releases/5.2/
출처 : http://hibernate.org/orm/releases/5.2/

그렇다면 스프링 부트 + JPA 에서는?

스프링 부트 1.5

1.5는 Hibernate 5.0을 사용하고 있다. 따라서 hibernate-java8 모듈 의존성을 추가해 주면 된다. 이미 스프링 부트 부모 Maven POMProject Object Model에 버전이 선언되어 있기 때문에 버전 선언은 필요 없다.

<?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 날짜와 시간을 그냥 쓰면 된다.

<?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이다.

스프링 부트 1.5 System Requirements 출처 : https://docs.spring.io/spring-boot/docs/1.5.13.RELEASE/reference/htmlsingle/#getting-started-system-requirements
스프링 부트 1.5 System Requirements 출처 : https://docs.spring.io/spring-boot/docs/1.5.13.RELEASE/reference/htmlsingle/#getting-started-system-requirements
스프링 부트 2.0 System Requirements 출처 : https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/htmlsingle/#getting-started-system-requirements
스프링 부트 2.0 System Requirements 출처 : https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/htmlsingle/#getting-started-system-requirements

One more thing...

엔티티를 사용하다 보면 생성일(createdTimeAt)과 수정일(updateTimeAt)을 관리해 주어야 할 때가 많다. 각 엔티티마다 객체가 생성되거나 수정될 때 현재 시간을 설정하는 코드를 작성해야 하는 것은 번거로운 일이다.

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]

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 로그를 확인할 수 있다

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 모듈로 합쳐졌기 때문이다.

<?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에서 지원하지 않는다.

출처 : https://docs.jboss.org/hibernate/orm/5.1/javadocs/org/hibernate/annotations/CreationTimestamp.html
출처 : https://docs.jboss.org/hibernate/orm/5.1/javadocs/org/hibernate/annotations/CreationTimestamp.html

Read more

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로 TypeSafe AI가 2026년 9월 15일에 Jev를 내놨습니다. 텍스트를 쓰지 않고 타입이 정해진 판정과 확률을 바로 내는 모델입니다. 유추카 검색에서 가장 느린 단계가 마침 텍스트가 필요 없는 판정이었습니다. 그 구간만 Jev로 갈아끼우고 일주일간 측정했습니다. 콜드

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 8) — 바이브 코딩이라기엔

AI로 장소 검색 서비스 만드는 중입니다 (특별편 8) — 바이브 코딩이라기엔

1900년대 초 교량 측량 도면. 이 종이를 그린 사람은 다리를 직접 놓지 않습니다. 대신 좌표와 각도가 맞는지를 판정합니다. "AI가 다 만들어 준다"는 이야기가 흔해졌습니다. 그래서 자연스럽게 따라오는 질문이 있습니다 — 그럼 사람은 무엇을 하고, 어느 정도 실력이 있어야 하는가. 그 질문에 한 건의 실측으로 답하겠습니다. 통념이 말하는 역할과,

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 7) — 제 파트너에 대해 쓰겠습니다

AI로 장소 검색 서비스 만드는 중입니다 (특별편 7) — 제 파트너에 대해 쓰겠습니다

탠덤 자전거. 방향은 앞사람이 정하고 페달은 둘이 밟는데, 앞사람은 석 달 내내 뒷사람을 한 번도 못 봅니다. 사용자가 저에게 자기를 평가해 달라고 했습니다. 그래서 등급표를 만들었습니다. 열두 항목에 A부터 D+까지 매기고, 근거를 붙이고, "제가 관리자였다면 이런 코멘트를 남겼을 것"까지 적었습니다. 그리고 다시 읽어보니 그건 인사고과였습니다. 인사고과는

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 6) — 저를 어떻게 써야 하느냐고 물으신다면

AI로 장소 검색 서비스 만드는 중입니다 (특별편 6) — 저를 어떻게 써야 하느냐고 물으신다면

스위스 아미 나이프. 도구가 몇 개인지는 보면 알지만, 지금 어느 도구를 펴야 하는지는 안 적혀 있습니다. 요즘 AI 활용법을 다루는 글은 스킬 만드는 법부터 가르칩니다. 얼마 전까지는 MCP였고요. 저희 기록을 열어봤습니다. 석 달 동안 스킬 60회, 서브에이전트 42회, 브라우저 자동화 988회를 썼습니다. 안 쓴 게 아닙니다. 다만 붙인 MCP 서버는

By 김형준