ORM 사용 시 발생할 수 있는 N+1 문제란 무엇인가요?

김상욱·2024년 12월 15일

ORM 사용 시 발생할 수 있는 N+1 문제란 무엇인가요?

What?

N+1 문제는 데이터베이스에 데이터를 조회할 때 발생하는 성능 저하 문제 중 하나로, 특히 ORM을 사용할 때 자주 나타납니다. 이 문제는 기본 쿼리 1개와 추가 쿼리 N개로 구성된 일련의 쿼리 실행 때문에 발생합니다. 결과적으로, 요청한 데이터의 양이 많아질수록 데이터베이스에 보내는 쿼리 수도 선형적으로 증가하여 성능이 크게 저하됩니다.

Why?

ORM은 객체 지향 프로그래밍과 관계형 데이터베이스 간의 불일치를 해결하기 위해 설계되었습니다. 하지만, 객체 간의 관계를 데이터베이스의 조인 관계로 매핑할 때, ORM이 자동으로 모든 관련 데이터를 로드하지 않고 필요할 때마다 추가 쿼리를 실행하는 경우가 있습니다. 이로 인해 기본 쿼리 외에 추가로 N개의 쿼리가 실행되어 N+1 문제가 발생합니다.

Spring과 JPA를 이용한 N+1 문제 예시

시나리오

예를 들어, 회원과 게시글 간의 관계에서 하나의 회원은 여러 게시글을 작성할 수 있습니다. 이를 JPA 엔티티로 정의하면 다음과 같습니다.

// Member.java
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // 게시글과의 일대다 관계
    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<Post> posts = new ArrayList<>();

    // getters and setters
}

// Post.java
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    // 회원과의 다대일 관계
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    // getters and setters
}
N+1 문제 발생 예시

여기서 MemberRepository는 기본 JpaRepository를 상속

// MemberService.java
@Service
public class MemberService {
    @Autowired
    private MemberRepository memberRepository;

    @Transactional(readOnly = true)
    public void printMembersAndPosts() {
        List<Member> members = memberRepository.findAll();

        for (Member member : members) {
            System.out.println("Member: " + member.getName());
            for (Post post : member.getPosts()) {
                System.out.println("    Post: " + post.getTitle());
            }
        }
    }
}
발생 쿼리
SELECT * FROM member;
SELECT * FROM post WHERE member_id = 1;
SELECT * FROM post WHERE member_id = 2;
...
SELECT * FROM post WHERE member_id = N;

총 N+1개의 쿼리가 실행되어 회원 수(N)가 많아질수록 데이터베이스에 보내는 쿼리 수가 급격히 증가하여 성능 저하가 발생합니다.

문제의 원인

Member 엔티티의 posts 필드는 LAZY 로딩으로 설정되어 있습니다. 따라서 member.getPosts()를 호출할 때마다 별도의 쿼리가 실행됩니다. 회원이 100명이라면 기본 쿼리 1개와 추가 쿼리 100개가 실행되는 셈입니다.

N+1 문제 해결 방법

N+1 문제를 해결하기 위해서는 가능한 한 최소한의 쿼리로 필요한 모든 데이터를 가져오는 것이 중요합니다. 이를 위해 다양한 전략과 JPA 기능을 활용할 수 있습니다.

  1. Fetch Join (Eager Loading) 사용
  • Fetch Join은 JPQL에서 JOIN FETCH를 사용하여 연관된 엔티티를 함께 조회하는 방법입니다. 이를 통해 N+1 문제를 해결할 수 있습니다.

Lazy Loading은 연관된 엔티티를 실제로 필요할 때 조회하여 초기 쿼리는 빠르지만 연관된 데이터가 많아질수록 추가 쿼리가 많이 발생할 수 있습니다. 하지만 Eager Loading은 연관된 엔티티를 처음부터 함께 조회하여 초기 쿼리가 조금 복잡해지지만 추가 쿼리를 줄여 전체 성능을 향상시킬 수 있습니다.

// MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("SELECT m FROM Member m JOIN FETCH m.posts")
    List<Member> findAllWithPosts();
}
// MemberService.java
@Service
public class MemberService {
    @Autowired
    private MemberRepository memberRepository;

    @Transactional(readOnly = true)
    public void printMembersAndPosts() {
        List<Member> members = memberRepository.findAllWithPosts();

        for (Member member : members) {
            System.out.println("Member: " + member.getName());
            for (Post post : member.getPosts()) {
                System.out.println("    Post: " + post.getTitle());
            }
        }
    }
}

이렇게 되면 다음과 같은 쿼리만 발생합니다.

SELECT m.*, p.* 
FROM member m 
JOIN post p ON m.id = p.member_id;

1개의 쿼리로 모든 회원과 그들의 게시글을 한 번에 조회할 수 있습니다.

  1. Entity Graph 사용
    JPA 2.1부터 지원되는 Entity Graph를 사용하여 연관된 엔티티를 함께 로드할 수 있습니다. Entity Graph는 특정 조회 시에 로드할 연관된 엔티티를 정의할 수 있는 방법을 제공합니다. 즉 1번과 마찬가지로 엔티티에 연결된 엔티티를 한번에 같이 조회합니다.
    하지만 Entity Graph는 Fetch Join과 달리 페이징과 함께 사용할 때 더 유연하게 동작하며 여러 쿼리에서 동일한 그래프를 재사용할 수 있습니다.
// Member.java
@Entity
@NamedEntityGraph(
    name = "Member.posts",
    attributeNodes = @NamedAttributeNode("posts")
)
public class Member {
    // 기존 코드
}
// MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    @EntityGraph(value = "Member.posts", type = EntityGraph.EntityGraphType.LOAD)
    List<Member> findAll();
}

이와 같이 작성하면 1번의 방법과 유사하게 1개의 쿼리로 모든 데이터를 조회합니다.

  1. Batch Fetching 설정
    Batch Fetching은 연관된 엔티티를 배치 단위로 조회하여 N+1 문제를 완화하는 방법입니다. Hibernate에서 지원하며, 설정을 통해 사용할 수 있습니다.

Batch Fetching은 연관된 엔티티들을 지정된 배치 크기 단위로 한번에 조호히합니다. 예를 들어, 배치 크기를 100으로 설정하면, 연관된 엔티티들을 최대 100개 씩 묶어서 조회합니다.

1. 기본 쿼리(1개) : 모든 회원 조회
2. 배치 쿼리(N/Batch_Size 개): 회원들의 게시글을 배치 단위로 조회합니다.
ex) 총 100명의 회원이 있고 배치 크기가 20이라면 기본 쿼리 1개와 추가 쿼리 5개가 실행됩니다.

Hibernate 설정을 위해 application.yml에 다음 설정을 추가합니다.

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100
// Member.java
@Entity
public class Member {
    // 기존 코드

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    @BatchSize(size = 100)
    private List<Post> posts = new ArrayList<>();

    // getters and setters
}

Hibernate는 한 번에 지정된 배치 크기만큼의 엔티티를 로드합니다. 예를 들어 100명의 회원이 있을 경우, 1개의 기본 쿼리와 추가적으로 1개의 쿼리로 모든 게시글을 로드할 수 있습니다.

  1. DTO를 사용한 쿼리 최적화

직접 필요한 데이터만을 조회하여 성능을 최적화할 수 있습니다.
DTO를 사용하여 필요한 필드만을 선택적으로 조회합니다.

// MemberPostDTO.java
public class MemberPostDTO {
    private String memberName;
    private String postTitle;

    public MemberPostDTO(String memberName, String postTitle) {
        this.memberName = memberName;
        this.postTitle = postTitle;
    }

    // getters and setters
}
// MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("SELECT new com.example.dto.MemberPostDTO(m.name, p.title) FROM Member m JOIN m.posts p")
    List<MemberPostDTO> findAllMemberPostDTOs();
}
// MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("SELECT new com.example.dto.MemberPostDTO(m.name, p.title) FROM Member m JOIN m.posts p")
    List<MemberPostDTO> findAllMemberPostDTOs();
}

이러면 다음과 같은 쿼리만을 호출하므로 필요한 데이터만을 조회하여 불필요한 데이터를 로드하지 않아 성능을 최적화할 수 있습니다

SELECT m.name, p.title 
FROM member m 
JOIN post p ON m.id = p.member_id;

ORM(Object-Relational Mapping)을 사용할 때 발생하는 N+1 문제는 백엔드 개발자가 자주 직면하게 되는 성능 관련 이슈 중 하나입니다. 신입 개발자나 취업 준비 중인 Java, Spring 백엔드 개발자라면 이 문제를 이해하고 해결하는 능력이 중요합니다. 이를 위해 실습할 만한 프로젝트와 단계별 가이드를 제공하겠습니다.

N+1 문제란 무엇인가?

N+1 문제는 데이터베이스에서 데이터를 조회할 때 발생하는 성능 저하 문제입니다. 주로 Lazy Loading 설정에서 발생하며, 기본 쿼리 1개와 추가 쿼리 N개가 실행되어 총 N+1개의 쿼리가 발생하는 상황을 말합니다. 예를 들어, 100명의 회원을 조회할 때 각 회원의 게시글을 조회하기 위해 추가로 100개의 쿼리가 실행되는 것입니다. 이는 데이터베이스와의 왕복 횟수가 증가하여 애플리케이션의 응답 시간이 길어지고, 서버 자원이 낭비되는 결과를 초래합니다.

실습 목표

신입 및 취업 준비 중인 개발자를 위해, 다음과 같은 목표를 설정합니다:

  1. N+1 문제 이해하기: 문제의 원리와 발생 원인을 파악합니다.
  2. 문제 재현하기: 실제 코드로 N+1 문제를 경험해봅니다.
  3. 해결 방법 적용하기: Fetch Join, Entity Graph, Batch Fetching을 사용하여 문제를 해결합니다.
  4. 성과 비교하기: 문제 해결 전후의 성능 변화를 확인합니다.

실습 환경 설정

1. 프로젝트 생성

Spring Boot를 사용하여 간단한 프로젝트를 생성합니다. Spring Initializr를 이용하면 쉽게 설정할 수 있습니다.

  • Project: Maven Project
  • Language: Java
  • Spring Boot: 최신 안정 버전 (예: 3.x)
  • Dependencies:
    • Spring Web
    • Spring Data JPA
    • H2 Database (개발 및 테스트 용도로 인메모리 데이터베이스 사용)
    • Lombok (선택 사항, 코드 간결화를 위해)

2. application.yml 설정

# src/main/resources/application.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driverClassName: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: create
      show-sql: true
      format_sql: true
      default_batch_fetch_size: 100
    properties:
      hibernate:
        batch_fetch_size: 100
  h2:
    console:
      enabled: true

설명:

  • H2 Database: 인메모리 데이터베이스로 빠른 테스트 환경 제공.
  • JPA 설정:
    • ddl-auto: create: 애플리케이션 시작 시 스키마 자동 생성.
    • show-sql: true: 실행되는 SQL 쿼리 로그 출력.
    • format_sql: true: SQL 로그 포맷팅.
    • default_batch_fetch_size: 기본 배치 크기 설정.

3. 엔티티 정의

MemberPost 간의 일대다 관계를 설정합니다.

// src/main/java/com/example/demo/entity/Member.java
package com.example.demo.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.BatchSize;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // 일대다 관계, LAZY 로딩
    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @BatchSize(size = 100) // 배치 크기 설정
    private List<Post> posts = new ArrayList<>();
}
// src/main/java/com/example/demo/entity/Post.java
package com.example.demo.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    // 다대일 관계, LAZY 로딩
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
}

설명:

  • Lazy Loading: 연관된 데이터를 실제로 필요할 때 로드하도록 설정 (fetch = FetchType.LAZY).
  • @BatchSize: 연관된 엔티티를 배치 단위로 로드하도록 설정.

4. 레포지토리 생성

// src/main/java/com/example/demo/repository/MemberRepository.java
package com.example.demo.repository;

import com.example.demo.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
}
// src/main/java/com/example/demo/repository/PostRepository.java
package com.example.demo.repository;

import com.example.demo.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
}

5. 데이터 초기화

테스트를 위해 초기 데이터를 삽입합니다.

// src/main/java/com/example/demo/DataInitializer.java
package com.example.demo;

import com.example.demo.entity.Member;
import com.example.demo.entity.Post;
import com.example.demo.repository.MemberRepository;
import com.example.demo.repository.PostRepository;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class DataInitializer {
    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private PostRepository postRepository;

    @PostConstruct
    public void init() {
        for (int i = 1; i <= 1000; i++) { // 1000명의 회원 생성
            Member member = new Member();
            member.setName("Member " + i);
            memberRepository.save(member);

            // 각 회원마다 10개의 게시글 생성
            for (int j = 1; j <= 10; j++) {
                Post post = new Post();
                post.setTitle("Post " + j + " of " + member.getName());
                post.setMember(member);
                postRepository.save(post);
            }
        }
    }
}

6. 서비스 작성

N+1 문제를 재현하고 해결하는 다양한 방법을 실습할 수 있는 서비스를 작성합니다.

// src/main/java/com/example/demo/service/MemberService.java
package com.example.demo.service;

import com.example.demo.entity.Member;
import com.example.demo.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class MemberService {
    @Autowired
    private MemberRepository memberRepository;

    // N+1 문제 발생 메서드
    @Transactional(readOnly = true)
    public void printMembersAndPostsWithNPlusOne() {
        List<Member> members = memberRepository.findAll();

        for (Member member : members) {
            System.out.println("Member: " + member.getName());
            for (var post : member.getPosts()) { // Lazy Loading으로 추가 쿼리 발생
                System.out.println("    Post: " + post.getTitle());
            }
        }
    }

    // Fetch Join을 사용한 메서드
    @Transactional(readOnly = true)
    public void printMembersAndPostsWithFetchJoin() {
        List<Member> members = memberRepository.findAllWithPosts();

        for (Member member : members) {
            System.out.println("Member: " + member.getName());
            for (var post : member.getPosts()) {
                System.out.println("    Post: " + post.getTitle());
            }
        }
    }

    // Entity Graph을 사용한 메서드
    @Transactional(readOnly = true)
    public void printMembersAndPostsWithEntityGraph() {
        List<Member> members = memberRepository.findAllWithEntityGraph();

        for (Member member : members) {
            System.out.println("Member: " + member.getName());
            for (var post : member.getPosts()) {
                System.out.println("    Post: " + post.getTitle());
            }
        }
    }

    // Batch Fetching을 사용한 메서드
    @Transactional(readOnly = true)
    public void printMembersAndPostsWithBatchFetching() {
        List<Member> members = memberRepository.findAll();

        for (Member member : members) {
            System.out.println("Member: " + member.getName());
            for (var post : member.getPosts()) { // Batch Fetching으로 배치 단위로 쿼리 발생
                System.out.println("    Post: " + post.getTitle());
            }
        }
    }
}

설명:

  • printMembersAndPostsWithNPlusOne(): 기본 findAll()을 사용하여 N+1 문제가 발생하는 예시.
  • printMembersAndPostsWithFetchJoin(): Fetch Join을 사용하여 N+1 문제를 해결.
  • printMembersAndPostsWithEntityGraph(): Entity Graph을 사용하여 N+1 문제를 해결.
  • printMembersAndPostsWithBatchFetching(): Batch Fetching을 사용하여 N+1 문제를 완화.

7. 레포지토리 메서드 수정

각 해결 방법에 맞는 레포지토리 메서드를 추가합니다.

// src/main/java/com/example/demo/repository/MemberRepository.java
package com.example.demo.repository;

import com.example.demo.entity.Member;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    // Fetch Join을 사용한 커스텀 쿼리
    @Query("SELECT m FROM Member m JOIN FETCH m.posts")
    List<Member> findAllWithPosts();

    // Entity Graph을 사용한 메서드
    @EntityGraph(attributePaths = "posts")
    List<Member> findAllWithEntityGraph();
}

설명:

  • findAllWithPosts(): JPQL JOIN FETCH를 사용하여 Fetch Join 구현.
  • findAllWithEntityGraph(): @EntityGraph 어노테이션을 사용하여 Entity Graph 구현.

8. 컨트롤러 작성

서비스 메서드를 호출할 수 있는 REST 컨트롤러를 작성합니다.

// src/main/java/com/example/demo/controller/MemberController.java
package com.example.demo.controller;

import com.example.demo.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MemberController {
    @Autowired
    private MemberService memberService;

    // N+1 문제 발생
    @GetMapping("/nplusone")
    public String nPlusOne() {
        memberService.printMembersAndPostsWithNPlusOne();
        return "N+1 Problem Executed";
    }

    // Fetch Join 사용
    @GetMapping("/fetchjoin")
    public String fetchJoin() {
        memberService.printMembersAndPostsWithFetchJoin();
        return "Fetch Join Executed";
    }

    // Entity Graph 사용
    @GetMapping("/entitygraph")
    public String entityGraph() {
        memberService.printMembersAndPostsWithEntityGraph();
        return "Entity Graph Executed";
    }

    // Batch Fetching 사용
    @GetMapping("/batchfetching")
    public String batchFetching() {
        memberService.printMembersAndPostsWithBatchFetching();
        return "Batch Fetching Executed";
    }
}

9. 실습 단계

이제 실제로 실습을 진행하면서 N+1 문제를 이해하고 해결해보겠습니다.

1단계: N+1 문제 재현

  1. 서비스 메서드 호출: /nplusone 엔드포인트에 GET 요청을 보냅니다.
  2. 쿼리 확인: 콘솔 로그에서 실행되는 쿼리를 확인합니다.
    • 기본 쿼리 1개 (SELECT * FROM member)
    • 각 회원의 게시글을 조회하는 추가 쿼리 N개 (SELECT * FROM post WHERE member_id = ?)
  3. 쿼리 수 관찰: 1000명의 회원이므로 1001개의 쿼리가 실행되는 것을 확인할 수 있습니다.

콘솔 로그 예시:

Hibernate: select member0_.id as id1_0_, member0_.name as name2_0_ from member member0_
Hibernate: select posts0_.member_id as member_i3_0_0_, posts0_.id as id1_1_0_, posts0_.id as id1_1_1_, posts0_.title as title2_1_1_, posts0_.member_id as member_i3_1_1_ from post posts0_ where posts0_.member_id=?
...
(총 1001개의 쿼리)

2단계: Fetch Join 적용

  1. 서비스 메서드 호출: /fetchjoin 엔드포인트에 GET 요청을 보냅니다.
  2. 쿼리 확인: Fetch Join을 사용하여 1개의 조인 쿼리만 실행됩니다.
  3. 쿼리 수 관찰: 1개의 쿼리만 실행되어 N+1 문제가 해결됨을 확인합니다.

콘솔 로그 예시:

Hibernate: select distinct member0_.id as id1_0_0_, member0_.name as name2_0_0_, posts1_.id as id1_1_1_, posts1_.title as title2_1_1_, posts1_.member_id as member_i3_1_1_ from member member0_ inner join post posts1_ on member0_.id=posts1_.member_id

3단계: Entity Graph 적용

  1. 서비스 메서드 호출: /entitygraph 엔드포인트에 GET 요청을 보냅니다.
  2. 쿼리 확인: Entity Graph을 사용하여 1개의 조인 쿼리만 실행됩니다.
  3. 쿼리 수 관찰: 1개의 쿼리만 실행되어 N+1 문제가 해결됨을 확인합니다.

콘솔 로그 예시:

Hibernate: select distinct member0_.id as id1_0_0_, member0_.name as name2_0_0_, posts1_.id as id1_1_1_, posts1_.title as title2_1_1_, posts1_.member_id as member_i3_1_1_ from member member0_ left outer join post posts1_ on member0_.id=posts1_.member_id

4단계: Batch Fetching 적용

  1. 서비스 메서드 호출: /batchfetching 엔드포인트에 GET 요청을 보냅니다.
  2. 쿼리 확인: Batch Fetching 설정에 따라 배치 단위로 쿼리가 실행됩니다.
  3. 쿼리 수 관찰: 예를 들어, 배치 크기가 100이라면 11개의 쿼리 (회원 조회 1개 + 게시글 조회 10개)가 실행됩니다.

콘솔 로그 예시:

Hibernate: select member0_.id as id1_0_, member0_.name as name2_0_ from member member0_
Hibernate: select posts0_.member_id as member_i3_0_, posts0_.id as id1_1_0_, posts0_.title as title2_1_0_, posts0_.member_id as member_i3_1_0_ from post posts0_ where posts0_.member_id in (1,2,3,...,100)
...
(총 11개의 쿼리)

실습을 통한 학습 포인트

  1. 쿼리 로그 분석: 각 단계에서 실행되는 쿼리 수를 비교하면서 N+1 문제가 어떻게 발생하고 해결되는지 이해합니다.
  2. 성능 향상 경험: 쿼리 수가 줄어들면서 애플리케이션의 응답 속도가 어떻게 향상되는지 체감합니다.
  3. 다양한 해결 방법 이해: Fetch Join, Entity Graph, Batch Fetching의 차이점과 장단점을 경험을 통해 학습합니다.
  4. 적절한 방법 선택: 실제 프로젝트에서 상황에 맞는 최적화 방법을 선택하는 능력을 키웁니다.

추가 실습 제안

1. 페이징과 Fetch Join 비교

Fetch Join을 사용하여 페이징을 적용하면 어떻게 동작하는지 확인해보세요. 페이징과 Fetch Join을 함께 사용할 때 발생할 수 있는 문제(데이터 중복 등)를 경험하고, Entity Graph나 Batch Fetching을 사용하여 해결해보세요.

2. Hibernate 통계 기능 활용

Hibernate의 통계 기능을 활성화하여 쿼리 실행 횟수를 모니터링해보세요.

// src/main/java/com/example/demo/service/StatisticsService.java
package com.example.demo.service;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import jakarta.persistence.EntityManager;

@Service
public class StatisticsService {
    @Autowired
    private EntityManager entityManager;

    public void printStatistics() {
        Session session = entityManager.unwrap(Session.class);
        SessionFactory sessionFactory = session.getSessionFactory();
        Statistics stats = sessionFactory.getStatistics();
        stats.setStatisticsEnabled(true);

        System.out.println("Query Count: " + stats.getQueryExecutionCount());
        System.out.println("Second Level Cache Hit Count: " + stats.getSecondLevelCacheHitCount());
        System.out.println("Second Level Cache Miss Count: " + stats.getSecondLevelCacheMissCount());
        System.out.println("Second Level Cache Put Count: " + stats.getSecondLevelCachePutCount());
    }
}

설명:

  • 쿼리 실행 횟수, 캐시 히트/미스 등을 확인하여 성능 최적화 효과를 평가합니다.

3. 다양한 관계 설정 실습

일대일, 다대다 관계에서도 N+1 문제가 발생할 수 있습니다. 다양한 관계 설정에서 N+1 문제를 재현하고 해결해보세요.

결론

N+1 문제는 ORM을 사용할 때 발생할 수 있는 흔한 성능 이슈입니다. 신입 개발자나 취업 준비 중인 개발자는 이를 이해하고 다양한 해결 방법을 실습을 통해 익히는 것이 중요합니다. 위의 실습을 통해 N+1 문제를 경험하고, Fetch Join, Entity Graph, Batch Fetching을 효과적으로 활용하여 성능을 최적화하는 방법을 습득하시기 바랍니다.

추가로, 실습을 진행하면서 다음을 유의하세요:

  • 실제 데이터와 유사한 데이터 사용: 실습 데이터가 실제 환경과 유사할수록 더 현실적인 성능 문제를 경험할 수 있습니다.
  • 쿼리 최적화 도구 활용: Hibernate 로그 외에도 SQL 실행 계획 분석 도구를 사용하여 쿼리의 효율성을 평가해보세요.
  • 지속적인 학습: JPA와 Hibernate는 매우 강력한 도구이므로, 공식 문서와 추가 자료를 참고하여 깊이 있는 이해를 추구하세요.

Happy Coding! 🚀

0개의 댓글