심화부트캠프에서 프로젝트 발표를 들어보면 생각보다 많이 나왔던 기술적의사결정 사항이였다.
JPA에 대해서는 이전 포스트에서 설명했다.
DB에 접근할때 사용되는 방법인데, JPA를 통해 연관 관계가 설정된 엔티티를 조회할 경우 조회된 데이터 개수만큼 연관관계의 조회 쿼리가 추가로 발생하게 되는 현상을 말한다.
예를 들어 내 벨로그에는 여러개의 게시글이 있고, 그 게시글에는 또 여러 댓글들이 존재한다.
내가 쓴 글을 조회하게되면 우선 게시글을 조회하게 될 것이고, 게시글마다 댓글을 조회하게되면 추가 쿼리가 발생하게된다.
바로 이때 JPA의 N+1 문제가 발생하게 된 것이다.
하나의 동작을 하는데 N+1 번의 쿼리 조회가 발생한다.
Action : 한 게시글에 댓글조회 (댓글이 5개 있다고 가정할때 N = 5)
쿼리조회 : 게시글 조회 1회 + 댓글 5개 조회 (5+1 = 6회의 쿼리조회 발생)
게시글과 댓글을 조회할때 이런 문제가 발생하는 이유는 게시글과 댓글이 1:N 관계가 설정되어서이다.
SELECT * FROM article;
SELECT * FROM comment WHERE article_id;
글로벌 패치 전략을 즉시로딩으로 설정시 findAll() 을 실행하게 되면 N+1 문제가 발생한다.
findAll()의 경우 내부적으로 SELECT u FROM User u 라는 JPQL 구문을 실행하게 된다.
JPQL은 글로벌패치 전략을 고려하지 않고 쿼리를 실행하게되는데, 이때 User를 조회하고, 즉시로딩 설정 확인 후 연관관계에 있는 모든 엔티티를 조회하는 쿼리를 실행한다.
글로벌패치 전략이 지연로딩(lazy) 상태에서 findAll()을 실행하게 되면 어떻게 될까?
N+1 문제가 발생하지 않는다!
연관관계에 있는 엔티티를 실제 객체가 아닌 프록시 객체로 생성 및 주입하기 때문에 N+1 문제가 발생하지 않는다.
N+1문제는 다음과같은 해결법을 고려할 수 있다.
JPQL 쿼리에서 명시적으로 Fetch Join을 사용해 연관된 엔티티를 즉시로딩한다.
@Query("SELECT p FROM Parent p JOIN FETCH p.children")
List<Parent> findAllWithChildren();
@EntityGraph 어노테이션을 이용해 연관된 엔티티를 미리 페치조인하여 조회한다.
@EntityGraph(attributePaths = "children")
@Query("SELECT p FROM Parent p")
List<Parent> findAllWithChildren();
엔티티 생성시 @batchSize 어노테이션을 사용해 크기를 지정해주거나
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
@BatchSize(size = 10)
private List<Child> children;
Hibernate 글로벌 설정으로 Batch Fetch Size를 지정해주는 등 설정해 놓은 특정크기의 연관 엔티티를 한번에 불러오는 방법으로 N+1 문제를 해결할 수 있다.
spring.jpa.properties.hibernate.default_batch_fetch_size=10
당연하게도 N+1 문제 발생시 조회성능이 떨어져 전체적인 서비스의 성능 저하를 유발할 수 있다.
위에서 언급한 해결방안인 @EntityGraph 어노테이션이나 FETCH JOIN을 적절히 사용해 해결해야 하고, DTO를 사용해 꼭 필요한 데이터만 사용하는 방법도 고려해보아야한다.
면접 질문 내용과 답변의 일부는 기술 면접 구독 서비스 - 매일메일 에 있다.
흥미로웠다면 구독해보는 것도 추천한다!