Spring jsr305

오늘은 Spring5 부터 지원하는 jsr305 어노테이션에 대해서 알아보자. 많은 이야기는 아니지만 Spring 에서 이 어노테이션을 몇가지 기능을 지원해 주고 있다.

Spring에서 사용하는 Nullable, NonNull, NonNullApi 어노테이션은 jsr305의 메타 어노테이션을 사용한다. 실제 간단히 코드를 보자면 다음과 같다.

//...
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierDefault;

//...
@Nonnull
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
public @interface NonNullApi {
}

jsr305 어노테이션은 그냥 메타 어노테이션으로만 사용하고 있다. 하지만 Spring 에서는 몇가지 기능을 지원해주고 있으니 알아보도록 하자.

Controller

Spring web에서 흔히 파라미터로 받을 경우 사용할 수 있다. @RequestParam 어노테이션을 사용할 경우 required 속성의 기본값은 true이다. 그래서 name이라는 파라미터를 보내지 않을 경우 에러가 발생한다.

@GetMapping("/")
public String hello(@RequestParam String name) {
  return "hello " + name + "!";
}

만약 위와 같이 @RequestParam의 required 속성을 false로 하지 않을 경우 아래와 같이 에러가 발생한다.

http :8080

HTTP/1.1 400
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 12:38:23 GMT
Transfer-Encoding: chunked

{
    "error": "Bad Request",
    "message": "Required String parameter 'name' is not present",
    "path": "/",
    "status": 400,
    "timestamp": "2018-05-07T12:38:23.120+0000"
}

물론 required 속성을 false로 해도 되지만 Spring5 부터는 @Nullable 어노테이션을 사용해서 null을 허용해도 된다.

@GetMapping("/")
public String hello(@RequestParam @Nullable String name) {
  return "hello " + name + "!";
}

위와 같이 @Nullable 어노테이션을 사용했을 경우에는 아래와 같이 에러가 발생하지 않는다.

http :8080

HTTP/1.1 200
Content-Length: 11
Content-Type: text/plain;charset=UTF-8
Date: Mon, 07 May 2018 12:41:14 GMT

hello null!

Endpoint

위와 비슷한 동일한 맥략이다. 커스텀한 Endpoint를 만들 경우에 @Nullable 어노테이션을 사용할 수 있다.

@Endpoint(id = "hello")
@Component
public class HelloEndpoint {

  @ReadOperation
  public String hello(String name) {
    return "hello " + name + "!";
  }
}

만약 위와 같은 코드를 작성했을 경우 name이라는 파라미터를 보내지 않으면 위와 동일하게 에러가 발생한다.

http :8080/actuator/hello

HTTP/1.1 400
Connection: close
Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8
Date: Mon, 07 May 2018 12:44:03 GMT
Transfer-Encoding: chunked

{
    "error": "Bad Request",
    "message": "Missing parameters: name",
    "path": "/actuator/hello",
    "status": 400,
    "timestamp": "2018-05-07T12:44:03.471+0000"
}

하지만 여기에서도 @Nullable 어노테이션을 작성하여 null을 허용할 수 있다.

@Endpoint(id = "hello")
@Component
public class HelloEndpoint {

  @ReadOperation
  public String hello(@Nullable String name) {
    return "hello " + name + "!";
  }
}

다음과 같이 작성할 경우에는 파라미터를 보내지 않아도 에러가 발생하지 않는다.

http :8080/actuator/hello
HTTP/1.1 200
Content-Length: 11
Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8
Date: Mon, 07 May 2018 12:45:17 GMT

hello null!

Null injection

Spring5 에서는 null을 허용하는 주입을 @Nullable 어노테이션을 사용하여 주입하면 된다. 예전에는 @Autowired 어노테이션의 required 속성을 false로 하면 주입하는 Object가 null 이어도 에러가 발생하지 않고 null 그대로 주입한다. 또한 필자는 요즘에 주입받는 Object에 @Autowired를 잘 작성하지 않는다. Spring 4.3 부터는 생성자 혹은 빈의 디펜더시 받는 Object에 @Autowired 가 존재 하지 않아도 자동으로 Spring이 주입을 해주고 있어서 좋은 기능 같다.

public class PersonService {
  //nothing
}

@Bean
ApplicationRunner applicationRunner(@Nullable PersonService personService) {
  return args -> {
  };
}

//생성자
public Some(@Nullable PersonService personService) {
  this.personService = personService;
}

위와 같이 PersonService는 빈으로 등록되지 않은 Object이다. 그래서 만약 이 상태로 주입받으려 하면 PersonService 라는 빈이 존재하지 않아 에러가 발생한다. 하지만 이제부터는 @Nullable 어노테이션을 사용해서 null을 허용하면 null이 주입된다.

만약 위와 같이 사용한다면 null check 는 꼭 해줘야 할 것 같다.

Spring data

Spring data 프로젝트에서도 jsr305 어노테이션을 지원한다. Spring data에서 query method를 사용할 경우 파라미터와 리턴값에 위와 같은 어노테이션을 작성할 수 있다.

여기서 주의할 점은 해당 패키지안에 package-info.java 를 작성해줘야 한다. 이때 사용하는 어노테이션은 @NonNullApi 어노테이션이다.

@NonNullApi
package ml.wonwoo.springjsr305.domain;

import org.springframework.lang.NonNullApi;

위와 같이 작성한다면 기본적으로 파라미터 와 리턴값은 null이 아니어야 한다.

public interface PersonRepository extends JpaRepository<Person, Long> {

  Person findByName(String name);
}

만약 NonNullApi 사용하고 위와 같이 사용한다면 name에는 null이 될수 없고 반환값도 null이 될 수 없다. 만약 null을 넣거나 null을 리턴한다면 IllegalArgumentException exception이 발생한다. 한번 controller로 테스트를 해보자.

@GetMapping("/hello")
public Person helloName(String name) {
  return personRepository.findByName(name);
}
http :8080/hello

HTTP/1.1 500
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 13:09:16 GMT
Transfer-Encoding: chunked

{
    "error": "Internal Server Error",
    "message": "Parameter of type String at index 0 in PersonRepository.findByName must not be null!",
    "path": "/hello",
    "status": 500,
    "timestamp": "2018-05-07T13:09:16.024+0000"
}

위와 같이 호출할 경우 파라미터가 null이라고 에러가 발생한다. 이번에는 리턴값이 null인 것으로 호출해보자.

http :8080/hello name==foo

HTTP/1.1 500
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 13:11:29 GMT
Transfer-Encoding: chunked

{
    "error": "Internal Server Error",
    "message": "Result must not be null!",
    "path": "/hello",
    "status": 500,
    "timestamp": "2018-05-07T13:11:29.367+0000"
}

이번에는 리턴값이 null이라고 에러가 발생한다. 한번 제대로 된 값으로 호출해보자.

http :8080/hello name==wonwoo

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 13:12:59 GMT
Transfer-Encoding: chunked

{
    "id": 1,
    "name": "wonwoo"
}

정상적으로 호출이된다. 만약 위와 다르게 null을 허용하고 싶다면 이때까지 알아본 @Nullable 어노테이션을 사용하면 된다. 이것은 파라미터와 메서드위에 작성할 수 있다.

public interface PersonRepository extends JpaRepository<Person, Long> {

  Person findByNameIsLike(@Nullable String name);

  @Nullable
  Person findByName(String name);
}

위와 같이 한다면 findByNameIsLike 파라미터는 null을 허용하고 즉 findByNameIsLike(null) 같이 메소드를 호출해도 된다. (하지만 해당 메서드는 null을 넣으면 에러가 발생한다. jsr305 때문이 아니라 쿼리 자체가 null을 허용하지 않는 듯 하다.)
또한 findByName 메서드는 파라미터에는 null을 작성하면 안되고 리턴값 자체를 null값 이어도 상관 없다.

오늘 이렇게 Spring 에서 지원해주는 jsr305 어노테이션에 대해서 알아봤다. 물론 더 많은 기능이 있을지는 모른다. 필자가 알아본 부분은 여기까지이다. 더 많은 정보가 있다면 댓글로..

이상!

Spring data common 기타 기능

오늘 이야기 하려는 것은 Spring data common 의 몇가지 기능을 알아보려고 한다. Spring data는 대부분 알고 있듯이 Query methods의 유용함을 다 알고 있을 듯하다. 그래서 따로 이부분은 설명하지 않겠다. 그래서 제목도 기타 기능이라고..

JPA를 사용할 떄 사용하는 JpaRepository나 기타 다른 스토어를 사용할때 사용하는 {store}Repository는 Spring data common 에 있는 것이 아니라 그에 따른 구현체별로 존재한다. 뭐 이미 다 알고 있겠지만 혹시나..

spring data common에 존재하는 RepositoryCrudRepositoryPagingAndSortingRepository 인터페이스만 존재하고 나머지는 그에 따른 구현체를 통해 사용해야 한다.

이 포스팅 또한 JPA기준이며 다른 스토어들에서는 동작을 하지 않을 수도 있다.

@RepositoryDefinition

spring data common에 존재하는 @RepositoryDefinition 어노테이션은 일반적으로 사용하는 그런 어노테이션은 아닌 것같다. 사용하고 싶다면 사용해야 되지만 글쎄다. 마치 그냥 Repository 인터페이스를 사용하는 것과 동일하다.

속성에는 idClassdomainClass 을 작성해주는 속성이 있다. 이 두 속성은 필수이다. Repository<T, ID> 이와 동일하다. T는 domainClass를 의미하고 ID 는 idClass 와 동일하다고 생각하면 될 듯 싶다.

예제를 통해 간단히 살펴보자.

@RepositoryDefinition(idClass = Long.class, domainClass = Account.class)
public interface AccountRepository {

    List<Account> findAll();

    Optional<Account> findById(Long id);

    Account save(Account account);
}

위와 같이 RepositoryDefinition 어노테이션을 사용했을 경우 위에서 말했듯이 Repository 인터페이스를 사용하는 것과 동일해 보인다. 위와 같이 정의 했을 경우 spring data가 적당하게 빈으로 등록을 시킨다. spring data 설정외의 따로 설정할 것은 없다.
한번 테스트를 해보자.

@RunWith(SpringRunner.class)
@DataJpaTest
public class AccountRepositoryTests {


    @Autowired
    private AccountRepository accountRepository;

    @Test
    public void save() {
        Account account = accountRepository.save(new Account("wonwoo", "test@test.com"));
        assertThat(account.getName()).isEqualTo("wonwoo");
        assertThat(account.getEmail()).isEqualTo("test@test.com");
    }

    @Test
    public void findById() {
        Account account = accountRepository.save(new Account("wonwoo", "test@test.com"));
        Optional<Account> newAccount = accountRepository.findById(account.getId());
        assertThat(newAccount.get().getName()).isEqualTo("wonwoo");
        assertThat(newAccount.get().getEmail()).isEqualTo("test@test.com");
    }

    @Test
    public void findAll() {
        accountRepository.save(new Account("wonwoo", "test@test.com"));
        accountRepository.save(new Account("kevin", "kevin@test.com"));
        List<Account> accounts = accountRepository.findAll();
        assertThat(accounts).hasSize(2);
    }
}

아주 잘 동작한다. 하지만 특별한 경우가 아니라면 그냥 Repository 인터페이스 하위의 있는 인터페이스를 사용하는 것이 나아보인다.

projections

Spring data 에서는 아주 간단하게 projections을 지원한다. 예를들어 특정한 도메인 오브젝트가 아닌 다른 오브젝트로 반환을 쉽게 할 수 있다. 몇가지 방법을 지원하니 사용하고 싶은 것으로 사용하면 되겠다.

interface

인터페이스를 사용해서 모델을 반환받을 수 있다. 사용법은 아주 간단하다.

public interface Named {

    String getName();
}

위와 같이 getter를 정의해서 사용하면 끝난다.

public interface PersonRepository extends CrudRepository<Person, Long> {

    List<Named> findByName(String name);

}

그리고 나서 위와 같이 리턴값에 해당 인터페이스를 작성하면 spring data가 자동으로 getName()을 호출할 때 해당 값을 불러오게 된다.

class

interface와 마찬가지다. 아주 쉽다. 흔히 이야기하는 dto를 만들어서 리턴값에 작성만 해주면 된다.


@Value public class PersonDto { String name; }

하지만 여기서 주의할 점은 위와 같이 lombok을 사용하다면 @Value 어노테이션을 사용해야 한다. 위 어노테이션은 불변의 클래스를 만들어주는 역할을 한다. 위의 코드를 바닐라 코드로 본다면 name이 있는 생성자와 getter가 생성된다. setter는 존재하지 않는다. 만약 lombok을 사용하지 못한다면 해당하는 필드의 생성자가 있어야 한다. 더 자세한건 테스트를 해보도록..

public interface PersonRepository extends CrudRepository<Person, Long> {

    List<PersonDto> findByName(String name);

}

그럼 위와 같이 작성하면 해당하는 dto로 반환시켜 준다. 잘되나 테스트를 해보자.

@RunWith(SpringRunner.class)
@DataJpaTest
public class PersonRepositoryTests {

    @Autowired
    private PersonRepository personRepository;

    @Test
    public void personsInterfaceProjectionsTest() {
        personRepository.save(new Person("wonwoo"));
        personRepository.save(new Person("wonwoo"));
        List<PersonDto> persons = personRepository.findByName("wonwoo");
//        List<Named> persons = personRepository.findByName("wonwoo");
        assertThat(persons).hasSize(2);
        assertThat(persons.iterator().next().getName()).isEqualTo("wonwoo");
    }
}

DynamicProjections

spring data는 DynamicProjections 도 지원한다. 이 것 또한 클래스와 인터페이스 모두 지원한다. 자신이 좋아하는 것로 개발하면 되겠다.

public interface PersonRepository extends CrudRepository<Person, Long> {

    <T> List<T> findByName(String name, Class<T> clazz);
}

위와 같이 리턴받을 타입을 제네릭으로 작성하면 spring data가 알아서 잘 변환해준다. 이 것 역시 위의 설명한 제약이 포함되어 있다. 동일하다고 생각하면 되겠다. 테스트를 해보자.

@RunWith(SpringRunner.class)
@DataJpaTest
public class PersonRepositoryTests {

    @Autowired
    private PersonRepository personRepository;

    @Test
    public void personsClassDynamicProjectionsTest() {
        personRepository.save(new Person("wonwoo"));
        personRepository.save(new Person("wonwoo"));
        List<PersonDto> persons = personRepository.findByName("wonwoo", PersonDto.class);
        assertThat(persons).hasSize(2);
    }

    @Test
    public void personsInterfaceDynamicProjectionsTest() {
        personRepository.save(new Person("wonwoo"));
        personRepository.save(new Person("wonwoo"));
        List<Named> persons = personRepository.findByName("wonwoo", Named.class);
        assertThat(persons.get(0).getName()).isEqualTo("wonwoo");
        assertThat(persons.get(1).getName()).isEqualTo("wonwoo");
    }
}

조금 괜찮은 기능이다. 필요하다면 자주 이용 해야겠다.

Custom Repository

필자도 자주 사용하는 기능이다. 대부분 querydsl을 사용할 때 이 기능을 사용한다. Spring data의 주요 장점인 Query methods를 사용할 수 없는 쿼리가 있을 경우 커스텀하게 Repository 를 만들 수 있다. 다음과 같이 몇몇 가지 코드를 작성해야 한다.

public interface CustomizedPersonRepository {

  Person selectPersonName(String name);

  void insert(Long id, String name);
}

public class PersonRepositoryImpl implements CustomizedPersonRepository {

  private final JdbcTemplate jdbcTemplate;

  public PersonRepositoryImpl(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @Override
  public Person selectPersonName(String name) {
    return jdbcTemplate.queryForObject("select * from person where name = ?",
        new BeanPropertyRowMapper<>(Person.class), name);
  }

  @Override
  public void insert(Long id, String name) {
    jdbcTemplate.update("insert into person (id, name) values (?,?)", id, name);
  }
}


public interface PersonRepository extends CrudRepository<Person, Long>, CustomizedPersonRepository {

   //etc
}

위와 같이 custom한 인터페이스를 만들고 그에 따른 구현체도 구현을 해야 한다. 당연히 커스텀하게 JdbcTemplate, querydsl등 기타 여러 가지 도구를 이용해서 구현하면 된다.
구현을 다 완료 하였다면 위와 같이 원래의 인터페이스에 자신이 만든 커스텀한 인터페이스를 상속하면 된다. 그럼 spring data가 해당 커스텀한 인터페이스를 따로 저장하여 관리한다.

여기서 몇가지 주의사항이 있다. 일단 첫 번째로 커스텀하게 만든 구현체의 네이밍이다. 기존의 PersonRepository 인터페이스를 만들어 Repository를 사용했다면 우리가 만든 커스텀클래스는 꼭 PersonRepositoryImpl 이어야 한다. 이건 규칙이다. 하지만 이 규칙은 변경할 수 있다. @EnableJpaRepositories 어노테이션에 repositoryImplementationPostfix 속성을 이용해서 변경할 수는 있다. 근데 특별한일이 아니라면 변경할 필요는 굳이 없어 보인다. 하지만 커스텀한 인터페이스의 명은 제약사항이 없다. 마음껏 네이밍해도 좋다.

필자도 이번에 하면서 알았다. 왜 이때까지 몰랐나 싶다. 생각해보면 그럴일이 없었던거 같다. PersonRepository 와 커스텀한 클래스인 PersonRepositoryImpl는 같은 패키지에 있어야 한다. 필자도 맨날 같은 패키지에 넣어서 몰랐던 것이다. 만약 다른 패키지에 넣는다면 에러가 발생한다. 대략 이정도만 주의하면 될 듯 싶다.

오늘은 이렇게 spring data common의 몇가지 기능을 대해서 알아봤다. 이보다 많은 기능이 존재한다. 따라서 각자가 문서를 보면서 공부하는 것이 좋겠다.

기회가 된다면 좀 더 많은 기능을 살펴보도록 하자.
해당 테스트 코드를 여기에 있으니 참고하면 되겠다.

Spring boot 2.0 과 Jpa2.2 그리고 Hibernate 5.2

오늘은 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 는 어렵다.