최원종의 개발 블로그

V3-6 연관 관계 및 N + 1 문제 확인 본문

Spring boot 입문

V3-6 연관 관계 및 N + 1 문제 확인

chl6698 2026. 5. 13. 16:49

application-dev.yml 파일 

더보기
server:
  servlet:
    encoding:
      charset: utf-8
      force: true
  port: 8080

logging:
  level:
    root: INFO #모든 라이브러리는 INFO 이상만 출력
    com.tenco: DEBUG # 내 프로젝트는 DEBUG 이상 모두 출력

spring:
  mustache:
    servlet:
      # 머스태치 템플릿 엔진에서 request 객체와 세션 객체에 접근할 수 있도록 허용하는 설정 추가
      expose-session-attributes: true
      expose-request-attributes: true


  #데이터베이스 연결 설정 (MySQL)
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/myblog?serverTimezone=Asia/Seoul
    username: root
    password: root
  #    driver-class-name: org.h2.Driver
  #    url: jdbc:h2:mem:test
  #    username: sa
  #    password:
  h2:
    console:
      enabled: true

  #      초기 데이터 설정
  sql:
    init:
      data-locations:
        - classpath:db/data.sql

  jpa:
    hibernate:
      # create 애플리케이션 시작시 테이블 새로 생성
      # 기존 데이터는 모드 삭제됨 (개발용)
      ddl-auto: update
    #SQL 쿼리를 콘솔에 출력 (개발용)
    show-sql: true
    properties:
      hibernate:
        # SQL 쿼리를 보기 좋게 포맷팅
        format_sql: true
        # N + 1 문제를 완화 하기위한 완충 장치 설정
        # 기본 WHERE 절에서 in 절(in 쿼리로 변경 해준다)
        default_batch_fetch_size: 10
    # data.sql 파일을 하이버네티트 초기화 이후에 실행

    # data.sql 파일을 Hibernate 초기화 이후에 실행
    defer-datasource-initialization: true



application-dev.yml 파일 변경점

1. 데이터베이스 연결 전환 (H2 → MySQL)
변경 전: 가상의 메모리 DB인 H2를 사용 (서버 재시작 시 데이터 삭제)

변경 후: 실제 물리 DB인 MySQL에 연결함.

2. JPA ddl-auto 전략 변경 (create → update)
변경 전 (create): 서버 실행 시마다 기존 테이블을 삭제하고 새로 만듬. (데이터 휘발)

변경 후 (update): 기존 테이블과 엔티티 객체를 비교하여 변경분만 반영함.
기존 데이터는 유지되므로 실제 개발 단계에서 더 많이 사용됨.

3. 성능 최적화: default_batch_fetch_size 추가
default_batch_fetch_size: 10

사용 이유: JPA에서 연관된 엔티티를 조회할 때 쿼리가 너무 많이 나가는 N+1 문제를 완화.

작동 원리: 1개씩 10번 물어볼 쿼리를 IN 절을 사용하여 한 번에 10개씩 묶어서 가져옴.

효과: DB 네트워크 통신 횟수가 획기적으로 줄어들어 전체적인 조회 성능이 크게 향상됨.

연관관계 핵심 정리 사항

JPA에서 DB의 JOIN이 필요한 상황에서 엔티티의 연관관계를 설정해 데이터를 조회할 때,

즉시 로딩과 지연 로딩의 차이와 지연 로딩에서 발생할 수 있는 N+1 문제, 그리고 이를 해결하기 위한 방법 정리

 


개념 정리

JPA에서 데이터베이스의 JOIN이 필요한 상황에서 엔티티 간 연관관계를 설정하여데이터를 조회할 수 있습니다. 
이때, 연관된 데이터를 가져오는 방식으로 즉시 로딩(Eager Loading)과 지연 로딩(Lazy Loading)을 설정할 수 있습니다.

 

1. 즉시로딩 (Eager Loading)

- 엔티티를 조회할 때 연관된 데이터도 즉시 함께 조회합니다.
- 예: @ManyToOne(fetch = FetchType.EAGER) 설정.
- 장점: 한 번의 쿼리로 필요한 모든 데이터를 가져옴.
- 단점: 불필요한 데이터까지 조회할 경우 성능 저하 가능.

 

2. 지연로딩 (Lazy Loading)

- 엔티티 조회 시 연관된 데이터는 실제로 필요할 때(접근 시) 조회됩니다.
- 예: @ManyToOne(fetch = FetchType.LAZY) 설정.
- 장점: 초기 쿼리 부담이 적고 필요한 데이터만 조회.
- 단점: 연관 데이터를 반복적으로 접근할 경우 **N+1 문제** 발생 가능.

 

3. N+1 문제 (EAGER 전략에 리스트 출력 시)

- 지연 로딩 설정 시, 메인 엔티티를 조회하는 1번의 쿼리와
연관된 엔티티를 조회하기 위해 N번의 추가 쿼리가 발생하는 문제.

- 예: 부모 엔티티 1개를 조회하고, 연관된 자식 엔티티 N개를 각각 조회하면 총 1+N번의 쿼리가 실행.
- 결과: 성능 저하.

 

4. Batch Fetch Size (N + 1 완화 작용)

- Batch Fetch Size는 application.yml에 한 줄만 추가하면 전역적으로 N+1 문제를 완화합니다.
- 지연 로딩은 유지하되, 지연 로딩이 발생할 때 하나씩 가져오지 않고 IN 쿼리로 묶어서 가져옵니다.
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

 

 

Batch Fetch Size 동작 원리

-- Batch Fetch Size 없을 때 (N+1 문제)
SELECT * FROM board;                    -- 1번
SELECT * FROM user WHERE id = 1;       -- 2번
SELECT * FROM user WHERE id = 2;       -- 3번
SELECT * FROM user WHERE id = 3;       -- 4번
...                                     -- 10번까지 반복
-- 총 11번 쿼리 실행
-- Batch Fetch Size: 100 설정 시 (IN 쿼리로 변환)
SELECT * FROM board;                                          -- 1번
SELECT * FROM user WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);  -- 2번
-- 총 2번 쿼리 실행

 

Batch Fetch Size 요약

지연 로딩이 발생할 때, 개별 SELECT가 아닌 IN 절을 사용한 SELECT로 변환됨.
Batch Size가 100이고 데이터가 250개면, IN 쿼리가 3번(100 + 100 + 50) 실행됨.
완전한 1+1이 아니라 1 + N/size .

 

5. N+1 문제 해결 방법: JOIN FETCH(N+1정밀제어)

- JPQL에서 JOIN FETCH를 사용해 연관된 엔티티를 한 번의 쿼리로 함께 조회.
- 예: SELECT p FROM Parent p JOIN FETCH p.children
- 장점: 단일 쿼리로 모든 데이터를 가져와 N+1 문제를 방지.
- 주의: 복잡한 연관관계나 대량 데이터 조회 시 쿼리 성능 최적화 필요.

JOIN FETCH 구문에 이해

Board 코드

@Entity
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @OneToMany(mappedBy = "board", fetch = FetchType.LAZY)
    private List<Comment> comments = new ArrayList<>();
}

@Entity
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id")
    private Blog blog;
}

 

Blog와 연관된 Comment를 한 번에 조회

// Blog와 연관된 Comment를 한 번에 조회
String jpql = "SELECT b FROM board b JOIN FETCH b.comments";
List<Blog> blogs = entityManager.createQuery(jpql, Blog.class).getResultList();