Spring Data JPA 쿼리 최적화(N+1 문제 해결)

CHOI YUN HO·2022년 9월 30일
0

SW Maestro

목록 보기
8/13

JPA를 이용하여 엔티티를 설계할 때,
연관관계가 꽤 복잡해지는 경우가 있다.

내 경우에는 다음과 같았다.

public class Lecture {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    ...
    ...
    ...

    @OneToMany(mappedBy = "lecture")
    private List<LectureTag> lectureTags = new ArrayList<>();

    @OneToMany(mappedBy = "lecture")
    private List<Chapter> chapters = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "instructor_id")
    private Instructor instructor;

위는 강의의 엔티티인데, 강의 목록을 조회할 때 instructor의 정보를 함께 조회하기 때문에
아래와 같이 쿼리가 나간다.

Hibernate: select lecture0_.id as id1_4_, lecture0_.category_id as categor17_4_, lecture0_.code as code2_4_, lecture0_.created_at as created_3_4_, lecture0_.deadline as deadline4_4_, lecture0_.description as descript5_4_, lecture0_.image_path as image_pa6_4_, lecture0_.info as info7_4_, lecture0_.info_image_path as info_ima8_4_, lecture0_.instructor_id as instruc18_4_, lecture0_.level as level9_4_, lecture0_.name as name10_4_, lecture0_.original_price as origina11_4_, lecture0_.sale_price as sale_pr12_4_, lecture0_.updated_at as updated13_4_, lecture0_.vendor_name as vendor_14_4_, lecture0_.vendor_url as vendor_15_4_, lecture0_.views as views16_4_ from lecture lecture0_ limit ?
Hibernate: select count(lecture0_.id) as col_0_0_ from lecture lecture0_
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?
Hibernate: select instructor0_.id as id1_3_0_, instructor0_.code as code2_3_0_, instructor0_.created_at as created_3_3_0_, instructor0_.info as info4_3_0_, instructor0_.name as name5_3_0_, instructor0_.updated_at as updated_6_3_0_, instructor0_.vendor_name as vendor_n7_3_0_ from instructor instructor0_ where instructor0_.id=?

현제 페이징이 적용되어 있고 페이지의 size = 24인데,
중복된 instructor을 제외하고 15개의 쿼리가 추가로 나가는 것을 확인할 수 있다.
(현재 가져오는 24개에서 instructor가 중복되기 때문에 영속성 컨텍스트덕에
15개지만, 원래는 24개의 쿼리가 추가로 나가는 것이다.)

Fetch Join

이런 경우 Fetch join을 사용하여 해결할 수 있는데,
JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 한번에 가져오게 하는 방법이다.

Repository에서 메소드를 아래와 같이 수정했다.

	@Query(value = "select l from Lecture l " +
    "left join fetch l.instructor ",
    countQuery = "select count(l.id) from Lecture l")
    Page<Lecture> findAll(Specification<Lecture> specification, Pageable pageable);

그 결과

Hibernate: select lecture0_.id as id1_4_0_, instructor1_.id as id1_3_1_, lecture0_.category_id as categor17_4_0_, lecture0_.code as code2_4_0_, lecture0_.created_at as created_3_4_0_, lecture0_.deadline as deadline4_4_0_, lecture0_.description as descript5_4_0_, lecture0_.image_path as image_pa6_4_0_, lecture0_.info as info7_4_0_, lecture0_.info_image_path as info_ima8_4_0_, lecture0_.instructor_id as instruc18_4_0_, lecture0_.level as level9_4_0_, lecture0_.name as name10_4_0_, lecture0_.original_price as origina11_4_0_, lecture0_.sale_price as sale_pr12_4_0_, lecture0_.updated_at as updated13_4_0_, lecture0_.vendor_name as vendor_14_4_0_, lecture0_.vendor_url as vendor_15_4_0_, lecture0_.views as views16_4_0_, instructor1_.code as code2_3_1_, instructor1_.created_at as created_3_3_1_, instructor1_.info as info4_3_1_, instructor1_.name as name5_3_1_, instructor1_.updated_at as updated_6_3_1_, instructor1_.vendor_name as vendor_n7_3_1_ from lecture lecture0_ left outer join instructor instructor1_ on lecture0_.instructor_id=instructor1_.id limit ?
Hibernate: select count(lecture0_.id) as col_0_0_ from lecture lecture0_

추가 쿼리 없이 성공적으로 조회가 가능했다.

하지만

fetch join도 한계가 있다.
위에 상황에서는 instructor에 대해서만 조회를 하고 있기 때문에 문제가 없지만,
위 객체에서 일대다로 묶인 다른 여러 컬랙션은 fetch join으로 한번에 끌어올 수 없다.

이에 대해서는 다음에 더 자세히 다뤄보겠다.

profile
가재같은 사람

0개의 댓글