N+1 문제

김상진 ·2024년 12월 7일
0

CS

목록 보기
14/30

N+1 문제와 조회수 예시

N+1 문제는 JPA를 사용할 때 발생하는 대표적인 성능 문제입니다.

가장 흔한 예시로 게시글 목록조회수 데이터를 가져올 때 발생하는 상황을 예로 들어 설명하겠습니다.


상황 예시: 게시글과 조회수

게시판 시스템을 만든다고 가정합니다.

  • Post 엔티티: 게시글 정보를 저장합니다.
  • View 엔티티: 각 게시글의 조회수를 저장합니다.

관계: PostView1대1 관계입니다.


1. 엔티티 설정

@Entity
public class Post {
    @Id
    @GeneratedValue
    private Long id;

    private String title;
    private String content;

    @OneToOne(mappedBy = "post", fetch = FetchType.LAZY)
    private View view;
}

@Entity
public class View {
    @Id
    @GeneratedValue
    private Long id;

    private int viewCount;

    @OneToOne
    @JoinColumn(name = "post_id")
    private Post post;
}

2. N+1 문제 발생 코드

게시글 목록을 가져오고 각 게시글의 조회수(View)를 함께 출력하려고 합니다.

List<Post> posts = em.createQuery("SELECT p FROM Post p", Post.class)
                     .getResultList();

for (Post post : posts) {
    System.out.println("Title: " + post.getTitle() + ", 조회수: " + post.getView().getViewCount());
}

쿼리 실행 과정

  1. 첫 번째 쿼리

    SELECT p FROM Post p

    게시글(Post) 목록을 조회합니다. (1개의 쿼리 실행)

  2. 지연 로딩(Lazy Loading)

    각 게시글의 조회수를 가져오기 위해 N번의 추가 쿼리가 실행됩니다.

    SELECT v.* FROM View v WHERE v.post_id = ?


실제 실행되는 SQL

sql
코드 복사
-- 첫 번째 쿼리: 게시글(Post) 조회
SELECT * FROM Post;

-- N번의 추가 쿼리: 각 게시글의 조회수(View)를 가져옴
SELECT * FROM View WHERE post_id = 1;
SELECT * FROM View WHERE post_id = 2;
SELECT * FROM View WHERE post_id = 3;
...

결과:

1 + N개의 쿼리가 실행됩니다.

게시글이 많아질수록 쿼리 횟수가 급격히 증가해 성능이 저하됩니다.


3. N+1 문제 해결 방법

해결 방법 1: Fetch Join 사용

JOIN FETCH를 사용해 게시글조회수를 한 번에 가져옵니다.

List<Post> posts = em.createQuery(
    "SELECT p FROM Post p JOIN FETCH p.view", Post.class)
    .getResultList();

for (Post post : posts) {
    System.out.println("Title: " + post.getTitle() + ", 조회수: " + post.getView().getViewCount());
}

실행되는 SQL:

-- Fetch Join을 사용해 Post와 View를 한 번에 조회
SELECT p.*, v.*
FROM Post p
JOIN View v ON p.id = v.post_id;

결과:

쿼리 1번만 실행되며 N+1 문제가 해결됩니다.


해결 방법 2: @EntityGraph 사용

@EntityGraph를 설정해 Fetch Join과 같은 효과를 줍니다.


EntityGraph 설정:

@Entity
@NamedEntityGraph(name = "Post.view", attributeNodes = @NamedAttributeNode("view"))
public class Post {
    @Id
    @GeneratedValue
    private Long id;

    private String title;
    private String content;

    @OneToOne(mappedBy = "post", fetch = FetchType.LAZY)
    private View view;
}

EntityGraph 사용:

List<Post> posts = em.createQuery("SELECT p FROM Post p", Post.class)
                     .setHint("javax.persistence.fetchgraph", em.getEntityGraph("Post.view"))
                     .getResultList();

for (Post post : posts) {
    System.out.println("Title: " + post.getTitle() + ", 조회수: " + post.getView().getViewCount());
}

실행되는 SQL:

-- Fetch Join을 사용해 Post와 View를 한 번에 조회
SELECT p.*, v.*
FROM Post p
JOIN View v ON p.id = v.post_id;

결과:

쿼리 1번만 실행되며 N+1 문제가 해결됩니다.


4. 해결 방법 비교

방법설명장점
Fetch JoinJPQL에 JOIN FETCH를 추가간단한 쿼리 최적화
@EntityGraph애너테이션 기반으로 Fetch Join 설정코드 재사용 및 유지보수 용이

5. 결론

N+1 문제는 게시판에서 게시글 조회조회수 조회와 같은 상황에서 자주 발생합니다.

  • Fetch Join이나 @EntityGraph 를 사용하면 1번의 쿼리로 모든 데이터를 효율적으로 가져올 수 있습니다.
  • 조회 성능이 개선되므로 꼭 확인하고 해결해야 합니다.

6. 1:1 관계에서 별도의 테이블을 사용하는 이유와 장점

1:1 관계에서 별도의 테이블을 사용하는 것이 직관적으로 이해되지 않을 수 있습니다. Post 테이블에 조회수 칼럼을 추가하는 것이 더 간단해 보이기 때문입니다. 하지만 이렇게 별도의 테이블을 사용하는 데에는 다음과 같은 이유와 장점이 있습니다.

1. 데이터의 분리와 확장성:

  • 관심사의 분리: Post 테이블은 게시글의 본질적인 정보(제목, 내용 등)를 담고, View 테이블은 부가적인 정보(조회수)를 담당합니다. 이는 데이터 모델의 모듈성을 높이고 관리를 용이하게 합니다.
  • 확장성: 조회수 외에도 추후에 게시글에 대한 다른 통계 정보(댓글 수, 좋아요 수 등)를 추가해야 할 경우, 별도의 테이블을 추가하여 유연하게 확장할 수 있습니다.

2. 데이터 타입의 유연성:

  • 다양한 데이터 타입 지원: 조회수는 숫자형 데이터이지만, 추후에 조회수 변화 추이를 저장하기 위해 날짜/시간 정보를 추가하거나, 다양한 차원의 통계 정보를 저장해야 할 수 있습니다. 별도의 테이블을 사용하면 각 테이블에 맞는 데이터 타입을 적용하여 효율적으로 데이터를 관리할 수 있습니다.

3. 데이터베이스 성능:

  • 인덱싱: 조회수에 대한 검색이나 정렬이 빈번하다면, View 테이블에 별도의 인덱스를 생성하여 조회 성능을 향상시킬 수 있습니다. Post 테이블에 조회수 칼럼을 추가하는 경우, 모든 Post 데이터에 대한 인덱스를 생성해야 하므로 성능 오버헤드가 발생할 수 있습니다.
  • 파티셔닝: 데이터량이 매우 큰 경우, View 테이블을 파티셔닝하여 조회 성능을 개선할 수 있습니다.

4. 데이터 모델의 명확성:

  • 관계 표현: 1:1 관계를 명확하게 표현하여 데이터 모델의 이해도를 높일 수 있습니다.
  • 데이터 정규화: 데이터 중복을 방지하고 데이터 무결성을 유지하는 데 도움이 됩니다.

5. 분산 시스템 환경:

  • 수평적 확장: 분산 시스템 환경에서 각 테이블을 다른 노드에 배치하여 시스템의 확장성을 높일 수 있습니다.

결론적으로, 1:1 관계에서 별도의 테이블을 사용하는 것은 단순히 데이터를 분리하는 것을 넘어, 데이터 모델의 유연성, 확장성, 성능 향상 등 다양한 이점을 제공합니다. 물론, 모든 경우에 별도의 테이블이 항상 최선의 선택은 아닙니다. 시스템의 요구사항과 특성을 고려하여 적절한 데이터 모델을 설계해야 합니다.

6. 예시:

다음과 같은 경우 별도의 테이블을 사용하는 것이 유리합니다.

  • 조회수 외에 다양한 통계 정보를 추가할 가능성이 높은 경우
  • 조회수에 대한 검색, 정렬, 분석이 빈번한 경우
  • 데이터량이 매우 크고 성능이 중요한 경우
  • 분산 시스템 환경에서 시스템을 확장해야 하는 경우

반대로, 다음과 같은 경우 Post 테이블에 조회수 칼럼을 추가하는 것이 간단할 수 있습니다.

  • 조회수 외에 다른 정보를 추가할 필요가 없고
  • 조회수에 대한 쿼리가 단순하며
  • 데이터량이 적은 경우
profile
알고리즘은 백준 허브를 통해 github에 꾸준히 올리고 있습니다.🙂

0개의 댓글

관련 채용 정보