참조: https://www.youtube.com/watch?v=MpXdx8-qWzo
위 백기선님의 영상에서 jpa-hibernate의 동작방식에 대해 더 알 수 있을만한 내용이 있어서 정리하려 한다.
테스트 코드에서는 select문이 한번인데 왜 controller에서의 동작은 select를 3번할까?
member만 쿼리 한번으로 가져오는 방법?
member와 team을 같이 쿼리 한번으로 가져오는 방법?
아래의 쿼리문에서 select문이 3번이나 수행되었다. 그 이유가 뭘까?
spring은 기본적으로는 같은 트랜잭션 안에서 영속성 컨텍스트를 공유한다.
@DataJpaTest안에는 @Transactional이 포함되어 있어 테스트코드는 하나의 트랜잭션으로 실행된다.
@transactional 안에서 실행된 로직안의 것들은 같은 영속성컨텍스트를 공유한다. 때문에 datajpa의 함수가 실행되면 db보다도 우선적으로 영속성 컨테스트(jpa 1차 캐시)를 찾아본다.
즉 테스트코드안에서 이미 만들어놓은 member객체와 team객체에 대한
정보가 영속성 컨텍스트에 있기에 쿼리 한번에 조회가 가능하다. 반면 실제 동작시 controller의 동작은 객체를 생성하는 것과 별개의 트랜잭션이기에 team을 따로 한번씩 더 조회한다.
jpa는 참조객체를 한번에 가져오지 않는 lazy로딩을 지원한다. 연결된 객체를 한번에 모두 가져와야만 한다면(eager로딩) 불필요하게 쿼리문을 날려야 되는 경우가 생기기 때문에 필요하다.
이 때 객체가 lazy로딩되며 그러면 참조된 객체는 어떤 식으로 저장되는 걸까? hibernate는 proxy객체를 두어 객체참조를 지연한다.
하지만 lazy로딩을 하며 엔티티를 외부에 직접 노출한다면 아래와 같은 메시지를 볼 수 있다.
"status": 500,
"error": "Internal Server Error",
"message": "Type definition error:
[simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor];
nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor
and no properties discovered to create BeanSerializer
(to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
json이 직렬화를 하면서 proxy객체를 검사하며 타입이 맞지 않는 것을 알고 에러를 내게 된다. 위에서 proxy객체를 생성할 떄는 ByteBuddyInterceptor라는 클래스가 대신 사용됨을 알 수 있다.
이를 해결하기 위해 jsonignore을 이용하는 방법도 있지만 Dto를 사용하는 게 다른 직렬화 문제까지도 예방하는 좋은 방법일 것 같다.
3번에서 team과 member를 쿼리 한번으로 조회하기 위해서는
spring에서 @Query를 이용해 직접 join과 select문을 작성하는 방법도 있다. 하지만 이외에도 엔티티그래프를 이용하면 적은 쿼리로 객체들을 가져올 수도 있다. 어떤 객체를 사용할 때 거의 대부분 참조객체도 같이 사용되어 eager로딩을 고려해봐야 될 떄 이 엔티티그래프를 사용하는 게 좋은 방법일 것 같다.