[JPA] N+1 문제
면접 때 자주 나오는 단골 문제 N+1 Ploblem이다.
내가 맡고 있는 팀은 그동안 테이블을 비정규화 해서 사용을 하고 있었기에 엔티티가 관계를 맺고 있지 않아서 이 문제를 만날 일이 없었다.
이번에 현금영수증 관련 기능을 개발하면서 그동안 중복저장하던 데이터를 좀 효율적으로 사용하기 위해 정규화를 해서 테이블을 쪼갰다.
드디어 N+1 문제를 만났다.
EntityA, EntityB 가 있는데 EntityB 안에 EntityA를 포함하고 있다고 치자.
FetchType이 EAGER 라면 EntityB를 조회하면 자동으로 EntityA 를 조회하는 쿼리까지 수행이된다.
조회하는 EntityB의 개수가 10개라면 각각 관련된 EntityA까지 추가로 +10회 조회가 되어 총 11회의 조회가 발생한다.
그렇다고 FetchType이 LAZY로 바꾼다면 1회의 EntityB를 조회하는 쿼리가 수행된다.
EntityA entityA= entityB.getEntityA() 를 하는 순간에는 문제가 없다.
entityA.getId() 도 문제가 없다. (컬럼 연관이 되는 FK 필드)
다른 프로퍼티의 getter를 호출하면 아래와 같은 예외가 발생한다.
org.hibernate.LazyInitializationException: could not initialize proxy [com.tistory.payment.dao.db.payment.entity.cashreceipt.EntityB#20] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:170)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:310)
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
at com.tistory.payment.dao.db.payment.entity.cashreceipt.EntityA$HibernateProxy$VFTDFzBv.getCashReceiptNumber(Unknown Source)
스프링에서 이 문제를 해결하는 쉬운 방법은 @Transactional을 붙이는 방법이 있다.
이렇게 하면 스프링은 해당 스코프의 메서드를 세션 관리로 처리를 해주므로 getter를 호출할 때 조회를 한다.
다만 주의할 점은 @Transactional로 묶이게 되면 명시적으로 save를 하지 않아도 메서드 영역을 넘어갈때 엔티디를 update를 하게 되므로 기존의 동작과 달라질 수 있다.
(나는 일부러 여러번 테스트를 하기 위해 주석으로 save를 처리해놓았는데 @Transactional 로 바꾸고 나서 자동으로 persist가 되는 현상을 보게 되었다. 다음 stackoverflow에 mowwalker가 잘 설명해놓았다.