N+1 이란?


n+1 문제란 1개의 쿼리를 위해서 추가적으로 발생하는 n개의 쿼리를 말한다. JPA의 성능 문제는 대부분 n+1 문제를 해결하는 데서 시작된다고 한다. 우선 이 문제가 왜 생기는 지 알아보자.

JPA는 객체 지향 설계를 따르므로, 연관관계를 통한 객체 그래프 탐색을 보장해야 한다. id가 1번인 Member의 정보를 가져온 뒤 그가 속한 Team의 Name을 검색하는 쿼리를 실행한다고 하자. Member.Team에 실제 엔티티가 아닌 프록시 객체가 들어있다면 쿼리를 해서 데이터를 채워야 한다. Member의 fk인 team_Id값을 이용해 WHERE 조건으로 Team 엔티티를 알아오는 것이다. 이 경우 1번의 SELECT Member 쿼리와 해당 Member의 Team.Name을 찾기 위한 SELECT 쿼리가 추가로 발생해 1+1 = 2번이 발생한다.

페치 전략 때문일까?


위의 예시 상황은 페치 전략이 LAZY일 때의 상황이지만 사실 EAGER이든 LAZY이든 상관없이 n+1 문제가 발생한다. 두 전략의 차이는 JPQL이 종료되는 시점에 영속성 컨텍스트를 살펴본 뒤 추가 쿼리를 날릴 것인지, 아니면 실제로 사용 요청이 들어올 때 프록시 초기화를 하여 쿼리를 날릴 지 시점의 차이일 뿐이다.

이제 어떤 코드가 n+1 문제를 일으키는지 보자.

@BeforeEach
void init(){
    for(int i = 0; i < 100; i++){
        Team team = new Team(i+"팀");
        em.persist(team);
        em.persist(new TestMember(i+"번째 GJ S", team));
    }
		em.flush();
    em.clear();
}

@Test
void nproblemTest(){
    List<TestMember> findMembers = em.createQuery("select m from TestMember m", TestMember.class)
																			.getResultList();
    
    for (TestMember member : findMembers) {
        System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
    }

}

이 코드는 100명과 100개의 팀이 있는 경우이다. 콘솔을 확인하면 전체 멤버인 100명을 조회하는 쿼리(1) + 100명의 각기 다른 팀을 조회(100), 따라서 총 101번의 쿼리가 호출되었다. 이 때 Team이 하나밖에 없고 모든 멤버가 이에 속한다면 1+1의 문제밖에 되지 않는다. 그러나 실제 사용하는 쿼리는 전자인 경우가 압도적으로 많다.

해결 방법