본문 바로가기

JPA

[JPA] JPA 성능최적화 - N+1문제

JPA 프로그래밍을 할때, 성능상 가장 주의해야할점은 N+1 문제다.

N+1문제란, 연관관계에서 발생하는 문제점으로, 연관관계를 조회할경우, 해당 연관관계의 사이즈만큼 SQL쿼리를 만들어 날리는 것을 말한다.

즉시로딩

아래 코드를 보자.

(동작 여부는 상관없이 플로우를 보도록하자)

@Entity
class TestEntity{

    @Id
    @GeneratedValue
    @Column(name = "ID")
    private id;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "testEntity")
    private List<Test2> lists = new List<Test2>();

}

public class EntityTest{
    /.../

    @Test
    public void N+1테스트(){
        em.find(TestEntity.class, id);
        em.createQuery("select t from TestEntity as t", TestEntity.class).getResultSet();
    }

}

 

TestEntity의 lists필드값을 즉시로딩(FetchType.EAGER)로 설정했기 때문에, TestEntity를 조회하는순간, 연관되어있는 Test2리스트도 한번에 조회된다.

 

em.find()로 데이터를 조회할경우 SQL은 JOIN문을 사용해 연관정보를 조회하지만, JPQL을 사용할경우 생성되는 SQL은 다음과 같다.

 

1. JPQL은 즉시로딩과 지연로딩에 대해 신경쓰지않고, JPQL만 사용해서 SQL을 생성한다.

select * from TestEntity;

 

2. 필드값의 Test2가 즉시로딩으로 설정되어 있으므로, JPQL은 다음 SQL들을 추가로 실행한다.

- select * from Test2 Where Test2ID = 1

select * from Test2 Where Test2ID = 2

select * from Test2 Where Test2ID = 3

select * from Test2 Where Test2ID = 4

select * from Test2 Where Test2ID = 5

select * from Test2 Where Test2ID = 6

.

.

.

 

이 처럼 연관관계를 즉시로딩으로 설정했을경우, JPQL은 N+1문제가 발생된다.

지연로딩

연관관계를 지연로딩으로 설정할경우, JPQL에서 N+1문제를 피할수있지만, 이후 비즈니스 로직에서 N+1문제가 발생할 수 있다.

public class EntityTest{
    
    @Test
    public void 엔티티_테스트(){
        List<TestEntity> lists = em.findAll();
        for(TestEntity testEntity : lists) testEntity.getLists().size();
    }
    
}

위 코드 또한, 당연히 TestEntity의 갯수만큼 SQL이 생성된다.

해결법

즉시로딩에서 발생하는 N+1문제는 예측하기 힘들고, 실수할 확률이 비교적 높다. 따라서, 되도록 FetchType은 지연로딩으로 하고, 최적화가 필요할때만, 즉시로딩을 하는것이 좋다.

 

1. JPQL join fetch

JPQL쿼리를 작성시, join fetch를 하면, 해당 엔티티를 함께 로딩한다. 

 

사용법)

em.createQuery("select a from A as a join fetch a.fields", ...)

 

2. 하이버네이트 @BatchSize

하이버네이트의 org.hibernate.annotations.BatchSize어노테이션을 지정한 size만큼 SQL의 IN절을 사용해서 조회한다.

 

사용법)

@Entity
class TestEntity{

    @Id
    @GeneratedValue
    @Column(name = "ID")
    private id;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "testEntity")
    @BatchSize(size = 10)
    private List<Test2> lists = new List<Test2>();

}

@BatchSize(size = 10)어노테이션으로 인해 lists를 처음조회할때, 미리 10개의 연관 데이터  SQL in절을 사용해 조회하고, 11번째 사용시, 다시 10개를 미리 조회해놓는다.

만약, 지연로딩이 아닌 즉시로딩이라면, 한번 조회때 10개씩, 모든 데이터를 조회할때까지 쿼리를 날린다.