fetch join을 통한 성능 향상

Choco·2023년 12월 16일
post-thumbnail

예시 데이터

현재 프로젝트에는 사람과 부두에 대한 테이블이 있다. 각 사람에게 출입가능한 부두를 나타내기 위해 Person과 Wharf는 다대다 관계이고 이를 연결 하기 위해 PersonWharf 테이블을 구축한 상태이다.
※ 필요한 필드값만 표시

@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long personId;
    
    private String name;

    @OneToMany(mappedBy = "person", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Permission> permissionList = new ArrayList<>();

}

@Entity
public class Wharf {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long wharfId;

    private String name;

    @OneToMany(mappedBy = "wharf", cascade = CascadeType.ALL)
    private List<Permission> permissionList = new ArrayList<>();

}
@Entity
public class Permission {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long permissionId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "personId")
    private Person person;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "wharfId")
    private Wharf wharf;

}

우선 fetch = FetchType.LAZY의 의미 부터 알아보자.

지연로딩

fetch = FetchType.LAZY은 이 값을 지연로딩을 통해 가져온다는 것을 의미한다. 반대의 개념은 fetch = FetchType.Eager 즉시로딩이 있다.

지연로딩으로 설정을 하면 해당 필드값이 호출되기 전까지는 객체를 가져오지 않는다.

예를 들어 person에 값을 가져오기 위해 personRepository.findById()를 호출했다고 하자.

그러면 select 쿼리문이 나가면서 person값을 가져올 것 이다. 하지만 DB 연관이 없는 personId 값은 가져오지만 FetchType.LAZY로 설정되어있는 permissionList 가져오지 않고 우선 proxy객체로 대체하고 추후에 이 객체값이 필요할때 가져온다.

지연로딩 장점

지연로딩을 하면 필요없는 값을 가져오는것을 사전에 방지하는 효과가 있다.

우리가 데이터를 가져올때 항상 그와 연관된 데이터 모두가 필요한것은 아니다.

예를들어 person 객체를 가져올떄 그 사람이 name값만 필요할 수도 있다. 이런 상황에서 person과 연관된 모든 데이터를 한번에 가져오면 성능이 매우 떨어지게된다.

지연로딩의 단점

하지만 person에 연관된 데이터도 가져오고 싶을때 문제가 생긴다.

만약 우리가 person을 통해 person과 연관있는 wharf의 name까지 알고 싶다고 하자. 이 때 JPA의 순서를 나타내면 이렇다.

  1. person 객체를 가져올떄 select 쿼리문 한번 호출

  2. permission를 가져올때 permissionList의 길이만 큼 select 쿼리문 N번 호출
    ->지연 로딩이니 1번에선 permissionList는 proxy 객체 였다.

  3. wharf를 가져올때 permissionList의 길이만 큼 select 쿼리문 N번 호출
    ->지연 로딩이니 2번에선 wharf는 proxy 객체 였다.

이렇게 쿼리문이 많이 나가는 현상이 발생한다. 이 문제를 N+1 문제 라고 한다.
1번의 명령만 내렸는데 연쇄적으로 N번의 쿼리가 나가는 현상이다.

실행

세팅

현재 데이터는

person1 -> wharf2,wharf3
person2 -> wharf1

으로 되어있다.

모든 person을 조회하고 해당 person에 연결된 wharf에 name값을 가져오는 코드를 실행한다.

fetch join X

//모든 Person 조회
List<Person> personList = personResoitory.findAll();
//person 연결된 Permission 객체를 가져오고 Permission객체와 연관된 Wharf의 name을 가져온다.
personList.stream().map(Permission::getWharf).map(Wharf::getName).collect(Collectors.toList());

실행 쿼리

//person 조회
2023-12-16T17:32:26.788+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        w1_0.person_id,
        w1_1.birth,
        w1_1.name,
        w1_1.nationality,
        w1_1.person_id,
        w1_1.phone,
        w1_1.sex,
        w1_0.account_id,
        w1_0.face_url,
        w1_0.position 
    from
        worker w1_0 
    join
        person w1_1 
            on w1_0.person_id=w1_1.person_id

//Permission 조회
2023-12-16T17:32:26.796+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL
    select
        p1_0.person_id,
        p1_0.permission_id,
        p1_0.wharf_id 
    from
        permission p1_0 
    where
        p1_0.person_id=?

//Wharf 조회
2023-12-16T17:32:26.811+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        w1_0.wharf_id,
        w1_0.name 
    from
        wharf w1_0 
    where
        w1_0.wharf_id=?
2023-12-16T17:32:26.815+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        w1_0.wharf_id,
        w1_0.name 
    from
        wharf w1_0 
    where
        w1_0.wharf_id=?

//Permission 조회
2023-12-16T17:32:26.818+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        p1_0.person_id,
        p1_0.permission_id,
        p1_0.wharf_id 
    from
        permission p1_0 
    where
        p1_0.person_id=?
        
//Wharf 조회        
2023-12-16T17:32:26.821+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        w1_0.wharf_id,
        w1_0.name 
    from
        wharf w1_0 
    where
        w1_0.wharf_id=?
  1. select person 1번

    person 객체 2개 생성

  2. select Permission 2번

    Permission 객체 3개 생성

  3. select Wharf 3번

1+2+3 총 6번의 쿼리가 발생

fetch join O

//모든 Person 조회
 @Query("SELECT p FROM Person p JOIN FETCH p.permissionList per JOIN FETCH per.wharf")
List<Person> findAllPersonWithWharf();

//모든 person 조회
List<Person> personList = personRepository.findAllPersonWithWharf();

//위와 동일
personList.stream().map(Permission::getWharf).map(Wharf::getName).collect(Collectors.toList());
//한번의 쿼리만 실행
2023-12-16T21:30:55.176+09:00 DEBUG 20324 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        w1_0.person_id,
        w1_1.birth,
        w1_1.name,
        w1_1.nationality,
        w1_1.person_id,
        p1_0.person_id,
        p1_0.permission_id,
        w2_0.wharf_id,
        w2_0.name,
        w1_1.phone,
        w1_1.sex,
        w1_0.account_id,
        w1_0.face_url,
        w1_0.position 
    from
        worker w1_0 
    join
        person w1_1 
            on w1_0.person_id=w1_1.person_id 
    join
        permission p1_0 
            on w1_1.person_id=p1_0.person_id 
    join
        wharf w2_0 
            on w2_0.wharf_id=p1_0.wharf_id

일반 join으로 하면 안되나요?

join을 이용하여 쿼리를 작성하면 데이터는 가져오지만 영속성 컨텍스트에 들어가진 않는다.

profile
주니어 백엔드 개발자 입니다:)

0개의 댓글