티스토리 뷰

위클리페이퍼

위클리 페이퍼9

noobdev25 2026. 1. 12. 09:40

JPA에서 발생하는 N+1 문제의 발생 원인과 해결 방안에 대해 설명하세요.

 

N+1 문제란 무엇인가?

JPA를 포함한 ORM 기술을 사용할 때 발생하는 대표적인 성능 이슈

1번의 쿼리로 N개의 데이터를 조회했는데, 연관된 데이터를 가져오기 위해 N번의 추가 쿼리가 발생하는 현상

 

예시

// 1. 회원 전체 조회 (쿼리 1번 발생)
List<Member> members = memberRepository.findAll(); 

for (Member member : members) {
    // 2. 각 회원의 팀 이름을 출력 (회원이 10명이면 쿼리 10번 발생)
    System.out.println("회원명: " + member.getName() + ", 팀명: " + member.getTeam().getName());
}

 

-- 1. 회원 전체 조회 (1번)
SELECT * FROM member;

-- 2. 각 회원별로 팀 정보를 찾기 위해 추가 쿼리 발생 (N번)
SELECT * FROM team WHERE id = 1;
SELECT * FROM team WHERE id = 2;
...
SELECT * FROM team WHERE id = 10;

 

발생 원인

이 문제는 "JPA가 실행하는 쿼리 방식"과 "개발자가 연관 데이터를 사용하는 시점"의 차이에서 발생함

EAGER(즉시 로딩)의 경우

동작 방식: 엔티티를 조회할 때 연관된 데이터도 항상 함께 가져오려 함

원인: JPQL로 Member를 조회하면 JPA는 SELECT * FROM Member를 먼저 실행. 이후 결과를 받아보니 연관된 Team이 즉시 로딩 설정인 것을 확인하고, 이미 가져온 N명의 회원 각각에 대해 팀 정보를 조회하는 추가 쿼리를 즉시 실행

특징: 메서드 호출 시점에 1+N개의 쿼리가 한꺼번에 쏟아짐

EAGER: 데이터를 가져올 때 N+1 발생

 

LAZY(지연 로딩)의 경우

동작 방식: 연관된 데이터를 실제 사용할 때까지 조회를 미룸(Proxy 객체 활용)

원인: 초기 조회 시에는 쿼리가 1번만 나가지만, 비즈니스 로직(예: 반복문)에서 member.getTeam().getName()처럼 연관 객체의 데이터를 실제로 참조하는 순간 JPA가 DB에 접근하여 데이터를 채워넣음(Proxy 초기화) 이 과정이 루프 횟수(N번)만큼 반복

특징: 데이터를 사용하는 시점에 뒤늦게 N번의 추가 쿼리가 발생

LAZY: 데이터를 사용할 때 N+1 발생

 

해결 방안

1. Fetch Join

가장 확실하고 많이 쓰이는 방법 SQL의 INNER JOIN을 사용하여 한 번에 모든 데이터를 가져옴

해결 방식: JPQL에서 JOIN FETCH 키워드 사용

public interface MemberRepository extends JpaRepository<Member, Long> {
    
    // @Query를 사용하여 직접 패치 조인 정의
    @Query("SELECT m FROM Member m JOIN FETCH m.team")
    List<Member> findAllWithTeamFetchJoin();
}
--PostgreSQL 실행 쿼리: 
SELECT m.*,
       t.* 
FROM member m 
INNER JOIN team t ON m.team_id = t.id
--쿼리 1번

 

2. @EntityGraph

패치 조인을 쿼리문 작성이 아닌 어노테이션으로 해결하는 방법 내부적으로는 패치 조인과 동일하게 LEFT OUTER JOIN을 수행

해결 방식: 메서드 위에 연관 관계 이름을 지정

public interface MemberRepository extends JpaRepository<Member, Long> {

    @EntityGraph(attributePaths = {"team"}) // 같이 가져올 연관 필드명 기입
    @Query("SELECT m FROM Member m")
    List<Member> findAllEntityGraph();
}
--PostgreSQL 실행 쿼리: 
SELECT m.*,
       t.* 
FROM member m 
LEFT OUTER JOIN team t ON m.team_id = t.id
--쿼리 1번

 

3. Batch Size

1:N 관계나 페이징이 필요해서 조인을 쓰기 부담스러울 때 대안 연관된 데이터를 하나씩 조회하지 않고 IN 절을 사용하여 설정한 개수만큼 묶어서 조회

해결 방식: 설정 파일(application.yml)에 전역 설정 추가

spring:
  jpa:
    properties:
      hibernate:
        # 한 번에 IN 절에 넣을 최대 개수 (보통 100~1000 권장)
        default_batch_fetch_size: 100
--회원 N명 조회
SELECT * FROM member;

--팀 N개를 한번에 조회
SELECT 
    * FROM team 
WHERE id IN (
    ?, ?, ?, ?, ?, 
    ?, ?, ?, ?, ?  -- 실제로는 조회된 ID 값들이 들어감
);

 

결론

N+1을 어떻게 다뤄야 할까? 

무조건적인 결벽증보다는 '성능'과 '유연함' 사이의 균형을 잡는 것이 중요


 

트랜잭션의 ACID 속성 중 격리성(Isolation)이 보장되지 않을 때 발생할 수 있는 문제점들을 설명하고, 이를 해결하기 위한 트랜잭션 격리 수준들을 설명하세요.

 

ACID

데이터베이스 관리 시스템(DBMS)에서 트랜잭션 처리의 신뢰성을 보장하기 위한 네 가지 주요 속성

데이터의 정확성과 일관성을 유지하면서 데이터베이스 시스템이 다양한 오류 상황에서도 신뢰성 있게 동작할 수 있도록 해준다

더보기
더보기
  • 원자성(Atomicity)은 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장한다는 것을 의미한다. 예를 들어, 자금 이체는 성공할 수도 실패할 수도 있지만 보내는 쪽에서 돈을 빼 오는 작업만 성공하고 받는 쪽에 돈을 넣는 작업을 실패해서는 안된다. 원자성은 이와 같이 중간 단계까지 실행되고 실패하는 일이 없도록 하는 것이다.
  • 일관성(Consistency)은 트랜잭션 처리 전과 처리 후 데이터 모순이 없는 상태를 유지하는 것을 의미한다. 예를 들어, 무결성 제약 때문에 모든 은행 계좌에 잔고가 있어야 한다면 이를 위반하는 트랜잭션은 중단되어야 한다.
  • 독립성(Isolation)은 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미한다. 이것은 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다. 예를 들어, 은행 관리자는 이체 작업을 하는 도중에 쿼리를 실행하더라도 계좌이체가 일어나는 도중의 송신자 계좌와 수신자 계좌의 금액들의 중간 값들을 볼 수 없어야 한다. 공식적으로 독립성은 트랜잭션 실행내역은 연속적이어야 함을 의미한다. 성능관련 이유로 인해 이 특성은 가장 유연성 있는 제약 조건이다.
  • 지속성(Durability)은 성공적으로 수행된 트랜잭션은 영원히 반영되어야 함을 의미한다. 예를 들어, 시스템 문제로 DB 일관성 체크를 하더라도 이미 수행된 트랜잭션은 유지되어야 한다. 전형적으로 모든 트랜잭션은 로그로 남고 시스템 장애 발생 전 상태로 되돌릴 수 있다. 트랜잭션은 로그에 모든 것이 저장된 후에만 commit 상태로 간주될 수 있다.

 

격리성(Isolation)이 보장되지 않을 때 발생할 수 있는 문제점

Dirty Read

아직 완료(Commit)되지 않은 데이터를 다른 트랜잭션이 읽는 현상

더보기
더보기

A가 잔액을 1,000원에서 2,000원으로 바꿨는데, 취소(Rollback)하기 직전에 B가 2,000원을 읽어버림

Non-repeatable Read

한 트랜잭션 내에서 같은 데이터를 두 번 조회했는데 결과가 다른 현상

더보기
더보기

A가 조회 후 작업 중인데, 그 사이 B가 데이터를 수정(Update)하고 Commit하여 A의 두 번째 조회 결과가 달라짐

Phantom Read

한 트랜잭션 내에서 일정 범위의 레코드를 두 번 조회했을 때, 없던 데이터가 나타나는 현상

더보기
더보기

A가 "성적 90점 이상"을 조회했는데, 그 사이 B가 새로운 95점 데이터를 삽입(Insert)하여 A의 결과 개수가 달라짐

 

이를 해결하기 위한 트랜잭션 격리 수준

Read Uncommitted (레벨 0)

특징: 트랜잭션의 변경 내용이 Commit이나 Rollback 여부와 상관없이 다른 트랜잭션에 보임

문제점: Dirty Read, Non-repeatable Read, Phantom Read 모두 발생

용도: 정합성이 중요하지 않은 대량의 통계 데이터 조회 시에나 간혹 쓰이지만, 일반적인 환경에서는 권장되지 않음

 

Read Committed (레벨 1)

특징: Commit 완료된 데이터만 읽을 수 있습니다. 오라클(Oracle) 및 PostgreSQL의 기본 설정

문제점: Dirty Read는 방지하지만, Non-repeatable Read와 Phantom Read는 여전히 발생

원리: 읽기 작업 시 실제 테이블 값이 아닌 Undo 영역의 백업된 데이터를 읽음

 

Repeatable Read (레벨 2)

특징: 자신의 트랜잭션이 시작되기 전에 Commit된 데이터만 읽음 트랜잭션 내내 동일한 스냅샷을 유지

문제점: Dirty Read, Non-repeatable Read는 방지하지만, 이론적으로 Phantom Read가 발생할 수 있음

 

Serializable (레벨 3)

특징: 가장 엄격한 수준으로, 트랜잭션을 순차적으로 진행시킴

문제점: 모든 부정확한 읽기 현상을 방지하지만, 동시 처리 성능이 급격히 저하되어 실무에서는 거의 사용하지 않음

 

결론

"데이터 정합성(정확성)과 시스템 성능(속도) 사이의 줄타기에서 최적의 균형점을 찾는 것"이 격리 수준 설정의 핵심

'위클리페이퍼' 카테고리의 다른 글

위클리 페이퍼11  (0) 2026.02.07
위클리 페이퍼10  (0) 2026.01.31
위클리 페이퍼8  (0) 2026.01.05
위클리 페이퍼7  (0) 2025.12.29
위클리 페이퍼6  (0) 2025.12.22
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/08   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함