JPA에서 발생하는 N+1 문제 / 트랜잭션의 ACID 속성

chaewon·2025년 6월 15일

JPA N+1 문제와 트랜잭션 격리성

JPA에서 발생하는 N+1 문제의 발생 원인과 해결 방안

1. N+1 문제란?

JPA에서 연관된 엔티티를 지연 로딩(LAZY)으로 설정해두면 발생하는 대표적인 퍼포먼스 이슈이다.

문제점설명
쿼리 과다 발생원래는 한 번에 처리할 수 있는 데이터를 위해 수십~수백 개의 쿼리가 추가로 실행됨
DB 부하 증가많은 쿼리가 동시에 실행되면 DB 성능이 급격히 저하됨
응답 지연클라이언트 입장에서는 응답 시간이 길어짐 → UX 악화
트래픽 비효율서버-DB 간 트래픽이 과도하게 증가
스케일 문제데이터가 많아질수록 성능이 기하급수적으로 떨어짐
디버깅 어려움문제를 코드에서 쉽게 발견하지 못하고, 실서비스에서 터질 때까지 모를 수 있음

예를 들어, 목록을 가져오는 수행을 한다.(1)
그 다음 각 팀의 멤버를 반복해서 가져오면(N)
→ 총 1+N개의 쿼리가 실행된다.

"하나의 조회에 대해 연관된 엔티티 수만큼 추가 쿼리가 발생하는 문제"이다.

SELECT * FROM users; -- 1번
SELECT * FROM orders WHERE user_id = 1;  -- 2번
SELECT * FROM orders WHERE user_id = 2;  -- 3번
...
SELECT * FROM orders WHERE user_id = 100; -- 101번

2. 문제가 생기는 원인

연관된 객체를 실제로 사용할 때까지 쿼리가 지연되기 때문이다.


3. 해결 방법

1. Fetch Join

  • JOIN FETCH 문법을 사용하여 연관된 엔티티를 한 번에 로딩
  • 예시:
    @Query("SELECT t FROM Team t JOIN FETCH t.members")
    List<Team> findAllWithMembers();
  • 주의점:
    • 컬렉션을 fetch join 하면 페이징이 불가능
    • 중복 데이터가 조회될 수 있음 → DISTINCT 키워드로 해결 가능

2. EntityGraph

  • JPA 2.1부터 지원되는 기능으로, JPQL 없이도 fetch join과 비슷한 효과를 얻을 수 있음
  • 예시:
    @EntityGraph(attributePaths = {"members"})
    List<Team> findAll();
  • 장점:
    • 코드가 더 선언적이고 깔끔함
    • 쿼리를 커스터마이징하지 않아도 원하는 연관 관계를 로딩할 수 있음

3. Batch Size 설정

  • JPA/Hibernate가 지연 로딩할 때 IN 쿼리로 한 번에 가져오도록 유도

  • 예시 설정 (application.yaml):

    spring:
      jpa:
        properties:
          hibernate.default_batch_fetch_size: 100
  • 예시 동작:

    • 100개의 팀을 가져오고 → 팀 ID들을 IN 절로 묶어서 멤버를 가져옴
      SELECT * FROM member WHERE team_id IN (1,2,3,...,100)
  • 장점:

    • 페이징과 함께 사용 가능
    • N+1 문제를 유연하게 해결 가능
  • 주의점:

    • 연관된 엔티티가 너무 많으면 한 번에 IN 쿼리가 너무 길어져 성능 저하될 수 있음

방법설명
Fetch Join연관된 엔티티를 한 번에 가져오는 JPQL
EntityGraph애노테이션으로 fetch join 효과 주기
Batch Size 설정여러 LAZY 객체를 한 번에 IN 쿼리로 조회

3. 한 줄 정리

N+1 문제는 지연 로딩된 연관 데이터를 반복 조회하며 쿼리가 과도하게 발생하는 문제로, 이를 해결하기 위해 Fetch Join, EntityGraph, Batch Size 설정 등을 사용해 연관 데이터를 효율적으로 미리 로딩하거나 묶어서 불러온다.


트랜잭션의 ACID 속성 중 격리성(Isolation)이 보장되지 않을 때 발생하는 문제점과 격리 수준

🧾 격리성(Isolation)이란?
ACID는 데이터베이스 트랜잭션이 신뢰성 있게 처리되도록 보장하는 4가지 핵심 속성이다.

그 중 격리성은 동시에 실행 중인 트랜잭션들이 서로의 중간 상태를 보거나 영향을 주지 않도록 보장한다.

1. 격리성이 깨지는 경우

문제설명
Dirty Read다른 트랜잭션이 커밋하지 않은 값을 읽음
Non-repeatable Read같은 SELECT인데 값이 바뀌는 경우
Phantom Read같은 조건인데 새로 생긴 행이 추가됨

2. 트랜잭션 격리 수준

수준설명막을 수 있는 문제
READ UNCOMMITTED다 읽음없음
READ COMMITTED커밋된 것만 읽음Dirty Read
REPEATABLE READ같은 SELECT는 같은 결과Non-repeatable Read
SERIALIZABLE가장 엄격, 완전한 고립Phantom Read까지 다 막음

3. 한 줄 정리

격리 수준이 높을수록: 데이터 충돌은 적지만, 성능은 떨어짐

1개의 댓글

comment-user-thumbnail
2025년 6월 16일

깔끔한 정리 👍 잘 읽고 갑니당

답글 달기