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
- findAll()을 한 순간, select r from review r 라는 JPQL 구문이 생성되고,
- 해당 구문을 분석한 select * from review 라는 SQL이 생성되어 실행된다.
- DB의 결과를 받아 review 엔티티의 인스턴스들을 생성한다.
- review와 연관되어 있는 user 도 로딩을 해야 한다.
- 영속성 컨텍스트에서 연관된 user가 있는지 확인한다.
- 영속성 컨텍스트에 없다면 3에서 만들어진 review 인스턴스들 개수에 맞게 select * from user where r_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )
Fetch 전략이 LAZY인 경우 flow
- findAll()을 한 순간, select r from review r 라는 JPQL 구문이 생성되고,
- 해당 구문을 분석한 select * from review 라는 SQL이 생성되어 실행된다.
- DB의 결과를 받아 review 엔티티의 인스턴스들을 생성한다.
- 코드 중에서 review 의 user 객체를 사용하려고 하는 시점에 영속성 컨텍스트에서 연관된 user가 있는지 확인한다.
- 영속성 컨텍스트에 없다면 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
https://programmer93.tistory.com/83
'개발 > Spring' 카테고리의 다른 글
JPA 프록시 개념/존재이유와 find(), getReferenceById() (0) | 2023.08.08 |
---|---|
[N+1 문제] 게시글 댓글 조회 기능 querydsl로 해결하기 (0) | 2023.08.06 |
QueryDsl을 이용한 No offset 구현하기 (0) | 2023.08.06 |
싱글톤 LazyHolder 적용해보기 (0) | 2023.08.03 |
spring-data-jpa Repository에서 Entity로 반환받는 이유 (0) | 2023.08.02 |
댓글