본문 바로가기
개발/Spring

N+1 문제란? feat. 해결방법 - 개념적 이야기

by meanjung 2023. 8. 6.

N+1 문제란,

연관관계가 설정된 엔티티를 조회할 경우, 조회된 데이터 개수(N)만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상을 말한다.

1+N 문제라고 하는 게 더 이해하기 쉬울 것이다.

 

@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "review")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Review {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "r_id")
    private Long reviewId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "u_id")
    private User user;

    @Column(name = "r_title")
    private String reviewTitle;

    @Column(name = "r_content")
    private String reviewContent;

}
@Getter
@Entity
@Table(name = "user")
public class User {

    @Id @GeneratedValue
    @Column(name = "u_id")
    private Long userId;

    @Column(name = "u_nickname", unique = true, length = 20)
    private String userNickname;

    @Column(name = "u_photo")
    private String userPhoto;

}

 

 

Review의 ManyToOne User에 집중한다.

만약 여러 Review를 조회하며, 리뷰마다 userId외에 userNickname이 필요하다면 review마다 userNickname을 가져오는 쿼리가 날라간다.(N번)

 

N+1 문제의 원인

JPA가 내부적으로 사용하는 JPQL은 기본적으로 글로벌 Fetch 전략을 무시하고 JPQL만 갖고 SQL을 생성하기 때문에, JPA Repository로 find 시 실행하는 첫 쿼리에서 하위 엔티티까지 한 번에 가져오지 않고, 하위 엔티티를 사용할 때 추가로 조회한다.

 

JPA는 대상이 되는 엔티티에만 신경을 써서 연관관계까지는 파악하지 못해서 발생한다고 이해할 수 있다.

 

FetchType.LAZY -> EAGER로 변경하면 된다고 생각하는가?

즉시로딩을 사용하면 1+N 쿼리가 한 번에 날라가는 것이고,

지연로딩을 사용하면 Review의 userId에 해당하는 userNickname을 가져올 때 쿼리가 계속 날라가게 된다.

결국 즉시로딩이든 지연로딩이든 1+N개의 쿼리는 계속 날라갈 수밖에 없다.

Fetch 전략이 EAGER인 경우 flow 

  1. findAll()을 한 순간, select r from review r 라는 JPQL 구문이 생성되고,
  2. 해당 구문을 분석한 select * from review 라는 SQL이 생성되어 실행된다.
  3. DB의 결과를 받아 review 엔티티의 인스턴스들을 생성한다.
  4. review와 연관되어 있는 user 도 로딩을 해야 한다.
  5. 영속성 컨텍스트에서 연관된 user가 있는지 확인한다.
  6. 영속성 컨텍스트에 없다면 3에서 만들어진 review 인스턴스들 개수에 맞게 select * from user where r_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )

Fetch 전략이 LAZY인 경우 flow

  1. findAll()을 한 순간, select r from review r 라는 JPQL 구문이 생성되고,
  2. 해당 구문을 분석한 select * from review 라는 SQL이 생성되어 실행된다.
  3. DB의 결과를 받아 review 엔티티의 인스턴스들을 생성한다.
  4. 코드 중에서 review 의 user 객체를 사용하려고 하는 시점에 영속성 컨텍스트에서 연관된 user가 있는지 확인한다.
  5. 영속성 컨텍스트에 없다면 3에서 만들어진 review 인스턴스들 개수에 맞게 select * from user where r_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )

 

N+1 문제 해결방법

1. Fetch Join

JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다.

public interface ReviewRepository extends JpaRepository<Review, Long> {
    @Query("select r from Review r join fetch r.user")
    List<Review> findAllFetchJoin();
}

 

2. @EntityGraph 사용하기

보통 사용하지 않는다고 한다.

 

3. Batch Size 사용하기

 


https://dev-coco.tistory.com/165

 

[JPA] N+1 문제 원인 및 해결방법 알아보기

JPA를 사용하면 자주 만나게 되는 것이 N + 1 문제이다. N + 1 문제는 성능에 큰 영향을 줄 수 있기 때문에 N + 1 문제가 무엇이고 어떤 상황에 발생되는지, 어떻게 해결하면 되는지에 대해 알아보고

dev-coco.tistory.com

https://programmer93.tistory.com/83

 

JPA N+1 문제 해결 방법 및 실무 적용 팁 - 삽질중인 개발자

- JPA N+1 문제 및 해결 방법 - JPA를 사용하다 보면 의도하지 않았지만 여러 번의 select 문이 순식간에 여러 개가 나가는 현상을 본 적이 있을 것이다. 이러한 현상을 N+1문제라고 부른다. 해당 포스

programmer93.tistory.com

 

댓글