Trouble Shooting

태태개발일지 - JPA N+1

태태코 2024. 10. 25. 21:34
반응형

JPA N+1

간단하게 이해하기 쉽게 설명하자면, 엔티티를 조회했는데 쿼리가 한번이 아닌 + N번 더 실행하여 성능을 저하시키는 문제를 말한다.

 

상황

1.

ex) select * from MainEntity where name = '?' 
이러한 쿼리를 통해서 MainEntity를 N개 가져왔을 경우 + MainEntity와 연관관계를 맺은 Entity가 ToOne관계일때 (지연로딩이던 즉시로딩이건) 연관관계를 맺은 Entity를 가져올때 N번의 쿼리가 더 발생하게 된다.

 

코드예시

 

public class MainEntity{
	
    @ManyToOne
    private 연관Entity; 

}

 

public findMainEntity(){
	//Main Entity를 가져온다. -> query 1번
    List<MainEntity> results = findByMainEntityByWhere(String name);
    
    //for문을 돌면서 ToOne연관관계를 가진 Entity를 가져온다.
    for(MainEntity result: results ){
    	//쿼리 한번
        result.get연관Entity();
    }
    
    //즉 반복문 수 만큼 query가 나간다. 
}

 

여기서 가장 큰 문제는 ToOne관계를 가진 Entity가 여러개라면 그냥 1+N+N+N ----- 이렇게 엉청난 성능 이슈를 가져올 수 있다.

 

해결법.

 

Fetch Join으로 모두 join을 통해서 한번에 가져오게 되면, 성능이슈 없이 한번에 가져올 수 있다.

 

fetch join이 번거롭다면 

@EntityGraph 를 사용한다면 명시만해도 연관된 모든 Entity를 가져와준다.

 

 

2.  위와 같은 상황이지만 ToMany 인 연관관계가 있을 때.

 

똑같이 fetch join으로 가져오거나 @EntityGraph를 이용하여 불러오면 된다.

 

public class MainEntity{
	
    @OneToMany
    private List<연관Entity>; 

}

 

*하지만 컬렉션(즉 ToMany) 관계일 때 모두 fetch join으로 불러오면 페이징 처리가 불가능하다.

이유는?

 

query문으로 보았을 때

1대 다 연관관계를 join하게 되면 MainEntity가 한개더라도 join되는 수에따라 결과값이 늘어나게된다.

 

select * from MainEntity inner join 연관Entity on MainEntity.id = 연관Entity.id where MainEntity.id =1 ;

 

이 쿼리를 보면 MainEntity의 결과값이 하나였더라도, join되는 컬럼의 수만큼 결과값이나온다.

 

그렇다면 위와 같은 이유로 (데이터가 3개라고 가정하면) MainEntity가 3개라고 인지한 JPA는 그 결과값만큼 중복된 데이터를 생산하게 되고,

 

 

 

이를 해결하기 위한 방법은 jpql에 Distinct 를 같이 써주는것이다. 하지만 이것으로는 페이징 처리를 할 수 없다.

 

해결법.

 

ToOne관계들만 모두 Fetch Join으로 끌고오고, ToMany는 @BatchSize로 가져오게되면, IN연산자를 사용하여 한번에 끌고오게된다. 즉 ToOne을 Paging 처리하여 원하는 수만큼 가져오고, 그에 맞게 IN연산자를 통해서 ToMany엔티티를 불러오는데 그것이 @BatchSize이다.

 

 

 

 

DTO 조회방식.

 

이 방식은 Fetch Join을 사용할 수 없다.

왜? result를 DTO로 받는 방식이기 때문이다.

 

ex) 위와 동일한 방식으로 ToOne관계는 Join으로 모두 받아온다.

em.createQuery("select new DTO() from Entity where ~~")

 

이 방식으로 우선 ToOne관계를 가져오고.

아래는 수도코드이다.

var result = findDtoById();
var ids = result.stream().collect(getId~~);

em.createQuery("select new dto from ~~ where id in {ids}")
.setParameters(ids);

Map<Long,Dto> = Map(groupBy(getID));

result.stream().setMany(map);

 

아래와 같이 ToMany관계는 ids를 뽑아내서 in절로 가져온후 id와 매핑시켜줘서 기존꺼에 넣는 방식이다.

 

 

*앤타타 조회 방식은 JPA가 많은 부분을 최적화 해주기 떄문에, 단순한 코드를 유지하면서, 성능을 최적화 할 수 있다.

반면에 DTO 조회 방식은 SQL을 직접 다루는 것과 유사하기 때문에, 둘 사이에 줄타기를 해야한다.*

 

반응형