PPAK

[JPA] Join 과 Fetch Join (N+1 문제) 본문

spring/jpa

[JPA] Join 과 Fetch Join (N+1 문제)

PPakSang 2022. 8. 4. 14:02

 

SQL 에서 서로 다른 테이블을 연관지어 불러오기 위해 사용되는 inner join 과 outer join 외에 JPA 의 JPQL 에서는 fetch join 을 제공합니다.

 

이전 포스팅 에서 확인했듯 JPA 는 영속화를 바탕으로 데이터베이스 테이블과 직접적으로 연결되는 엔티티 객체를 추적하고, 관리하는 방식을 채택하여 불필요한 쿼리 생성을 최소화 합니다.

 

대표적으로 Transaction 내의 쓰기 지연 방식과 지연 로딩을 예로 들 수 있는데 오늘은 지연 로딩 사용시 발생할 수 있는 N+1 문제와 이를 해결할 수 있는 방법인 Fetch Join(join fetch) 에 대해서 알아보려고 합니다.

 

지연 로딩(Lazy Loading) 은 실제 연관관계에 있는 엔티티들 중 하나를 조회하려고 했을 때, 데이터베이스에서 모든 연관관계에 있는 엔티티 데이터들을 조회하는 것이 아닌 실제로 필요한 때에 초기화 하여 사용하는 방식을 의미합니다.

 

쉽게 말해 내가 선택한 엔티티의 데이터만 불러오고, 연관된 엔티티는 또 그 엔티티의 데이터가 필요할 때 추가적으로 데이터베이스에서 꺼내오는 아주 합리적인 방식을 의미한다고 볼 수 있습니다.

 

N+1 문제

단순 테이블 조회 쿼리를 하나 생성하였는데, 연관관계에 있는 데이터를 모두 불러오기 위해서 select 쿼리가 테이블의 레코드 수(혹은 호출한 데이터 수) 만큼 더 생성되는 문제를 의미합니다.

 

즉시로딩은 최초 테이블 데이터를 모두 불러온 후 바로 연관관계가 있는 엔티티를 조회하는 방식을

지연로딩은 최초 테이블 데이터를 모두 불러온 후 필요한 시점에 연관관계가 있는 엔티티를 조회하는 방식을 채택합니다.

 

즉시로딩은 워딩 그대로 조회 시점에 연관관계에 있는 엔티티가 모두 조회되어 N+1 문제가 생긴다는 것을 직관적으로 알 수 있지만 지연로딩은 얼핏보면 로딩 시점에 쿼리가 더 나가지 않아 N+1 문제가 발생하지 않을 것으로 보입니다. 하지만 지연로딩 N+1 문제를 피해갈 수는 없는데, 그 이유는 연관관계에 있는 엔티티가 조회 시점에 영속화되지 않았기 때문입니다.

 

일반적으로 JPA는 JPQL 로 생성된 쿼리문을 데이터베이스와 connection 을 만든 후에 전송하고, 이에 대한 결과 데이터를 전달받아 실제 엔티티 객체를 생성하고 이를 영속화 합니다.

 

하지만 단순한 join 쿼리를 작성한다면 연관관계에 있는 데이터의 영속화는 수행되지 않습니다.

// SomeEntity 와 연관관계에 있는 OtherEntity 를 호출하는 쿼리
@Query("select t From SomeEntity t join t.otherEntity")

가령 위와 같이 연관관계에 있는 데이터를 조회하기 위해 JPQL 로 join 문을 작성한다면, OtherEntity 와 연관관계를 가지는 SomeEntity 데이터를 불러올지는 몰라도 OtherEntity 의 데이터는 얻을 수 없습니다.

 

따라서 SomeEntity 를 통해 OtherEntity 를 조회하려고 한다면 추가적인 쿼리가 생성될 것이고, 이는 N+1 문제를 발생시킵니다.

 

@Entity
@Getter @Setter
public class SomeEntity {

    @Id @GeneratedValue
    private Long id;

    private String someField;

    @ManyToOne(fetch = FetchType.LAZY)
    private OtherEntity otherEntity;
}
@Entity
@Getter @Setter
public class OtherEntity {

    @Id @GeneratedValue
    private Long id;

    private String otherField;
}​

위와 같은 간단한 Entity 를 생성하여 확인할 수 있습니다.

 

일반 join 문을 통해서 테이블 데이터를 호출하면 위와같이 otherEntity 에 대한 데이터는 불러오지 않는다는 것을 확인할 수 있고, 실제로 참조하려고 하면

 

N+1 문제가 발생합니다.

// SomeEntity 와 연관관계에 있는 OtherEntity 를 호출하는 쿼리
@Query("select t From SomeEntity t join fetch t.otherEntity")

 

하지만 위와같이 fetch join 으로 코드를 수정하면

 

테이블 데이터를 호출할 때 연관관계에 있는 테이블 데이터까지 함께 불러오는 것을 확인할 수 있고, N+1 문제 역시 해결되는 것을 확인할 수 있습니다.

 

LazyInitializationException

N+1 문제와 마찬가지로 "일대다" 연관관계를 가지는 엔티티 객체 데이터를 테이블에서 불러올 때 "다" 에 해당하는 테이블 데이터가 불러와지지 않아 해당 데이터를 조회하려고 할 때 발생하는 예외를 의미합니다.

 

엄밀히 이야기하면 테이블에서 연관관계에 있는 테이블의 pk 를 가지고 있지도 않아 기존 N+1 에서 본 추가 쿼리 생성도 하지 않고 예외를 발생시키는 것이라고 생각할 수 있습니다.

 

그렇다면 항상 fetch join 을 수행하는 것이 유리해 보이는데 join 문은 어떤 상황에서 작성할 수 있을까 생각을 할 수 있습니다.

 

ORM 을 가장 적절하게 사용하는 방법중 하나의 문맥(Transaction) 에서 필요한 데이터만을 DB 에서 꺼내와서 쓰는 것이라고 말할 수 있는데, 그 점에서 fetch join 은 문맥에서 필요하지않은 데이터까지도 불러올 수 있다는 단점 아닌 단점을 가지고 있습니다.

 

따라서 검색조건과 같은 필터링은 필요하지만 실제 연관관계에 있는 테이블 데이터가 필요하지않은 문맥에서는 join 문을 통해서 쿼리를 생성하는 것이 바람직하다고 볼 수 있습니다.

 

잘못된 정보가 있다면 댓글로 알려주시면 감사하겠습니다!!

'spring > jpa' 카테고리의 다른 글

[Spring/JPA] JPA 란? (ORM/Persistence Context)  (0) 2022.07.02
Comments