[Spring Boot] N+1 문제

2024. 10. 5. 18:34·Back-End/Spring Boot
728x90

 

 

들어가기 전에

 

엔티티를 작성할 때 연관된 다른 테이블을 필드에 작성하는 경우가 있다. 한 테이블이 외래키를 갖는 경우 @ManyToOne을 통해 매핑하게 되는데, 여기서 우리는 FetchType을 설정하게 된다. 로딩 방식에는 Lazy Loading과 Eager Loading이 있다. 이 둘에 대해 알아보고 관련해서 나타나는 N+1 문제를 살펴보자.

 

 

Lazy Loading & Eager Loading

Lazy Loading

 

Lazy Loading은 지연 로딩이라고 하며, 연관된 엔티티를 실제로 접근할 때 불러오는 방식이다. 데이터베이스에서 엔티티를 가져올 때 로드하지 않고 해당 필드에 처음 접근할 때 쿼리가 발생한다. 초기 로드 시 필요한 데이터만 가져오므로 메모리 사용량이 줄어드는 이점이 있다. 하지만 이후에 연관된 엔티티에 접근할 때 추가적인 쿼리가 발생해 성능 저하를 일으킬 수 있으며, N+1 문제가 발생할 가능성이 높다.

 

Eager Loading

 

Eager Loading은 즉시 로딩이라고 하며, 한번의 쿼리로 데이터베이스에서 엔티티를 가져올 때 연관된 데이터까지 함께 가져오는 방식이다. 추가적인 쿼리 없이 필요한 데이터를 한번에 로드할 수 있어 쿼리 수를 줄일 수 있다는 장점이 있다. 하지만 불필요한 데이터를 미리 로드하는 경우가 있기 때문에 데이터 양이 많은 경우 메모리 사용량이 증가할 수 있다는 문제가 있다.

 

 

N+1 문제?

 

N+1 문제는 하나의 쿼리로 N개의 데이터를 조회한 후, 각 데이터와 연관된 데이터를 조회하기 위해 N개의 추가 쿼리가 발생하는 현상이다. 이로 인해 불필요한 쿼리가 다수 실행되어 성능 저하가 일어나게 된다. 로딩 방식에 따라 N+1 문제가 발생하는 예시를 함께 보자.

 

 

각 로딩에서의 N+1 문제

Lazy Loading에서의 N+1 문제

 

Member와 Order가 1:N 관계를 가지고 있는 상황이다.

// Member Entity
@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<Order> orders;
}

// Order Entity
@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    private String productName;

    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;
}

 

* 참고로 @OneToMany의 기본 로딩 방식은 Lazy Loading이나 명시적으로 작성하였다.

 

MemberService
// 클래스명, 메소드명 생략
// MemberService
List<Member> members = memberRepository.findAll();

for (Member member : members) {
    List<Order> orders = member.getOrders();
    for (Order order : orders) {
        System.out.println(order.getProductName());
    }
}

 

메소드 내에서 처음 findAll()를 호출할 때 SELECT * FROM Member 쿼리가 실행된다. 이후, 각 회원의 orders에 접근하면 N번의 추가 쿼리가 발생하게 된다. Console를 보면 다음과 같은 쿼리가 출력된다.

SELECT * FROM Order WHERE member_id = ?

 

만약 회원이 10명 있는 경우, Member를 가져오는 쿼리 1번과 Order를 조회하는 쿼리 10번이 추가로 실행되어 N+1 문제가 발생하게 된다.

 
Eager Loading에서의 N+1 문제
// Member Entity
@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Order> orders;
}

// Order Entity
@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    private String productName;

    @ManyToOne(fetch = FetchType.EAGER)
    private Member member;
}

 

* 참고로 @ManyToOne의 기본 로딩 방식은 Eager Loading이나 명시적으로 작성하였다.

 

MemberService
// 클래스명, 메소드명 생략
// MemberService
List<Member> members = memberRepository.findAll();

for (Member member : members) {
    List<Order> orders = member.getOrders();
    for (Order order : orders) {
        System.out.println(order.getProductName());
    }
}

 

Eager Loading은 처음부터 연관된 데이터를 즉시 불러오므로, Member를 전체 조회하는 SELECT * FROM Member 쿼리 외에도 각 Member과 연관된 Order 데이터를 모두 가져온다. 작성된 메소드를 보면 Member를 조회할 때 Order까지 즉시 로딩하면서 각 회원에 대해 개별 쿼리가 실행될 수 있다. Lazy Loading과 유사하게 Member를 조회하는 쿼리 1개와 Order를 조회하는 쿼리 여러 개가 실행될 수 있다.

 
N+1 문제를 어떻게 해결할까?

 

의도하지 않은 쿼리가 추가로 발생하게 된다면 성능 상에 문제가 발생하게 될 것이다. N+1을 해결하는 데에는 크게 3가지가 있다.

 

 

N+1 문제 해결 방법

JOIN FETCH 사용

 

명시적으로 JOIN FETCH를 사용해 연관 데이터를 한 번의 쿼리로 가져오도록 작성한다. 이 경우 다른 테이블와 연관되어 있는 엔티티에 대해 JOIN FETCH로 연결해줘야 한다.

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
	@Query("SELECT m FROM Member m JOIN FETCH m.orders")
	List<Member> findAllWithOrders();
}

 

@EntityGraph 사용

 

JPA의 EntityGraph를 이용하여 필요한 데이터만 미리 로딩하는 방법이다.

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
	@EntityGraph(attributePaths = {"orders"})
	List<Member> findAll();
}
Batch Size 설정

 

hibernate.default_batch_fetch_size 속성을 설정해 연관된 엔티티를 배치로 로딩하여 N+1 문제를 완화할 수 있다.

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

 

 

 

728x90
저작자표시 비영리 변경금지 (새창열림)

'Back-End > Spring Boot' 카테고리의 다른 글

[Spring Boot] Spring AOP  (0) 2024.10.10
[Spring Boot] 스크래핑 비동기 처리  (6) 2024.10.09
[Spring Boot] JPA 쿼리 최적화  (1) 2024.10.04
[Spring Boot] Setter vs Builder  (0) 2024.03.28
[Spring Boot] Logging 처리  (0) 2023.11.27
'Back-End/Spring Boot' 카테고리의 다른 글
  • [Spring Boot] Spring AOP
  • [Spring Boot] 스크래핑 비동기 처리
  • [Spring Boot] JPA 쿼리 최적화
  • [Spring Boot] Setter vs Builder
hxxzz
hxxzz
개발새발 안 되게 개발 노력 중
  • hxxzz
    개발새발
    hxxzz
  • 전체
    오늘
    어제
    • 분류 전체보기 (104)
      • Java (3)
      • Back-End (9)
        • Spring Boot (7)
        • DevOps (1)
        • Redis (1)
      • Computer Scrience (4)
        • Data Structrue (4)
        • Algorithm (0)
      • SQLD (3)
      • 코딩테스트 연습 (85)
        • Programmers (30)
        • 백준 (15)
        • etc. (0)
        • 99클럽 (40)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    N+1 문제
    Stack
    99클럽
    LeetCode
    SQLD
    SQL
    코딩테스트 준비
    java
    프로그래머스
    dfs
    개발자 취업
    백준
    스택
    redission
    자료구조
    jpa
    SpringBoot
    BFS
    til
    Spring Boot
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
hxxzz
[Spring Boot] N+1 문제
상단으로

티스토리툴바