Spring

JPA - N+1

jaewoo 2022. 9. 19. 21:13

JPA 에서 연관관계 @OneToMany를 사용하다보면 당연하게 만나게 될 수 있는 이슈이다.

하나에 객체(Board - 1) 을 조회하는 쿼리를 날리면 따라서 하위 객체(BoardImage - N)이 계속해서 쿼리가 호출된다.

 

Board 와 BoardImage는 일대다 관계이다. 

1. Board

코드가 너무 길어서 중요부분만 정리

Board와 BoardImage는 양방향 관계이다. 그러므로 Board에서 imageSet 필드를 가지고 있음

2. BoardImage

 

서로 1대다 관계이다. 여기서 만약 Board에 PK를 참조하는 BoardImage 객체를 호출하는 테스트 코드를 실행해보자

지연로딩이기에 @Transactional로 묶어줘야한다. 

테스트 성공하면 이렇게 Board를 조회하는 쿼리 하나와 그 Board에 PK를 참조하는(where로 체크함) BoardImage를 조회하는 쿼리가 하나 날아간다. 

 

여기서 만약 여러개 Board 객체를 조회한다면?

테스트 코드를 실행해보자

이 쿼리가 계속해서 BoardImage에 데이터만큼 쿼리가 실행된다. Board를 조회하는 쿼리는

딱 한번 select * from Board; 형태로 날아간다. 그러면서 Board 객체를 참조하는 BoardImage 를 조회하는 쿼리가 N개가 실행되버려서 N+1 이슈이다. BoardImage 조회하는 쿼리는 위와 같이 select * from BoardImage where board_bno=?;

로 날아가는데 BoardImage가 참조하는 데이터가 하나라도 있는 Board 객체만큼 select 쿼리가 실행된다.

 

이 문제를 해결하는 방법은 여러가지가 있다. 2가지를 살펴볼 것이다.

 

1.EntityGraph

 

먼저 Repository에 JPQL을 사용하여 method 하나 선언한다. 그리고 위에 @EntityGraph로 하위 엔티티 타입에 필드명을 attrubutePaths 속성값으로 설정해준다. 그러면 이 하위엔티티를 join 처리하여 같이 조회가 가능하다.

join으로 같이 로딩하기에 @Transactional은 없어도 무방하다.

SELECT b,i FROM Board b,BoardImage i LEFT OUTER JOIN i on b.bno = i.board_bno;

이렇게 하위 엔티티와 join 처리하여 쿼리가 날아가고 다른 쿼리는 실행되지 않는다.

 

2.@BatchSize 

상위 엔티티에서 하위 엔티티와 연관관계가 되어있어 그 부분을 호출하기 때문에 계속 쿼리가 실행되는 것이었기에 상위 엔티티에서 하위엔티티 필드위에 @BatchSize()를 선언해주면 된다. 

이건 select * from BoardImage i where i.board_bno  in (?,?,?);

이런식으로 쿼리가 실행된다. in 뒤에 크기는 size 속성값으로 선언한만큼 조절된다.

Board를 조회하면 그 Board를 참조하는 BoardImage 객체를 조회하는 쿼리를 가져오는 것이 문제였다. 그렇기에

Board에 데이터 갯수만큼 N 번에 쿼리가 일어났던 것인데 이걸 N번을 size만큼 묶어서 실행한다고 보면 이해가 쉽다.

Board 데이터는 총 101개이다. 여기서 size는 10이므로 총 11번에 쿼리가 실행될 것이다. 101/10 은 10이고 나머지 1번에 대한 조회 쿼리가 실행된다.

이렇게 select i from BoardImage i where i.board_bno in (? *10); 이 10번 실행되고

마지막에 데이터 하나를 조회하는 쿼리가 실행된다. 이렇게 총 101개에 boardImage데이터를 11번에 조회쿼리로 가져온다. 만약 @BatchSize에 속성값인 size 크기를 더 늘린다면 조회쿼리가 더 줄어드는 것을 알 수 있다.