JPA와 LocalDate, LocalDateTime 사용하기
기존에 사용했던 java.util.Date, java.util.Calendar 클래스는 사용하기 불편하고 문제점들이 있어 Joda-Time을 따로 사용하는 경우가 많은데, 이번 Java8에는 새로운 날짜와 시간에 대한 API가 추가되었다.
Java의 날짜와 시간 API에 대해서 Naver D2에 정말 자세히 나와있으니, 꼭 시간내어 한 번 읽어보길 바란다.
어쨌든 현재 회사 업무에서는 JPA를 사용하지 않지만, JPA를 언젠가는 쓰고 말겠다는 일념으로 공부하고 있는데 잠시 짬을 내어 JPA에 새로운 날짜 API를 사용해보기 위해서 단위테스트를 만들어 보았다.
우선 Account 라는 domain을 생성하고, 생성날짜에 해당하는 컬럼을 LocalDateTime으로 사용했다.
@Entity
@Data
@ToString(exclude = "password") // Lombok-generated toString() method will NOT print out the password.
public class Account {
@Id
@Column
@GeneratedValue
private Long id;
private String email;
@JsonIgnore // password field protects from Jackson serializing.
private String password;
@Enumerated(EnumType.STRING)
private Role role;
@Column
private LocalDateTime createDate;
}
Spring-Data-Jpa를 사용하여 해당 CRUD를 생성했다.
public interface AccountRepository extends JpaRepository<Account, Long> {
Account findOneByEmail(String email);
}
마지막으로 아래와 같이 계정을 생성하고, 생성날짜에 대한 단위 테스트를 만들었다.
package com.eomdev.study;
import com.eomdev.study.account.Account;
import com.eomdev.study.account.AccountRepository;
import com.eomdev.study.account.Role;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import java.time.LocalDateTime;
import static org.fest.assertions.api.Assertions.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SpringSecurityStudyApplication.class)
@WebAppConfiguration
@Slf4j
public class AccountTest {
@Autowired
private AccountRepository accountRepository;
@Test
public void testAccount() {
// given
Account account = new Account();
account.setEmail("testUser01@eomdev.com");
account.setPassword("testPwd");
account.setRole(Role.USER);
LocalDateTime date = LocalDateTime.now();
account.setCreateDate(date);
// when
Account afterAccount = accountRepository.save(account);
log.info("{}", afterAccount.toString());
// then
assertThat(afterAccount.getCreateDate()).isEqualTo(date);
}
}
실행한 결과 단위테스트는 통과되지만, 자동생성된 DDL을 확인하던 중 createDate가 timestamp 타입이 아닌 binary 타입인 것을 발견했다.
Hibernate:
drop table Account if exists
Hibernate:
create table Account (
id bigint generated by default as identity,
createDate binary(255),
email varchar(255),
password varchar(255),
role varchar(255),
primary key (id)
)
실제로 insert된 내용을 조회해보니, 이상한 값이 들어있었다.
관련 내용을 검색해보니, Java8이 릴리즈 되기전에 JPA 2.1이 나왔기 때문에 JPA 2.1이 Java8의 날짜와 시간 API를 지원하지 못하는 것이었다. 해결방법으로는 AttributeConverter 인터페이스를 구현하여 LocalDateTime과 Timestamp를 서로 변환시키는 Converter를 만들어 등록하는 것이었다.
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.sql.Timestamp;
import java.time.LocalDateTime;
@Converter(autoApply = true)
public class LocalDateTimeAttributeConverter implements AttributeConverter<LocalDateTime, Timestamp> {
@Override
public Timestamp convertToDatabaseColumn(LocalDateTime locDateTime) {
return (locDateTime == null ? null : Timestamp.valueOf(locDateTime));
}
@Override
public LocalDateTime convertToEntityAttribute(Timestamp sqlTimestamp) {
return (sqlTimestamp == null ? null : sqlTimestamp.toLocalDateTime());
}
}
위의 LocalDateTimeAttributeConverter.java를 만들고, 다시 한 번 단위테스트를 실행하였더니 실제로 생성되는 DDL은 아래와 같이 createDate가 timestamp 타입으로 정상적으로 생성되었다.
Hibernate:
drop table Account if exists
Hibernate:
create table Account (
id bigint generated by default as identity,
createDate timestamp,
email varchar(255),
password varchar(255),
role varchar(255),
primary key (id)
)
실제로 H2 DB의 내용을 조회하니, 아래와 같이 이상없이 잘 나오는걸 확인할 수 있었다.
참고자료
- Java의 날짜와 시간 API, http://d2.naver.com/helloworld/645609
- How to persist LocalDate and LocalDateTime with JPA http://www.thoughts-on-java.org/persist-localdate-localdatetime-jpa/