[query Optimization] LAZY 로딩과 N+1 문제
대부분의 경우, 어떠한 엔티티를 가져올 때 모든 연관된 엔티티도 함께 가져오는 eager fetch 대신, 그때 그때 필요한 엔티티만 join해서 가져오는 Lazy Fetch를 사용한다.
하지만 이러한 방법에는 N+1 문제가 발생하게 된다.
[N+1 문제]
N+1 문제는 데이터베이스에서 데이터를 조회할 때 발생할 수 있는 성능 이슈 중 하나다. N+1 문제는 한번의 쿼리로 가져올 수 있는 데이터를 가져오기 위해 N개의 쿼리를 더 실행하는 문제를 의미한다.
예를 들어, 게시물과 댓글을 저장하는 데이터베이스가 있다고 가정해보자. 각 게시물은 여러 개의 댓글을 가질 수 있다. 이때, 게시물과 그에 대한 댓글 정보를 모두 가져와야 한다고 가정하면, 다음과 같이 쿼리를 작성할 수 있다.
SELECT *
FROM post
이 쿼리는 모든 게시물을 가져오지만, 이어서 각 게시물에 대한 댓글 정보를 가져오기 위해 다음과 같은 쿼리를 작성해야 한다.
SELECT *
FROM comment
WHERE post_id = ?
이렇게 각 게시물마다 댓글 정보를 가져오기 위해 총 N개의 쿼리가 실행된다.
이 때문에, 데이터베이스에 부하가 걸리고 조회 시간이 길어지는 문제가 발생한다.
N+1 문제는 데이터베이스에서 많은 양의 데이터를 가져올 때 흔히 발생하는 문제이며, 이를 해결하기 위해서는 쿼리 최적화, 캐싱, 미리 연관된 데이터를 가져오는 등의 방법이 있다.
또한, ORM 프레임워크에서 제공하는 지연 로딩, 명시적인 데이터 로딩 방법 등을 사용하여 N+1 문제를 방지할 수도 있다.
나는 Spring 프레임워크를 사용하기에, Spring 의 ORM 프레임워크인 Spring JPA, Spring Data JPA에서 이러한 N+1 문제를 해결하는 법을 알아보겠다.
우선 대부분의 연관관계에서 연관된 객체를 가져오는 fetchType은 Lazy 를 기본으로 해야한다. ( 매번 다 가져오는 것은 오버헤드가 너무 크다)
하지만, 그렇기에 막상 어떤 객체와 그 연관된 몇개의 객체를 가져오는 작업이 빈번하게 일어난다면, Lazy 로딩으로 인해 매 쿼리 수행 시마다, 여러개의 쿼리가 별도로 나가게 된다.
따라서 우리는 이 부분은 Lazy로딩이 아니라 마치 Eager 로딩 처럼 처리할 수 있는데, 스프링에서는 크게 두가지를 이용 가능하다.
1) fetch join
2) @EntityGraph
간단한 기능은, @EntityGraph로 미리 명시한 엔티티들을 함께 로딩하는 것으로 처리하고, 복잡한 기능의 경우, JPAL로 풀어서 정리해야할 때가 있는데, 이때 fetch join을 사용하면 된다.
fetch join
@Query("SELECT f FROM Friend f JOIN FETCH f.fromMember fm JOIN FETCH f.toMember tm WHERE (fm = :member OR tm = :member) AND f.status = :status")
List<Friend> findFriendsByMemberAndStatus(@Param("member") ClubMember member, @Param("status") FriendStatus status);
entityGraph :
엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능
- 정적으로 정의하는 Named 엔티티 그래프
- 동적으로 정의하는 엔티티 그래프
아래와 같이, 같이 가져와야할 엔티티를 정의해놓으면, 즉시로딩처럼 동작한다.
@EntityGraph(attributePaths = {"fromMember", "toMember"})
@Query("SELECT f FROM Friend f WHERE (f.fromMember= :member OR f.toMember = :member) AND f.status = :status")
List<Friend> findFriendsByMemberAndStatus(@Param("member") ClubMember member, @Param("status") FriendStatus status);
namedEntityGraphs
@NamedEntityGraph(
name = "friend-with-members",
attributeNodes = {
@NamedAttributeNode("fromMember"),
@NamedAttributeNode("toMember")
}
)
@Entity
public class friend{
.....
}
위와 같이 엔티티에서 미리 엔티티 그래프를 정의한 뒤,
@EntityGraph(value = "friend-with-members")
@Query("SELECT f FROM Friend f WHERE (f.fromMember= :member OR f.toMember = :member) AND f.status = :status")
List<Friend> findFriendsByMemberAndStatus(@Param("member") ClubMember member, @Param("status") FriendStatus status);
필요한 곳에서 이름을 가지고 사용 가능.
다만, 위의 예시의 경우, Select f(friend) 만 선택하고, toMember, fromMember의 다른 정보들을 필요로 하지 않는다.
따라서 굳이 @EntityGraph를 사용하지는 않아도 된다.( 실제로 다른 테이블에만 있는 데이터를 가져올 경우, 쿼리가 2번 나가는 것을 방지하기 위해 쓰는 것)