예시)
SQL 1번으로 100명의 회원을 조회하였는데,
각 회원마다 주문한 상품을 추가로 조회하기 위해 100번의 SQL을 추가로 실행하는 상황을 말한다.
한번 SQL을 실행해서 조회된 결과 수만큼 N번 SQL을 추가로 실행한다고 해서 N+1 문제라 한다.
N+1 문제는 엔티티 글로벌 페치전략에 즉시 로딩을 사용할 때 발생할 수 있다.
JPA를 사용하면서 성능상 가장 조심해야 하는 문제가 바로 N+1 문제이다.
em.find()메소드로 엔티티를 조회할 때 연관된 엔티티를 로딩하는 전략이 즉시 로딩이면
데이터베이스에 JOIN 쿼리를 사용해서 한 번에 연관된 엔티티까지 조회한다.
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();
// 연관된 모든 엔티티를 조회한다.
JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용한다.
따라서 즉시 로딩 / 지연 로딩을 구분하지 않고 JPQL 쿼리 자체에 충실하게 SQL을 만든다.
위 JPQL을 실행하면 아래와 같은 과정이 일어난다.
1) select o from Order o JPQL을 분석해서 select * from OrderSQL을 생성한다.
2) DB에서 결과를 받아 order 엔티티 인스턴스들을 생성한다.
3) Order.member의 글로벌 페치 전략이 즉시 로딩이므로 order를 로딩하는 즉시 연관된 member도 로딩해야 한다.
4) 연관된 member를 영속성 컨텍스트에서 찾는다.
5) 만약 영속성 컨텍스트에 없으면 SELECT * FROM MEMBER WHERE id=?SQL을 조회한 order 엔티티 수만큼 실행한다.
⇒ 만약 조회한 order 엔티티가 10개이면 member를 조회하는 SQL도 10번 실행된다.
이처럼 처음 조회한 데이터 수만큼, 다시 SQL을 사용해서 조회하는 것을 N+1 문제라고 한다.
N+1이 발생하면 SQL이 상당히 많이 호출되므로 조회 성능에 치명적이다.
이러한 N+1 문제는 JPQL 페치 조인으로 해결할 수 있다.
글로벌 페치 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에 영향을 주므로 너무 비효율적이다.
이번에는 JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있는 페치 조인을 알아보자.
즉 JPQL만 페치 조인을 사용하도록 변경해보자.
// 페치 조인 사용 전
JPQL : select 0 from Order o
SQL : select * from Order
// 페치 조인 사용 후
// join 명령어 마지막에 fetch를 넣어주면 된다.
JPQL :
select -
from Order o
join fetch o.member
SQL:
select o.*, m.*
from Order o
join Member m on o.MEMBER_ID=m.MEMBER_ID
이러한 JPQL 페치 조인은 N+1 문제를 해결하면서 화면에 필요한 엔티티를 미리 로딩하는 형식적인 방법이다.
즉 다시 둘을 정확히 비교하자면, N+1 문제에서는 한 개의 Order 엔티티를 조회할 때
select * from Order
SELECT * FROM MEMBER WHERE id=?
위와 같이 Order의 연관객체 Member를 즉시 로딩하기 위해 총 2번의 SQL이 실행되지만,
JPQL 페치 조인을 사용하면
select o.*, m.*
from Order o
join Member m on o.MEMBER_ID=m.MEMBER_ID
와 같이 연관 객체 Member를 함께 조회하기 위해 join을 활용한 1번의 SQL문만 실행되게 된다.
페치 조인이 현실적인 대안이긴 하지만 무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가할 수 있다.
(= 특정 화면 전용에서만 사용될 수 있는 리포지토리 메소드의 증가. 이럴 경우 메서드 재사용 불가)
결국 프레젠테이션 계층(view)이 알게모르게 데이터 접근 계층(repository)을 침범하는 것이다.
각 화면마다 즉시로딩, 지연 로딩의 요구사항이 다르다면 각각 다른 메소드를 정의하고,
각 화면에서 필요한 메서드만 호출하는 방식으로 최적화 할 수 있지만, 뷰와 리포지토리 간에 논리적인 의존관계가 발생한다.
이럴땐 무분별한 최적화로 프레젠테이션 계층과 데이터 접근 계층 간에 의존관계가 급격하게 증가하는 것보다는 적절한 선에서 타협점을 찾는 것이 합리적이다.
[Java, JPA] 도메인 객체간 연관관계에서 즉시로딩과 지연로딩 and 프록시 객체 (0) | 2019.08.11 |
---|
댓글 영역