책에 있는 예제를 정리하면서 배워보자


JPA를 사용하면서 성능상 가장 조심해야 하는 것이 바로 N+1 문제다. N+1 문제가 어떤 것인지 알아보자.

em.find() 메소드로 엔티티를 조회할 때 연관된 엔티티를 로딩하는 전략이 즉시 로딩이면 데이터베이스에 JOIN 쿼리를 사용해서 한 번에 연관된 엔티티까지 조회한다. 다음 예제는 Order.member 를 즉시 로딩으로 설정했다. em.find()로 조회해보자.

Order order = em.find(Order.class, 1L);

실행된 SQL은 다음과 같다.

select o.*, m.*
from Order o
left outer join Member m on o.Member_ID=m.Member_ID
where O.id=1

실행된 SQL을 보면 즉시 로딩으로 설정한 member 엔티티를 JOIN 쿼리로 함께 조회한다. 여기까지 보면 글로벌 즉시 로딩 전략이 상당히 좋아보이지만 문제는 JPQL을 사용할 대 발생한다.

위처럼 즉시 로딩으로 설정했다고 가정하고 JPQL로 조회해보자.

List<Order> orders = em.createQuery("select o from Order o", Order.class)
												.getResultList(); //연관된 모든 엔티티 조회

실행된 SQL은 다음과 같다.

select * from Order              //JPQL로 실행된 SQL
select * from Member where id=?  //EAGER로 실행된 SQL
select * from Member where id=?  //EAGER로 실행된 SQL
select * from Member where id=?  //EAGER로 실행된 SQL
select * from Member where id=?  //EAGER로 실행된 SQL
select * from Member where id=?  //EAGER로 실행된 SQL
...

JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용한다.

따라서 즉시 로딩이든 지연 로딩이든 구분하지 않고 JPQL 쿼리 자체에 충실하게 SQL을 만든다.

코드를 분석하면 내부에서 다음과 같은 순서로 작동한다.

  1. select o from Order o JPQL을 분석해서 select * from Order SQL을 생성한다.
  2. 데이터베이스에서 결과를 받아 order 엔티티 인스턴스들을 생성한다.
  3. Order.member 의 글로벌 페치 전략이 즉시 로딩이므로 order를 로딩하는 즉시 연관된 member 도 로딩해야한다.
  4. 연관된 member를 영속성 컨텍스트에서 찾는다.