반응형

N+1 이란?

예를 들어 Stuedent 엔티티에 GradeInfo엔티티와 ClassInfo엔티티가 EAGER Loading으로 연관매핑된 경우

@Comment("교실정보")
@ManyToOne //디폴트 값은 (fetch = FetchType.EAGER)
@JoinColumn(name = "class", referencedColumnName = "classId")
private ClassInfo class;


@Comment("학년정보")
@ManyToOne
@JoinColumn(name = "grade", referencedColumnName = "gradeId")
private GradeInfo grade;

아래와 같은 JPA 쿼리메소드를 사용할 때

studentRepository.findAll();

findAll() 수행 시점에 Student 엔티티를 조회하는 select 쿼리와

+ 매핑된 GradeInfo 엔티티를 조회하는 select 쿼리,

+ 매핑된 ClassInfo 엔티티를 조회하는 select 쿼리,

모두 수행된다.

예를 들어 1000개의 Student 엔티티는 각각 1,2,3,4,5,6 이라는 6개 중 하나의 gradeInfo 값을 가질 수 있고

1-1, 1-2, 1-3, 1-4, 1-5, 1-6 부터 6학년 6-6 까지 이라는 총 36개중 하나의 ClassInfo 값을 가질 수 있다하면

전체 Student 를 조회할 시 student에 연관된 GradeInfo 엔티티를 조회하기 위해 총 6번의 select,

ClassInfo 엔티티를 조회하기 위한 36번의 select가 추가로 나감. 이것을 N+1 문제라고 함

 

왜 join으로 쿼리 생성이 되지 않고 select가 각각 나가는걸까

=> JPA가 메소드 이름을 분석해서 JPQL을 생성하고 실행함.
=> JPQL을 생성할때는 fetch 전략을 참고하지 않기 때문

 

LAZY Loading 전략을 사용하면 N+1문제가 해결되나?

=> 해당 엔티티에 연관매핑된 엔티티를 조회(사용)할 때 select가 추가로 나가게 됨. 

=> 또는 엔티티 return 시에 결국 select하게 됨

=> 해결 안 됨

 

QueryDSL을 사용해도 N+1 문제가 생기나

QueryDSL은 JPQL 빌더역할을 해주는 것이기 때문에 QueryDSL 로 entity select하는 경우에도 발생함.

leftJoin을 걸어놨지만 join이 되지않고 select가 각각 나간다.

public List<Goods> nplus1test(){
        List<Student> result = queryFactory
                    .select(qStudent) 
                    .from(qStudent)
                    .leftJoin(qStudent.grade,qGradeInfo)
                    .leftJoin(qStudent.class,qClassInfo)  
                    .fetch();

 

entity 전체가 아닌 컬럼을 지정해서 뽑는다면 해결되나

컬럼 지정시 join이 되지만 컬럼을 지정해서 뽑으면 tuple 로 반환되어  불편함

public List<Tuple> nplus1test(){
  List<Tuple> result = queryFactory
                                  .select(qStudent.name,qStudent.grade)
                                  .from(qStudent)
                                  .fetch();
  for(Tuple tuple : result){
          System.out.println(tuple.get(qStudent.name));
          System.out.println(tuple.get(qStudent.grade));
  }
  return result;
}

=> Projections.bean 혹은 Projections.fields를 사용해 DTO를 반환받으면 됨

 

그래도 entity 전체를 select하고 싶다면

EntityGraph를 사용하면 됨

EntityGraph : DataJPA에서 fetchjoin을 어노테이션으로 사용할 수 있도록 하였다. 연관관계가 지연로딩으로 되어있는 엔티티를 조회할 경우 fetch join을 사용한다. 

fetchjoin : select 대상 엔티티와 fetch join이 걸려있는 엔티티를 포함해 join하여 select 함. 

EntityGraph 사용 예시

@EntityGraph(attributePaths = {"class","grade"})
List<Student> findAll();

 

fetchjoin 사용 예시

@Override
public List<Goods> nplus1test(){
        List<Goods> result = queryFactory
                    .select(qStudent)
                    .from(qStudent)
                    .leftJoin(qStudent.class,qClassInfo)
                    .fetchJoin()  
                    .leftJoin(qStudent.grade,qGradeInfo)
                    .fetchJoin()    
                    .fetch();
        return result;
}
반응형

+ Recent posts