[백엔드 로드맵 - DB] N+1 Problem

Sierra·2022년 8월 10일
0

Backend-Roadmap

목록 보기
32/43
post-thumbnail

Intro

ORM 등을 활용하다보면 N+1 문제에 대한 이야기를 반드시 듣게 된다.
이번 포스팅의 주제는 N+1. 서버 프로그래밍을 하는 도중 반드시 겪게 될 문제이므로 해당 지식에 대해 숙지해야 한다.

N+1 문제란?

ORM을 사용하다보면 발생할 수 있는 문제다. 엔티티를 조회 시 쿼리 내부에 존재하는 다른 연관관계에 접근할 때 또 다시 쿼리가 발생하는 상황을 의미한다.

쿼리 한 번으로 N개의 데이터를 가져왔을 때 연관관계에 대한 데이터를 얻기 위해 N건의 데이터를 데이터 수 만큼 반복해서 2차적으로 쿼리를 수행하는 문제이다.

게시판으로 예를 들어보면, 게시판 N건의 목록을 얻기위한 쿼리가 1회 진행되고, 각 게시판의 글을 쓴 사용자 이름, 댓글 같은 데이터를 가져오는 쿼리가 N번 진행 될 것이다. 이런 로직들은 N + 1 문제를 일으킬 수 있다.

$books = query_rows("SELECT * FROM books");
foreach( $books as &$book ) {
    $book['author_name'] = query_one("
        SELECT name 
        FROM authors 
        WHERE id=?", $book['author_id']); 
}

위의 php코드를 살펴보면 book N건을 얻기위한 쿼리 1회, 그리고 author_name을 얻기 위한 쿼리 N회가 시행된다.
물론 이런 직관적인 쿼리는 쿼리를 사람이 이해하는데 도움이 되지만, 성능 측면에서 매우 불리해진다.
데이터 양이 많다면 이러한 문제가 훨씬 크게 다가 올 것이고 최악의 경우에는 서비스를 릴리즈 할 수 없는 상황까지 올 수 있다.

해결 방안

JPA 상에서는 Fetch 모드를 어떻게 두든 간에 N+1 문제가 발생한다. 그러므로 Fetch Join, EntityGraph 어노테이션 등의 방법을 활용한다.

Fetch Join은 SQL 의 JOIN과 같이 처음부터 연관 된 데이터까지 가져오도록 처리하는 것이다. 이 경우 별도의 메소드를 만들어줘야 한다.

public interface TeamRepository extends JpaRepository<Team, Long>{
	@Query("select t from Team t join fetch t.users")
    List<Team> findAllFetchJoin();
}

JPA 뿐만 아니라 다른 ORM에서도 JOIN 쿼리를 통해 해당 문제를 해결 할 수 있다.

EntityGraph 어노테이션은 Fetch type이 Lazy인 엔티티를 한번에 조인해서 가져오고싶을 때 사용한다.

public interface UserRepository extends JpaRepository<User, Long>{
	@EntityGraph(attributePaths={"address"}, type = EntityGraph.EntityGraphType.LOAD)
    Optional<User> findWithAddressById(Long userID);
}

Outro

백엔드 개발자라면, SQL 또한 잘 알아야 한다는 이유가 바로 이런 것 아닐까 싶다. 다음 포스팅은 정규화에 대해 써 보도록 하겠다.

Reference

https://zetawiki.com/wiki/N%2B1_%EC%BF%BC%EB%A6%AC_%EB%AC%B8%EC%A0%9C
https://www.brentozar.com/archive/2018/07/common-entity-framework-problems-n-1/
https://programmer93.tistory.com/83

profile
블로그 이전합니다 : https://swj-techblog.vercel.app/

0개의 댓글