오늘은 Spring boot2 기반으로 Jpa2.2 대해서 알아볼 예정이다. 실제 Spring boot 2.0은 기본적으로 하이버네이트 5.2를 지원한다.
그렇다고 해서 Spring boot 2.0 에 대해서 알아볼건 아니고 Spring boot 가 여러가지로 편해서 Spring boot 기반으로 작성할 예정이다. 만약 단독으로 Hibernate 를 사용한다면 여러 설정을 해야하므로 설정 관련은 따로 보는 것으 좋을 듯하다. 주로 JPA 2.2에 대해서 알아보도록 할 것이다. 시작해보자!

기본적으로 Spring boot 2.0Hibernate 5.2.16을 지원하고 있으며 (현재 이글을 쓰고 있을 때 최신버전은 Spring boot 2.0.1) Hibernate 5.2은 JPA2.1을 디펜더시 받고 있으며 2.1을 지원하고 있다. 그렇지만 Hibernate 5.2는 JPA2.2 도 일부 지원하고 있다. 그럼 지원하는 몇가지를 알아보도록 하자.

일단 spring boot 기준으로 설명하니 디펜더시도 Spring boot 기준일 것이다.

Maven Install

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.1-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>javax.persistence-api</artifactId>
    <version>2.2</version>
</dependency>

Spring data jpa 에서 hibernate-jpa-2.1-api 를 제외하고 그대신 javax.persistence-api 2.2 를 작성하면 된다. 그럼 일단 준비는 완료다.

JSR-310

JPA2.2 에서는 JSR-310 스펙인 Date and Time API를 지원한다.

@Converter(autoApply = true)
public class LocalDateTimeConverter implements AttributeConverter<LocalDateTime, Date> {

    @Override
    public Date convertToDatabaseColumn(LocalDateTime date) {
        //blabla
    }

    @Override
    public LocalDateTime convertToEntityAttribute(Date date) {
        //blabla
    }
}

예전에는 LocalDateTime, LocalDate, LocalTime 등 JSR-310을 사용하려면 Converter를 만들어서 위와 같이 변환을 했어야 했다. 하지만 JPA2.2 부터는 그럴 필요 없다. 기본적으로 LocalDate, LocalTime, LocalDateTime, OffsetTime, OffsetDateTime 등을 지원하고 있으니 참고하면 되겠다.

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private String email;

    private LocalDateTime date;

   //etc
}

그냥 위와 같이 작성해도 정상적으로 동작한다.

@Repeatable

여러 어노테이션에서 @Repeatable 어노테이션이 추가가 되었다.

  • AssociationOverride
  • AttributeOverride
  • NamedQuery
  • NamedStoredProcedureQuery
  • PersistenceUnit
  • PrimaryKeyJoinColumn
  • SequenceGenerator
  • SecondaryTable
  • SqlResultSetMapping
  • TableGenerator
  • JoinColumn
  • MapKeyJoinColumn
  • NamedEntityGraph
  • NamedNativeQuery
  • PersistenceContext

필자가 찾아본 것으로는 대략 위와 같다. 얼추 찾아본거라 더 있을 수 있거나 잘못 작성한 내용도 있을 수 있으니 다시 확인하길 바란다.
기존에는 위의 어노테이션을 여러개 사용했을 경우 아래와 같이 사용했어야 했다.

@Entity
public class Person {

    //...

    @Embedded
    private Phone phone;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "number1", column = @Column(name = "telNumber1")),
            @AttributeOverride(name = "number2", column = @Column(name = "telNumber2")),
            @AttributeOverride(name = "number3", column = @Column(name = "telNumber3"))
    })
    private Phone telNumber;
    //...
}

하지만 이제는 그럴 필요 없이 @AttributeOverride 어노테이션만 여러개 작성하면 된다. 좀 더 깔끔해졌다. 물론 위의 코드가 더 낫다고 생각할 수 도 있겠지만 필자는 아래가 더 깔끔한 것 같다.

@Entity
public class Person {

    //...

    @Embedded
    private Phone phone;

    @Embedded
    @AttributeOverride(name = "number1", column = @Column(name = "telNumber1"))
    @AttributeOverride(name = "number2", column = @Column(name = "telNumber2"))
    @AttributeOverride(name = "number3", column = @Column(name = "telNumber3"))
    private Phone telNumber;

    //...
}

@Repeatable어노테이션을 이해하려면 좀 더 내용을 찾아보기를 권장한다. 그렇게 어려운 내용은 아니니 말이다.

resultStream

EntityManager로 통해서 값을 가져올 경우에 Stream 으로 변환해서 가져올 수 있다. 실제 이 메서드는 TypedQuery 인터페이스의 default 메서드로 List 를 Stream 으로 변환하는 단순한 메서드 이다.

default Stream<X> getResultStream() {
    return getResultList().stream();
}

딱히 어려운 부분은 없는 것 같다.

@Test
void entityManagerStream(@Autowired EntityManager entityManager) {
    entityManager.persist(new Person("wonwoo", "wonwoo@test.com", LocalDateTime.now(),
            new Phone("000", "111", "2222"), new Phone("333", "444", "5555")));
    List<PersonDto> persons = entityManager.createQuery("select p from Person p", Person.class)
            .getResultStream()
            .map(person -> new PersonDto(person.getName(), person.getEmail())).collect(Collectors.toList());
    assertThat(persons).hasSize(1);
    PersonDto personDto = persons.iterator().next();
    assertThat(personDto.getName()).isEqualTo("wonwoo");
    assertThat(personDto.getEmail()).isEqualTo("wonwoo@test.com");
}

위와 같이 map, filter등 변환할 것이나 stream 메서드를 사용하고 싶다면 getResultList() 메서드 말고 getResultStream() 메서드를 사용하면 된다.

CDI Injection

글쎄다. 일단 CDI Injection 스펙은 JPA2.2 에 존재하지만 아직 Hibernate에서는 지원하지 않는 것으로 보인다. 아니면 필자가 잘몰라서..

@Converter(autoApply = true)
public class TypeConverter implements AttributeConverter<Type, String> {

    @Inject
    private PersonService personService;

    @Override
    public String convertToDatabaseColumn(Type attribute) {
        return attribute.getType();
    }

    @Override
    public Type convertToEntityAttribute(String dbData) {
        return new Type(dbData);
    }
}

위와 같이 DI를 받을 수 있다는 내용 같다. 하지만 필자는 동작하지 않는다.ㅠㅠ 만약 되는 것이라면 피드백을..

참고

하다가 몇가지 발견한게 있는데 뭐 중요한 내용은 아니지만 그래도 혹시나 누군가 이런 상황이 온다면 잘 해결하길 바란다. (무책임) 필자는 그냥 테스트한번 해볼려다 당한거니..
Hibernate 5.2의 SessionFactoryEntityManagerFactory를 상속 받고 있다.

public interface SessionFactory extends EntityManagerFactory, HibernateEntityManagerFactory, Referenceable, Serializable, java.io.Closeable {
   // ...
}

기존에는 다음과 같은 형태의 인터페이스 였다.

public interface SessionFactory extends Referenceable, Serializable, java.io.Closeable {
   //...
}

Hibernate 의 Session 또한 EntityManager를 상속 받고 있다. 기존의 Session 인터페이스는 다음과 같다.

public interface Session extends SharedSessionContract, java.io.Closeable {
   //..
}

하지만 현재 Hibernate 5.2에서는 다음과 같다.

public interface Session extends SharedSessionContract, EntityManager, HibernateEntityManager, AutoCloseable {
   //...
}

그것도 모르고 필자가 Hibernate API 를 사용하고 싶어서 문서에 있는 그대로 설정하였으나 제대로 동작하지 않았다. 아마도 그이유는 Spring boot의 자동 설정 때문일 것 같다.

@ConditionalOnMissingBean({ LocalContainerEntityManagerFactoryBean.class,
        EntityManagerFactory.class })
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
        EntityManagerFactoryBuilder factoryBuilder) {
  // ...
}

딱히 필자가 중요한 내용은 아니라서 그냥 알고만 있는 중이다. 나중에 필요하다면 좀 더 자세히 살펴보려고 한다. 혹시나 그 이유와 해결책을 알고 있다면.. 댓글로!
위의 예제 코드들은 여기에 있으니 참고하면 되겠다.

이상으로 오늘은 Hibernate 5.2의 JPA2.2를 알아봤다.
JPA 는 어렵다.