[JPA] 연관관계 조회 방식별(Fetch, Lazy) 성능 차이 테스트

uijin kim·2023년 3월 12일
0

개요

N+1 문제와 해결 방법
JPA를 이용해 별도의 옵션 없이 엔티티를 조회할 경우, 엔티티에 연결되어 있는 연관관계들이 함께 조회된다.
함께 조회되는 시점에 부모 엔티티를 조회 하는 쿼리(1) + 연결되어 있는 연관관계 엔티티들에 대한 조회 쿼리(N)가 DB 요청으로 날아가게 된다.
예를들어 4개의 연관관계를 가지고 있는 엔티티 100개를 조회할 경우 100 + 400, 총 500회의 조회 쿼리가 날아가게 됨으로 성능에 큰 영향을 미치게된다.

이런 문제는 완벽하지는 않으나 Fetch Join, Entity Graph, Batch_Size를 통해 해결 할 수 있다.
보통 연관관계 엔티티들에 대해 FetchType을 통해 연관관계 엔티티를 즉시 조회(Eager)할지, 지연 조회(Lazy)할 지 정할 수 있다.

여기서 한가지 의문점이 생겼다.
Fetch Join은 부모 엔티티 조회 시점에 연관관계의 엔티티들을 함께 조회 하는 방식이며, 2개 이상의 1:N관계에 대해서는 Fetch Join을 사용할 수 없기에 일반적으로 Fetch Join + Batch Size 설정을 통해 사용하게 된다.

과연 Lazy Loading을 통해 부모 엔티티 조회 후 연관관계를 조회하는 것과
Fetch Join + Batch Size를 통한 조회의 성능 차이가 얼마나 클지 의문이 들어 직접 테스트 해보게 되었다.

테스트 관련 코드

  • 서버와 DB는 모두 로컬환경에서 진행
    [서버, DB가 로컬이 아닌 인터넷상 라우팅을 통해 통신해야 하는 원격지에 위치해 있다면 결과의 차이가 더 클 것으로 예상]
    싱글 스레드 환경에서 진행 되었으며, 서버-DB통신 간에 데이터의 크기는 굉장히 작기 때문에 다중 요청의 멀티스레드 환경, 규모가 큰 데이터 환경에서는 테스트의 결과가 달라질 수 있다고 생각 된다.

  • 테스트 환경
    Language - Java 11
    FrameWork - Spring Boot 2.7.9
    ORM - Spring Data JPA + QueryDsl
    DB - MySql

Controller

public void test() {
        var testCount = 1000;
        long start = System.nanoTime();
        for(int i=0 ; i<testCount; i++) {
            testService.test();
        }
        long end = System.nanoTime();

        var result = ((double)end-(double)start)/1000000000;
        System.out.println();
    }

Service

private final OwnerRepository ownerRepository;

    public void test(){
        var result = ownerRepository.findAllt();

        for(Owner owner: result) {
            var test1 = owner.getDogList();
            System.out.println(test1.get(0));
            var test2 = owner.getDuckList();
            System.out.println(test2.get(0));
            var test3 = owner.getCatList();
            System.out.println(test3.get(0));
        }

        System.out.println();
    }

Entity

Lazy Loading 테스트 조건

@Entity
@Getter
public class Owner {
    @Id
    long id;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "owner")
    private List<Cat> catList;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "owner")
    private List<Dog> dogList;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "owner")
    private List<Duck> duckList;
}

Fetch Join + Batch Size 테스트 조건

@Entity
@Getter
public class Owner {
    @Id
    long id;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "owner")
    private List<Cat> catList;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "owner")
    @BatchSize(size=100)
    private List<Dog> dogList;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "owner")
    @BatchSize(size=100)
    private List<Duck> duckList;
}

연관관계 엔티티

@Entity
public class Cat {
    @Id
    private int id;

    @ManyToOne
    @JoinColumn(name = "owner_id")
    private Owner owner;
}

@Entity
public class Dog {
    @Id
    private int id;

    @ManyToOne
    @JoinColumn(name = "owner_id")
    private Owner owner;
}

@Entity
public class Duck {
    @Id
    private int id;

    @ManyToOne
    @JoinColumn(name = "owner_id")
    private Owner owner;
}

Repository(QueryDsl)

// Lazy 조건
@Override
    public List<Owner> findAllt() {
        return memberJpaQueryFactory.selectFrom(owner)
                .from(owner)
                .leftJoin(owner.catList, cat)
                .leftJoin(owner.dogList, dog)
                .leftJoin(owner.duckList, duck)
                .fetch();
    }
    
// Fetch, Batch_Size 조건
@Override
    public List<Owner> findAllt() {
        return memberJpaQueryFactory.selectFrom(owner)
                .from(owner)
                .leftJoin(owner.catList, cat).fetchJoin()
                .leftJoin(owner.dogList, dog)
                .leftJoin(owner.duckList, duck)
                .fetch();
    }

DB Data

insert into owner(id) values(1);
insert into owner(id) values(2);
insert into owner(id) values(3);
insert into owner(id) values(4);
insert into owner(id) values(5);

insert into dog(id, owner_id) values (1, 1);
insert into dog(id, owner_id) values (2, 2);
insert into dog(id, owner_id) values (3, 3);
insert into dog(id, owner_id) values (4, 4);
insert into dog(id, owner_id) values (5, 5);
insert into dog(id, owner_id) values (6, 1);
insert into dog(id, owner_id) values (7, 2);
insert into dog(id, owner_id) values (8, 3);

insert into cat(id, owner_id) values (1, 1);
insert into cat(id, owner_id) values (2, 2);
insert into cat(id, owner_id) values (3, 3);
insert into cat(id, owner_id) values (4, 4);
insert into cat(id, owner_id) values (5, 5);
insert into cat(id, owner_id) values (6, 1);
insert into cat(id, owner_id) values (7, 2);
insert into cat(id, owner_id) values (8, 3);

insert into duck(id, owner_id) values (1, 1);
insert into duck(id, owner_id) values (2, 2);
insert into duck(id, owner_id) values (3, 3);
insert into duck(id, owner_id) values (4, 4);
insert into duck(id, owner_id) values (5, 5);
insert into duck(id, owner_id) values (6, 1);
insert into duck(id, owner_id) values (7, 2);
insert into duck(id, owner_id) values (8, 3);

결과

단위(sec)

Fetch Join + Batch Size

1 - 36.95

2 - 39.22

3- 37.7

Lazy 이후 조회

1 - 66.56

2 - 63.13

3 - 68.3

4 - 64.19

원격지의 DB와의 통신이 아닌 로컬에서만 진행했음에도 굉장히 큰 속도 차이를 확인할 수 있다.

profile
느리더라도, 꾸준하게

0개의 댓글