티스토리 뷰
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)되지 않은 데이터를 다른 트랜잭션이 읽는 현상
Non-repeatable Read
한 트랜잭션 내에서 같은 데이터를 두 번 조회했는데 결과가 다른 현상
Phantom Read
한 트랜잭션 내에서 일정 범위의 레코드를 두 번 조회했을 때, 없던 데이터가 나타나는 현상
이를 해결하기 위한 트랜잭션 격리 수준
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)
특징: 가장 엄격한 수준으로, 트랜잭션을 순차적으로 진행시킴
문제점: 모든 부정확한 읽기 현상을 방지하지만, 동시 처리 성능이 급격히 저하되어 실무에서는 거의 사용하지 않음
결론
"데이터 정합성(정확성)과 시스템 성능(속도) 사이의 줄타기에서 최적의 균형점을 찾는 것"이 격리 수준 설정의 핵심
