JPA - N+1 문제, Fetch join, @EntityGraph

·2024년 5월 6일
0

Spring/JPA

목록 보기
12/15

인프런 김영한 강사님 강의를 듣고 정리한 내용입니다.

N+1 문제

엔티티 연관관계에 따라서 N+1 문제가 발생할 수 있다.

하나의 팀에 여러 멤버를 둘 수 있다고 가정하자.
Member 엔티티와 Team 엔티티가 매핑될 때, Member 기준에서 N:1 관계로 매핑되고, Team 기준에서는 1:N 관계로 매핑된다.
어떤 엔티티와 연관관계에 있는 다른 엔티티는 필드로 저장된다.

엔티티로부터 데이터를 조회할 때 연관된 다른 엔티티가 join 되어 한꺼번에 조회될 수 있다.
Member 객체의 데이터를 조회하고자 할 때, Member에 관한 데이터만 확인하고 싶지만 연관관계 필드인 Team 까지 함께 조회된다.

주의해야할 점은 조회의 주체가 되는 엔티티만 영속성 컨텍스트의 1차 캐시에 저장된다는 것이다. 그래서 매번 Member를 조회할 때마다 Member에 관한 데이터는 1차 캐시를 확인하면 되지만, Team에 대한 조회 쿼리는 무조건 수행되게 된다.

Member에 대한 조회 쿼리 1개를 할 때, 불필요한 Team에 관한 조회 쿼리가 N개 추가되어 수행되는 것이다. 이와 같은 문제를 N+1 문제라고 한다.

XtoOne fetch type 변경

조회할 필요가 없는 연관관계 필드를 조회하지 않도록 하려면, fetch type을 변경하면 된다.

1:N 관계일 때는 fetch type이 default로 Lazy로 설정되어 있다. fetch type이 Lazy라면, 1쪽의 엔티티를 조회할 때 N쪽의 엔티티를 조회하지 않는다. Lazy 로딩은 지연로딩이라고도 한다.

하지만 X:1 관계일 때는 fetch type이 default로 Eager로 설정되어 있다. fetch type이 Eager라면, 연관관계 필드를 무조건 조회하게 된다. 그래서 N+1 문제가 발생할 우려가 있다. 그래서 항상 XtoOne 관계에서 fetch type을 Lazy로 설정해줘야 한다. Eager 로딩은 즉시로딩이라고도 한다.

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

Member 엔티티에서 Team에 관한 fetch 타입을 다음과 같이 설정해서 N+1 문제를 막았다.

Lazy Loading이 이루어지는 시점은 연관된 엔티티의 메서드를 호출할 때이다. 그 전까지는 프록시 객체로 아무런 값이 들어있지 않고, 메서드가 호출되고 나서야 DB에서 값을 읽기 위해 쿼리가 수행된다.

Fetch Join

연관된 엔티티를 함께 조회할 때

fetch type을 Lazy로 변경했더라도, 연관관계에 있는 엔티티를 반드시 조회해야한다면 어떨까?

이때는 N+1 문제가 발생할 수 밖에 없다. 앞서 말했듯 join을 하게 되면 조회의 주체가 되는 엔티티는 영속성 컨텍스트의 1차 캐시에 저장되지만 연관관계에 있는 엔티티의 데이터는 저장되지 않는다.

연관관계 엔티티의 실제 조회 시점에서는 N+1 문제가 발생하게 되는 것이다.

이를 해결하기 위해서 fetch join을 사용할 수 있다.
fetch join을 하게 되면, Member 엔티티를 조회할 때 Team 엔티티를 join하여 함께 조회하게 된다. 그리고 Team의 데이터도 영속성 컨텍스트의 1차 캐시에 저장된다.

즉, fetch join을 하면 연관관계 엔티티도 영속성 컨텍스트의 1차 캐시에 저장되어 여러번 쿼리를 할 필요가 없고 N+1 문제가 발생하지 않게되는 것이다.

이 연관된 엔티티를 객체 그래프라고도 한다.

추가적으로 fetch join은 JPQL에서만 사용되는 문법이며, 연관관계에 있는 엔티티에만 적용이 가능하다.

@EntityGraph

위와 같이 연관관계에 있는 엔티티를 한꺼번에 조회할 필요가 있다고 가정하자.

스프링 데이터 JPA에서 jpql을 짤 때 fetch join을 위한 jpql을 따로 짤 필요없이 @EntityGraph 어노테이션을 사용할 수 있다.

@EntityGraph 어노테이션을 메서드 위에 붙이면 내부적으로 fetch 조인을 하게 된다. jpql을 만들고 그 위에 @EntityGraph를 붙여서 fetch join을 수행하도록 할 수도 있다.

공통 메서드를 오버라이드 해서 @EntityGraph를 붙일 수도 있다.

//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)

attributePaths에 연관관계에 걸려있는 필드명을 정의해주어야 한다. 정의된 연관관계 필드에 대해서만 fetch join을 수행한다.

@EntityGraph는 fetch join의 간편 버전으로 Left Outer Join을 사용한다.

최적화 주의사항

하지만 꼭 fetch join이 필요하지 않은 경우에 이 어노테이션을 사용하는 것은 비효율적일 수 있다.
쿼리 횟수로 결정되는 네트워크 호출과, 데이터 용량으로 결정되는 네트워크 전송 사이의 Trade Off가 있기 때문이다.
쿼리 횟수를 줄일 수 있지만 불필요하게 많은 row를 조회하여 네트워크 전송량이 늘 수 있다.

최적화를 위해서 fetch join이 필요한 경우를 위한 메서드와, 그렇지 않은 경우를 위한(이 경우에는 그냥 Lazy Loading) 메서드를 분리하여 사용하는 것이 적절하다.

profile
티스토리로 블로그 이전합니다. 최신 글들은 suhsein.tistory.com 에서 확인 가능합니다.

0개의 댓글