JPA N + 1 문제에 대하여

JPA를 사용하다보면 다양한 문제를 마주치곤 하는데요.
오늘은 그 중 가장 대표적인 문제인 N + 1 문제에 대해 알아보고자 합니다.




개요

JpaRepository를 통해 엔티티 리스트를 조회하니 리스트에 존재하는 Entity의 갯수만큼 연관되어 있는 Entity에 대한 조회 쿼리가 수행되는 모습을 볼 수 있었습니다.

@Entity
@Getter
@Table(name = "products")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private long price;

    @Getter(AccessLevel.NONE)
    @Embedded
    private ProductImages productImages;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "product")
    private List<Review> reviews;

    ...
}

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Review {
    @Id
    private Long id;

    @Column(name = "user_id")
    private String reviewer;

    private String content;

    @ManyToOne
    private Product product;

    ...
}



public interface ProductRepository extends JpaRepository<Product, Long> {
}



@SpringBootTest
class ProductRepositoryTest {

    @Autowired
    ProductRepository productRepository;

    @Test
    void N_1_문제_테스트() {
        // given
        Product product_1 = Product.of("product_1", 30000, asList(ProductImage.of("imagePath", 0)));
        Product product_2 = Product.of("product_2", 20000, asList(ProductImage.of("imagePath", 0)));
        productRepository.save(product_1);
        productRepository.save(product_2);

        // when
        productRepository.findAll();
    }
}

우선 예상과는 다르게 Join 쿼리가 수행되지 않고 Product 쿼리와 Review 쿼리가 각각 따로 수행되는 모습을 볼 수 있는데,
여기서 더 이상한 점은 Product의 수만큼 Review 쿼리가 수행된 것을 볼 수 있습니다.




Why ?

왜 이러한 문제가 발생하는 것일까요 ?
우선 문제는 아래의 코드에 있습니다.

...
public class Product {
	
	...

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "product")
    private List<Review> reviews;

    ...
}

이 부분을 보시면 현재 로딩 전략이 EAGER 로딩으로 되어있는데요.

JPA의 글로벌 Fetch 전략은 단일 Entity를 조회할 경우에만 즉시 적용이 되지만,
Entity 리스트 조회시 즉시 적용이 되지 않고, Entity 리스트를 PersistContext에 올린 후
적용
됩니다.

그렇기 때문에 Entity 리스트가 PersistContext에 모두 올라간 후, EAGER fetch 전략임을 확인하여
Entity 리스트 조회후 Entity 리스트에 존재하는 Entity들의 연관된 Entity를 조회하게 됩니다.

그래서 결과적으로 N + 1 문제가 발생하게 됩니다.




Resolve

Fetch join 사용

  • JPA Query Method 사용시
  • Querydsl 사용시

Fetch join 사용시 페이징 쿼리가 수행되지 않음


@EntityGraph 어노테이션 사용


@BatchSize 어노테이션 사용

좋은 웹페이지 즐겨찾기