Welcome! Everything is fine.

[Spring] @EntityGraph로 N+1문제 해결하기 본문

TIL

[Spring] @EntityGraph로 N+1문제 해결하기

개발곰발 2025. 2. 26. 00:14
728x90

다음 getTodos() 메서드에서 발생하고 있는 N+1 문제를 @EntityGraph를 사용해 해결해야 한다.

public Page<TodoResponse> getTodos(int page, int size) {
    Pageable pageable = PageRequest.of(page - 1, size);

    Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);

    return todos.map(todo -> new TodoResponse(
            todo.getId(),
            todo.getTitle(),
            todo.getContents(),
            todo.getWeather(),
            new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
            todo.getCreatedAt(),
            todo.getModifiedAt()
    ));
}

 

위 코드에서 할일을 모두 불러온 후, 그 아래 유저를 또 다시 불러오는 과정에서 N+1 문제가 발생할 가능성이 높아보인다.

Todo 엔티티의 일부를 살펴보면 User와 @ManyToOne 다대일 연관관계를 맺고 있고, 지연 로딩(FetchType.LAZY)으로 설정되어 있다. 지연 로딩으로 설정하면 Todo를 조회할 때 User를 바로 가져오는 것이 아니라 프록시 객체로 있다가 실제 사용될 때 SELECT 쿼리가 실행되는 것이다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

 

먼저 findAllByOrderByModifiedAtDesc() 메서드를 통해 다음과 같이 모든 todo를 불러오고 있다.

Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);

 

이때 다음과 같은 쿼리가 실행될 것이다.

SELECT * FROM todo ORDER BY modified_at DESC LIMIT ?, ?

 

그리고 다음 코드에서 todo.getUser()를 하면 User에 대한 SELECT 추가 쿼리가 실행된다. 즉 todo가 100개라면 위에서 1개의 쿼리를 실행하고, 아래에서 100개의 쿼리가 또 발생하게 되는 것이다.

todos.map(todo -> new TodoResponse(
        todo.getId(),
        todo.getTitle(),
        todo.getContents(),
        todo.getWeather(),
        new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()), // N+1 문제 발생 가능
        todo.getCreatedAt(),
        todo.getModifiedAt()
));

 

Repository에서는 이미 다음과 같이 fetch join을 사용해 N+1 문제를 해결했다.

@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

 

여기서 @EntityGraph라는 어노테이션을 활용해 해결해보자. @EntityGraph(attributePaths = {"user"})와 같이 적어주면 User도 함께 조회하도록 설정할 수 있다.

@EntityGraph(attributePaths = {"user"})
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

 

+) N+1 문제 눈으로 확인하기👀

오늘 팀원분께서 N+1 문제를 해결하지 않았을 때의 결과와 해결했을 때의 결과를 직접 보여주셨다. 그래서 나도 따라서 테스트 해보니 훨씬 이해가 잘됐다. 미리 유저 2명을 생성하고, 일정도 각 유저마다 1개씩 만들어 테스트 했더니 다음과 같은 결과가 나왔다.

@EntityGraph 적용 전(왼) / @EntityGraph 적용 후(오)

 

N+1 문제를 해결하기 전에는 일정을 조회하고, 각 유저마다 한 번씩 더 조회하고 있다. 유저의 수(2명)만큼 쿼리가 실행된 것이다. N+1 문제를 해결한 후에는 유저를 추가로 조회하는 것 없이 쿼리가 한 번만 실행되는 것을 볼 수 있다.