[Project] N+1 문제 해결

조수훈·2023년 10월 22일
0

Project

목록 보기
5/8

본 포스팅은 스스로 공부한 내용을 정리하고 기록하기 위하여 올리는 내용이며, 잘못된 내용이 있을 수도 있음을 미리 밝힙니다. 잘못된 내용이 있거나, 더 좋은 방법이 있다면 댓글로 남겨주시기 바랍니다.


N+1 문제

업로드된 Post를 전체 조회하는 로직에서 N+1 문제가 생겼습니다.
Post Entity 는 Club Entity 와 연관관계가 맺어있습니다.

다음과 같이 Post에 관련된 Club 을 꺼내와 ClubImage를 GetAllPostResponse DTO 로 만들어주는 과정에서 N+1 문제가 발생했습니다.

서로다른 3개의 Post가 처음에 조회되고, 그 다음에 Post에 연관된 Club 이 조회됩니다.


이때 연관된 Club 3개가 서로 같다면, 처음에 조회할때 해당 Club 이 프록시에 저장되어 다음 두개를 조회할때도 같은 Club 을 조회하게 됩니다. 따라서, 쿼리는 한번만 더 추가해서 나가게 됩니다.


하지만 다르다면, 프록시에 계속해서 다른 Club 들을 저장해야 합니다. 결국엔 다음과 같이 3번의 쿼리가 나가게 됩니다.

FetchType.LAZY (지연로딩)

FetchType.LAZY는 JPA (Java Persistence API)에서 사용되는 매핑 어노테이션 중 하나로, 연관 관계를 지연 로딩하는 옵션입니다. 이 옵션을 설정하면 연관된 엔티티를 바로 가져오는 대신, 해당 엔티티에 대한 데이터베이스 쿼리는 연관된 엔티티가 실제로 사용될 때까지 지연됩니다.

예를 들어, Post 엔티티와 Club 엔티티가 있을 때, Post와 Club 간의 관계를 FetchType.LAZY로 설정한다면, Post를 검색할 때 Club을 가져오지 않습니다. 대신, Post 객체를 사용하는 동안 Club에 대한 데이터베이스 쿼리가 실행됩니다. 이것은 성능을 향상시킬 수 있습니다.

그러나 여기서 다음과 같이 N+1 문제가 발생하게 됩니다.

  1. Post 엔티티 목록을 가져옵니다. (쿼리 1 번)
  2. 각 Post에 대한 Club을 처음 접근할 때마다 데이터베이스에서 해당 Club에 대한 쿼리를 실행하게 됩니다. (Post 가 N 개라면, N 개의 쿼리가 더 실행)

FetchType.EAGER (즉시로딩)

즉시로딩을 할경우에도 마찬가지로 N+1 문제가 발생합니다.
JPA 에서는 내부적으로 join 문에 대한 쿼리를 만들어서 반환을 하기 때문에 즉시로딩으로는 문제가 없어보이기도 합니다. 하지만, 문제는 JPQL 입니다. 저희는 직접 JPQL 문을 짜서 전달하기도 하며, Data JPA 를 활용해서 쿼리 메소드를 사용하기도 합니다. 이럴때는 저희가 예상할수 없는 쿼리문들이 쏟아져 나올수가 있습니다.

해결법(in Spring Data JPA)

우선 FetchType.EAGER 를 설정하게 될 경우에는 예상할수 없는 쿼리문들이 쏟아져 나올수가 있습니다. 따라서 항상 관계매핑에는 FetchType.LAZY 를 적용합니다. 이후, 다음 방법을 사용하여 N+1 문제를 방지합니다.

1. Fetch Join

public interface PostRepository extends JpaRepository<Post, Long> {
    @Query("select p from Post p join fetch p.club")
    List<Post> findAll();
}

@Query 를 이용하여 JPQL 로 쿼리를 작성합니다. join 뒤에 fetch 라는 키워드 사용으로 간단하게 해결됩니다.

2. @EntityGraph

public interface PostRepository extends JpaRepository<Post, Long> {
	@Override
    @EntityGraph(attributePaths = {"club"}
    List<Post> findAll();
}

Spring Data JPA 의 쿼리 메소드를 Override한후, @EntityGraph 에 club 객체를 표기하여 추가합니다.

3. @NamedEntityGraph

@NamedEntityGraph(name = "post.findAll", attributeNodes = @NamedAttributeNode("club"))
public class Post {
    // 생략
}

public interface PostRepository extends JpaRepository<Post, Long> {
	@Query("select m from Member m")
	@EntityGraph("post.findAll")}
    List<Post> findAll();
}

Entity 클래스에 @NamedEntityGraph 를 추가하고 Repository 쿼리메소드에 @EntityGraph 의 속성으로 앞에서 정의한 이름을 넣어주면 됩니다.

위의 방법들을 적용했을시, 다음과 같이 쿼리가 한번만 나가게 되는 것을 확인하였습니다.

Reference

Spring Data Jpa 활용 해결
https://jaime-note.tistory.com/54

profile
잊지 않기 위해 기록하기

0개의 댓글