Spring

[Spring] Spring Data JPA에서 @ManyToOne 관계 N + 1 문제 해결하기

2023. 7. 31. 23:43
목차
  1. 개요
  2. 배경
  3. N + 1 문제란?
  4. N + 1 문제 예시 - Lazy Loading
  5. N + 1 문제 해결하기 - Fetch Join
  6. N + 1 문제의 또 다른 해결 방법

개요

이번 글에서는 Spring Data JPA에서 @ManyToOne 관계를 가지는 엔티티를 조회할 때 발생하는 N + 1 문제를 해결하는 방법에 대해 이야기해 보려고 합니다.

 

 

배경

최근에 사내에 ORM을 사용하는 프로젝트가 많아지면서 @ManyToOne 관계를 가지는 엔티티를 조회할 때 N + 1 문제에 대해 고려하지 않고 개발되어 조회 쿼리가 추가적으로 발생되는 이슈가 있어 N + 1 문제가 무엇인지 알아보고 어떻게 해결하는지에 대해 설명해 보겠습니다.

 

 

N + 1 문제란?

연관 관계가 설정된 Entity를 조회할 경우에 조회된 데이터 개수(N) 만큼 연관관계의 조회 쿼리가 추가로 발생하는 문제이다.

 

N + 1 문제 예시 - Lazy Loading

@Entity
@Getter
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "category_id")
    private Long id;

    private String name;
}
@Entity
@Getter
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_id")
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id", nullable = false)
    private Category category;
}

위 코드와 같이 1:N 관계를 가지는 카테고리와 게시글 테이블이 있다고 가정하겠습니다.

 

게시글 테이블이 이 연관 관계의 주인이 될 수 있도록 설정해 주고(게시글 테이블이 카테고리 테이블의 PK를 FK로 가지고 있기 때문) JPA에서 기본이 EAGER로 되어 있기 때문에 Hibernate에서도 스펙에 따르고 있지만 기본적으로 LAZY로 변경해서 사용하길 권장하고 있어 FetchType을 Lazy로 설정해 줍니다.

FetchType을 Lazy로 권장하는 이유 또한 N+1 문제와 관련이 있습니다.

 

Lazy Loading은 연관된 엔티티가 필요하지 않으면 추가 쿼리가 실행되지는 않아 LAZY로 변경해서 사용하길 권장하고 있지만 카테고리를 proxy 객체로 가지고 있고 연관된 엔티티가 필요한 시점에 쿼리가 실행되게 됩니다.

 

 

게시글(Board) 테이블
카테고리(Category) 테이블

위와 같이 데이터가 저장되어 있고 boardRepository에서 findAll 쿼리를 실행하여 Category의 데이터를 참조하려고 할 때 어떤 일이 발생하는지 알아보겠습니다. 

 

@Test
@DisplayName("N+1 발생")
void test1() {
    List<Board> result = boardRepository.findAll();
    result.forEach(board -> {
                System.out.println("Board Title: " + board.getTitle());
                System.out.println("Category Name: " + board.getCategory().getName());
            });
}

이렇게 테스트 코드를 작성하고 테스트를 실행해보겠습니다.

 

 

N + 1 문제 발생

위 로그와 같이 N + 1 문제가 발생한 걸 확인할 수 있습니다.

쿼리를 상세하게 살펴보자면

boardRepository.findAll();

위 코드를 통해 Board 테이블을 조회했는데 카테고리 테이블에 대한 조회가 3건이나 추가로 발생한 것을 확인할 수 있습니다.

모든 Board 테이블에서 FK로 가지고 있는 category_id가 1 ~ 3 범위를 가지기 때문에 추가 쿼리가 3건이 발생했습니다.

이렇게 Lazy Loading에서도 N+1 문제가 발생하는 것을 알아낼 수 있습니다. 

 

위 예시에서는 기본 1건 + 추가 3건이 발생하여 사소하다고 생각될 수 있지만 엔티티 연관관계나 테이블에 적재되어 있는 데이터의 양에 따라 추가 쿼리가 엄청나게 발생할 수 있습니다. 

 

N + 1 문제 해결하기 - Fetch Join

N + 1 문제를 해결하기 위한 여러 방법들이 존재합니다.

이 중 Fetch Join을 통해 N + 1 문제를 해결하는 방법에 대해 설명해 보겠습니다.

 

우선 Fetch Join이 무엇인지부터 알아보겠습니다.

Fetch Join은 JPQL에서 성능 최적화를 위해 제공하는 조인의 종류로 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능입니다.

JPA는 JPQL을 기반한 쿼리들을 지원해 주기 때문에 Fetch Join 또한 사용할 수 있습니다.

이번 포스팅에서는 QueryDSL을 통해 Fetch Join을 사용하는 예시를 작성해보겠습니다.

 

public class BoardCustomRepositoryImpl implements BoardCustomRepository {

    private final JPAQueryFactory jpaQueryFactory;

    public BoardCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    @Override
    public List<Board> findAllByFetchJoin() {
        return jpaQueryFactory.selectFrom(board)
                .innerJoin(board.category, category)
                .fetchJoin()
                .fetch();
    }
}

 

이렇게 QueryDSL을 통해 Fetch Join을 사용하여 테스트를 다시 실행해보겠습니다.

 

Fetch Join 적용

위 사진과 같이 Fetch Join을 적용한 후에는 1개의 쿼리만 실행되는 것을 확인할 수 있습니다.

 

@OneToMany 상황에서는 Category 테이블에서 조인을 통해 Board 테이블을 참조하는 상황이라면 추가로 Distinct를 사용하여 데이터의 중복을 제거할 수 있습니다.

 

N + 1 문제의 또 다른 해결 방법

N + 1 문제를 해결하기 위한 방법은 여러 가지가 존재합니다.EntityGraph를 사용하거나 Batch Size를 설정하는 방법, 연관관계 자체를 맺지 않는 방법 등 여러 방법으로 N + 1 문제를 해결할 수 있는 방법들이 존재합니다.

각각의 장단점을 잘 확인하고 프로젝트의 환경에 맞는 방법을 사용하면 될 거 같습니다.

또 다른 해결 방법들이나 OneToOne, xToMany 관계에 대한 해결 방법들은 추후에 필요한 상황이 오면 테스트 및 포스팅 진행하겠습니다.

저작자표시 비영리 동일조건

'Spring' 카테고리의 다른 글

[Spring] @Valid 사용시에 @RequestBody와 @ModelAttribute에 커스텀 예외처리 적용하기  (0) 2023.08.23
[Spring] JPA NativeQuery 사용시 @Param으로 객체 사용하기  (0) 2022.12.16
[Spring] JPA에서 findBy 매개변수 없이 사용하기  (0) 2022.12.07
[Spring] SpringBoot에서 환경에 따른 Properties 사용하기(Spring Profiles)  (0) 2022.11.12
[Spring] SpringBoot 프로젝트 WAS 배포 후 @PathVariable을 사용하는 Controller에서 MethodArgumentTypeMismatchException이 발생할 때 해결하는 방법  (0) 2022.11.05
  1. 개요
  2. 배경
  3. N + 1 문제란?
  4. N + 1 문제 예시 - Lazy Loading
  5. N + 1 문제 해결하기 - Fetch Join
  6. N + 1 문제의 또 다른 해결 방법
'Spring' 카테고리의 다른 글
  • [Spring] @Valid 사용시에 @RequestBody와 @ModelAttribute에 커스텀 예외처리 적용하기
  • [Spring] JPA NativeQuery 사용시 @Param으로 객체 사용하기
  • [Spring] JPA에서 findBy 매개변수 없이 사용하기
  • [Spring] SpringBoot에서 환경에 따른 Properties 사용하기(Spring Profiles)
Doshisha
Doshisha
Doshisha
Doshisha
Doshisha
전체
오늘
어제
  • 분류 전체보기
    • Java
    • Spring
    • Project
      • Gameple
      • 피파온라인 검색 사이트
    • Node.js
    • DBMS
      • MySQL
      • MSSQL
    • AWS
    • BOJ
    • 프로그래머스
    • 프로그래머스-SQL
    • 컴퓨터 구조
    • 네트워크
    • Git
    • IDE
    • 후기 및 회고
    • 기타
    • Linux
    • Frontend
      • Vue.js
      • jQuery
      • JavaScript
    • Unity
    • WAS
      • Tomcat
    • Jenkins

블로그 메뉴

  • 방명록
  • Github

공지사항

인기 글

태그

  • 게임 플랫폼 개발
  • 네트워크
  • Spring Data JPA
  • java
  • 백준
  • boj
  • C++ BFS
  • 프로그래머스
  • 카카오 코테
  • 백트래킹
  • 카카오 코딩테스트
  • 카카오
  • 구현
  • 게임 API 연동
  • 모두의 네트워크
  • MySQL
  • 게임 플랫폼
  • BFS
  • 자바
  • 프로그래머스 SQL
  • 문자열
  • 코테
  • mysql 서브쿼리
  • 일본
  • SpringBoot Jenkins
  • c++
  • Gameple
  • DP
  • 넥슨 오픈 API
  • SpringBoot Jenkins CI/CD

최근 댓글

최근 글

hELLO · Designed By 정상우.
Doshisha
[Spring] Spring Data JPA에서 @ManyToOne 관계 N + 1 문제 해결하기
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.