본문 바로가기

토이프로젝트/리뷰어(영화 리뷰 사이트)

16일차 - JPA N+1 문제

JPA N+1문제

리뷰 댓글 조회 api 구현 중 N+1 문제가 발생했다.

jpa 문제는 크게 2가지 경우일 때 발생한다.

첫번째는 fetch전략을 EAGER로 했을경우이다.

특정 엔티티가 연관관계를 가지고 있을 때,

하위 엔티티의 정보를 모두 받아오도록 하는 설정이기 때문에

조회 쿼리가 엔티티 수만큼 실행되기 때문에 발생한다.

그래서 보통 LAZY로 설정하고 사용하게된다.

두번째는 LAZY로 설정했지만 하위 엔티티를 다시 조회할 경우이다.

나의 경우가 2번째에 해당한다.

리뷰의 댓글 목록을 조회하면서 작성자의 정보도 같이 받아오면서

User 테이블 조회 쿼리가 한번씩 더 실행되기 때문에 N+1 문제가 발생했다.

ReviewComment.java

@Entity
@Getter
@Table(name = "review_comment")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ReviewComment extends BaseEntity {

  @Id
  @Column
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable = false, length = 300)
  private String contents;

  @Column
  private Long likeCount;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(referencedColumnName = "id")
  private User user;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(referencedColumnName = "id")
  private Review review;

}

해당 도메인의 목록인 List<ReviewComment>를 받아오도록 했을 경우 실행되는 쿼리는 다음과 같다.

 

 

한번의 api를 실행했지만, 총 4번의 쿼리가 실행됐다.

맨 윗줄은 reviewComment를 조회하고, 나머지  3개의 쿼리는 User를 조회하고있다.

총 댓글의 갯수가 3개이기 때문에 3+1번의 쿼리가 실행됐다.

 

 

해결방법

fetch join

jpa 쿼리문에 fetch join을 추가하는 것이다.

 

  @Query("SELECT c FROM ReviewComment c join fetch c.user where c.review.id = :id")
  List<ReviewComment> findReviewCommentsByReviewId(@Param("id") Long reviewId);

 

 

실행하면 한번의 쿼리만 실행되는것을 확인할 수 있다.

 

 

 

 

 

@EntityGraph

이전에 N+1문제를 만났을 때도 주로 사용해왔던 방법이다.

@EntityGraph를 사용해 특정 엔티티를 EAGER로 조회해준다.

 

 

  @EntityGraph(attributePaths = "user")
  List<ReviewComment> findReviewCommentsByReviewId(@Param("id") Long reviewId);

 

마찬가지로 실행했을 경우 1개의 쿼리문이 실행된다.

 

 

fetch join과 @EntityGraph의 차이점

두 방식에는 큰 차이점이 있다.

fetch join은 inner join이고, @EntityGraph는 outer join이라는 점이다.

그리고 두가지 모두 카데시안 곱이 발생한다. 즉, N번만큼 중복발생하는 것이다.

 

해결하기 위해서 2가지의 방법이 있었다.

- set으로 받아오기
- distinct로 중복을 제거하기

 

사용이 간편한 distinct를 추가했다.

 

  @Query("SELECT DISTINCT c FROM ReviewComment c join fetch c.user where c.review.id = :id")
  List<ReviewComment> findReviewCommentsByReviewId(@Param("id") Long reviewId);

 

 

 

 

-

@EntityGraph가 사용이 간편해서 주로 사용했었는데 참고한 글에서 

해당 에너테이션이 outer join를 유발하기 때문에 좋지 않다는 댓글이 있었다.

사실 jpa에 대해서 깊이 공부해보지 않아서 왜 안좋다는지 잘 모르겠다..

 

그렇다고 무조건 fetch join을 사용하는게 정답은 아니고 

pagination 작업하다보면 또 문제가 발생한다는데 이부분도 너무 어려워서 이해가 잘 안됐다.

 

N+1에 대해서 좀더 알게된 것 같은데

알게된 만큼 새롭게 공부해야할 내용도 많이 생겼다.

다음에 다시한번 이 부분들에 대해서 알아봐야겠다.

 

reference

https://jojoldu.tistory.com/165

 

JPA N+1 문제 및 해결방안

안녕하세요? 이번 시간엔 JPA의 N+1 문제에 대해 이야기 해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용을 정리하는 Github와 세미나+

jojoldu.tistory.com

 

https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85

 

JPA 모든 N+1 발생 케이스과 해결책

N+1이 발생하는 모든 케이스 (즉시로딩, 지연로딩)에서의 해결책과 그 해결책에서의 문제를 해결하는 방법에 대해 이야기 하려합니다 😀

velog.io