N+1 문제 → ‘N+1은 쿼리가 몇 번 나가는지’의 문제, 1번 나갈거 여러번 나간다.

Ollsy 프로젝트의 성능을 개선하기 위해 쿼리 발생 개수를 개선해 보겠다. + JPA 연관관계 복습

확실한 비교를 위해 Item의 데이터를 10만개 만들었다.

public class Item extends DateEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "item_id")
    Long id;

    @Column(nullable = false)
    String name;

    @Column(nullable = false)
    String description;

    @Column(nullable = false)
    int price;

    @Column(nullable = false)
    int stock;

    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;

    @OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ItemImage> images = new ArrayList<>();

Itme 클래스는 ItemImage, Category클래스와 연관관계를 맺고 있음

Category클래스와는 연관관계의 주인을 맡고 있고 ItemImage와는 양방향 연관관계를 맺고 있다.

    @Test
    @DisplayName("N+1 발생 테스트")
    void nPlusOneTest() {
        System.out.println("----------- Item 전체 조회 -----------");
        List<Item> items = itemRepository.findAll();
        System.out.println("----------- Item 전체 조회 완료[쿼리 1개 발생하남??] -----------");
    }

이렇게 테스트 코드를 작성해 보았다. Item을 가져오는 것이었고 당연히 Item만 가져오니 쿼리는 select쿼리 하나만 가져오겠지?

결과

----------- Item 전체 조회 -----------
[Hibernate] 
    /* <criteria> */ select
        i1_0.item_id,
        i1_0.category_id,
        i1_0.create_at,
        i1_0.description,
        i1_0.name,
        i1_0.price,
        i1_0.stock,
        i1_0.update_at 
    from
        items i1_0
[Hibernate] 
    select
        c1_0.category_id,
        c1_0.depth,
        c1_0.name,
        c1_0.parent_id 
    from
        categories c1_0 
    where
        c1_0.category_id=?
[Hibernate] 
    select
        c1_0.category_id,
        c1_0.depth,
        c1_0.name,
        c1_0.parent_id 
    from
        categories c1_0 
    where
        c1_0.category_id=?
[Hibernate] 
    select
        c1_0.category_id,
        c1_0.depth,
        c1_0.name,
        c1_0.parent_id 
    from
        categories c1_0 
    where
        c1_0.category_id=?
[Hibernate] 
    select
        c1_0.category_id,
        c1_0.depth,
        c1_0.name,
        c1_0.parent_id 
    from
        categories c1_0 
    where
        c1_0.category_id=?
----------- Item 전체 조회 완료[쿼리 1개 발생하남??] -----------

5개의 select 쿼리가 발생했다. 왜? → 기본적으로 ManyToOne의 관계는 Eager조회를 기본으로 하고 있기 때문이다. Eager조회는 해당 엔티티를 조회할 때 엔티티가 연관관계 주인으로 있는 엔티티를 전체 조회하기 때문이다. 즉 item을 조회할 때 마다 Category를 전부 조회하기 때문이다. N+1 발생!!

방법: Fetch전략 lazy로 변경

코드의 Fetch전략을 lazy로 변경해 주었다.

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;
----------- Item 전체 조회 -----------
[Hibernate] 
    /* <criteria> */ select
        i1_0.item_id,
        i1_0.category_id,
        i1_0.create_at,
        i1_0.description,
        i1_0.name,
        i1_0.price,
        i1_0.stock,
        i1_0.update_at 
    from
        items i1_0
----------- Item 전체 조회 완료[쿼리 1개 발생하남??] -----------

보시는 것과 같이 item을 가져오는 쿼리 하나만 발생했다. → Lazy는 연관관계에 있는 엔티티를 직접 조회하는 것이 아니면 별도의 쿼리가 발생하지 않는다. 즉 연관된 엔티티(Category)를 필요할 때 까지 쿼리 발생 시점을 늦추는 것이지 N+1이 발생하지 않는 것이 아니다.

Fetch전략 Lazy일 때 N+1발생 테스트