N+1문제는 통상적으로 여러개의 결과를 가져오는 1개의 쿼리를 실행했을 때, N개 쿼리가 추가로 발생하는 문제를 말한다. 프로세스 입장에서 I/O바운드인 DB쿼리는 굉장히 비싼 시간적 비용임으로, 서버에 성능적인 하락과 함께 DB에 불필요한 부하를 일으킨다.
EntityManager가 N개의 결과를 가져오는 쿼리를 1개 실행하고 나서야, 나머지 쿼리의 조건을 지정할 수 있다. 즉, 첫번째 쿼리의 결과가 두번째 쿼리의 조건이 된다. 하지만 진짜 문제는 두번째 쿼리에서 발생한다.
JPA는 첫번째 쿼리의 결과로 부터 연관된 Entity의 ID를 조건으로 N개의 쿼리를 날린다. 최악의 상황에는 아무생각 없이 한 FindAll() 요청 한번이 해당 테이블의 데이터 갯수만큼의 쿼리를 날리는 것이다.
그렇다면 JPA는 두번째 쿼리를 왜 이렇게 비효율적으로 보낼까?
N+1의 발생 원인을 상세하게 알기 위해서, FindAll 과 같은 메소드가 어떻게 동작하는지 알아야 한다.
JPA는 EntityManger의 createQuery()메소드를 통해 JPQL을 사용하여 DB에 질의 한다. 이때 EntityManager는 쿼리의 조건에 따라서 쿼리를 다르게 처리한다/
ID조건을 통한 쿼리의 경우, Persistence Context에 캐시된 데이터가 있다면 이를 가져오고, 아닐 경우 D로 SQL 쿼리를 보낸다. (Persistence가 트랜잭션 레벨의 캐싱 역할)
ID가 아닌 다른 조건을 갖는 쿼리의 경우, Persistence Context를 체크하지 않고, 항상 DB에 바로 쿼리를 날린다.
둘 중 어떤 경우든, 쿼리결과를 받아온 후 영속성 컨텍스트에 덮어쓴다.
JPA는 두번째 쿼리를 기본적으로 ID조건을 이용하여 쿼리한다. 즉, 조회할 두번째 Entity들이 Persistence Context에 이미 존재하여 Cache hit 하기를 기대하는 것이다.
사실, 이 방법이 아니면 JPA는 전체 ID에 대해서 굉장히 큰 한덩이의 WHERE IN
쿼리를 날려야 한다. 그리고 이것은 DB에 N x M번의 비교 연산을 수행하게 한다. 어쩌면, JPA의 관점에서는 DB에 큰 연산 하나를 수행시키기 보다 Cache hit을 바라면서 최대한 작은 트랜잭션 단위로 쿼리를 보내는 것이 최선의 선택지가 아니었을까 싶다.
N+1 쿼리 문제는 1:N 관계의 엔티티에 대한 쿼리를 날릴 때 발생하며, 두가지 경우로 나뉜다.
EAGER_LOADING
으로 설정된 칼럼을 가진 엔티티를 기준으로 쿼리할 때.LAZY_LOADING
으로 설정된 칼럼을 가진 엔티티의 Collection에 대해서 참조 엔티티를 사용 할 때즉, EAGER_LOADING
이나LAZY_LOADING
이나, N:1 관계의 엔티티의 Collection에 대해서 연속적인 접근을 하는 이상 무조건 발생한다.
TODO: 수정 중
앞서 말했듯이, N+1쿼리 문제는 ORM의 한계 중 하나이므로 완전히 극복할 방법은 없다. N+1 쿼리문제를 이해하지 않고 사용하게되면 어떤 쿼리를 실행할지 모르기 애플리케이션의 성능에 치명적인 경우가 많다. 따라서 적재적소에 맞게 해결책을 사용해야한다.
명시적 EagerLoading, FetchType.LAZY 무의미.
카티잔 곱으로 조인됨
ManyToOne, OneToOne 관계의 A, B Entity에서 A를 기준으로 조회 시 A의 갯수만큼 가져오게됨 (중복된 값이 프로퍼티로 숨음, 주 엔티티객체에는 중복이 없음)
하지만 OneToMany, ManyToMany 관계의 A, B Entity에서 A를 기준으로 조회 시 (A의 갯수) x (B의 갯수) 만큼의 엔티티 객체가 조회됨.(중복된 값이 드러남)
@OneToMany에서 둘 이상의 컬렉션을 페치 할 수 없다. simultaneously fetch multiple bag 오류 뜸
즉, ManyToOne or OneToOne 관계에서의 사용에 최적화 되어있음 -> 다른 경우는 중복 제거 필요.
fetch join vs join
그냥 join 은 JPQL통해서 모든 데이터를 가져오지만, 연관관계를 고려하지 않고 select 의 대상으로 지정한 엔티티만 리턴됨.
리턴되지 않은 컬렉션을 사용하는 시점에서 N+1 쿼리 문제 그대로 발생