09b @OneToMany·연관관계 매핑 📚 전체 맵 10a Bean 생명주기
Series 09 · JPA 완전 이해 · 3/3

LAZY
N+1
FETCH JOIN

지연로딩 vs 즉시로딩 · N+1 문제가 생기는 원리 · fetch join으로 해결
실무에서 가장 자주 만나는 JPA 성능 이슈

09a 영속성 컨텍스트·더티체킹 09b @OneToMany·연관관계 09c 지연로딩·N+1·해결
01 Java 02 Spring 03 웹/통신 04~08 프로젝트·심화·DI 09 JPA 완전 이해 10 Bean·어노테이션 11 Docker
이 파일의 학습 목적
09c — 지연로딩 · N+1 · fetch join — JPA 성능의 핵심

게시글 100개 목록을 조회했는데 쿼리가 101번 실행된다. 이것이 N+1 문제다. LAZY 로딩이 왜 권장되는지, N+1이 왜 생기고 어떻게 해결하는지를 이해하면 JPA 성능 이슈의 90%는 해결할 수 있다.

이 파일에서 배울 것
  • LAZY vs EAGER — 로딩 타이밍 차이
  • 프록시 객체 — 가짜 User가 진짜로 바뀌는 시점
  • N+1 문제 — 원인과 쿼리 수 계산
  • fetch join (@Query) — 가장 유연한 해결책
  • @EntityGraph — 어노테이션 방식 해결
프로젝트 코드 연결
  • · 게시글 목록 + 작성자 닉네임 → N+1 발생 지점
  • · @EntityGraph(attributePaths={"user"})
  • · JOIN FETCH b.user — @Query JPQL
  • · LazyInitializationException — 트랜잭션 밖 접근
시리즈 순서: 09a·09b → 09c ← 지금 여기 → 10a Bean 생명주기
01 — 지연로딩 vs 즉시로딩
연관된 Entity를 언제 DB에서 가져오느냐 — LAZY vs EAGER
FetchType — 로딩 타이밍 FetchType
📦 비유 — 택배 수령 방식
LAZY (지연로딩) = 택배 보관함 → 필요할 때 직접 꺼내러 감
EAGER (즉시로딩) = 문 앞 배달 → Board 꺼낼 때 User도 같이 딸려옴
→ LAZY가 기본 권장 — 안 쓰는 데이터까지 가져오면 낭비
✅ LAZY — 지연로딩 (권장)
@ManyToOne(fetch = FetchType.LAZY)
private User user;

// Board 조회 시:
// SELECT * FROM board WHERE id=1
// → User는 아직 안 가져옴

// getUser() 호출 시:
// SELECT * FROM users WHERE id=1
// → 그때서야 가져옴
⚠️ EAGER — 즉시로딩
@ManyToOne(fetch = FetchType.EAGER)
private User user;

// Board 조회 시:
// SELECT b.*, u.* FROM board b
// JOIN users u ON b.user_id = u.id
// → User까지 항상 같이 가져옴
// → 필요 없을 때도 낭비
Java — LAZY 프록시 객체 동작 원리
// LAZY로 Board 조회
Board board = boardRepository.findById(1L).get();
// board.user 는 아직 진짜 User가 아님
// → Hibernate가 만든 프록시 객체 (가짜 User)
// → 껍데기만 있고 DB 조회는 안 함

String nickname = board.getUser().getNickname(); // ← 이때!
// getUser().getNickname() 호출 순간
// → 프록시가 진짜 DB 조회 실행
// → SELECT * FROM users WHERE id = board.user_id
// → 진짜 User 객체로 교체
⚠️ LazyInitializationException
트랜잭션 밖에서 LAZY 연관 객체에 접근하면 발생
영속성 컨텍스트가 이미 닫혀서 프록시가 DB 조회 못함
해결책: @Transactional 범위 안에서 접근하거나 → fetch join 사용
02 — N+1 문제
게시글 100개 조회 = 쿼리 101번 — 성능 최악의 주범
💥 N+1 문제 — JPA 대표 성능 이슈 Performance
🍕 비유 — 피자 가게 주문
게시글 목록 가져오기 = 피자 목록 조회 (1번)
각 게시글의 작성자 이름 = 각 피자마다 만든 요리사 확인 (N번)
→ 피자 100개 → 요리사 100번 따로따로 물어봄 = 총 101번 왕복
피자 N개면 N+1번 쿼리
1번
SELECT * FROM board — 게시글 100개 조회
+1
SELECT * FROM users WHERE id = 1 — board[0] 작성자
+1
SELECT * FROM users WHERE id = 2 — board[1] 작성자
+1
SELECT * FROM users WHERE id = 3 — board[2] 작성자
...
100번 반복
총 쿼리: 101번 😱
Java — N+1 문제가 발생하는 코드
// 게시글 목록 조회
List<Board> boards = boardRepository.findAll();
// → 쿼리 1번: SELECT * FROM board

for (Board board : boards) {
String nickname = board.getUser().getNickname();
// board.getUser() 호출할 때마다
// → SELECT * FROM users WHERE id = ?
// → 게시글 100개면 100번 추가 쿼리!
}
// 총 101번 쿼리 (1 + N)
💡 N+1은 LAZY만의 문제가 아님
EAGER도 N+1 발생 가능 — findAll() 같은 JPQL 쿼리에서
LAZY는 루프 안에서 접근할 때 / EAGER는 조회 시점에 N번 추가 쿼리
근본 해결책은 fetch join
03 — N+1 해결 방법
fetch join · @EntityGraph · @BatchSize — 상황별 선택
🔧 해결책 1 — fetch join (가장 많이 씀) Fetch Join
🚌 비유 — 단체 버스
N+1 = 게시글마다 택시 한 대씩 → N대 택시
fetch join = 버스 한 대에 다 태우기 → JOIN으로 한 번에 가져옴
❌ 일반 조회 — N+1 발생
// Repository
List<Board> findAll();

// 실행되는 SQL:
// SELECT * FROM board
// SELECT * FROM users WHERE id=1
// SELECT * FROM users WHERE id=2
// ... N번 반복
✅ fetch join — 쿼리 1번
// Repository — JPQL
@Query("SELECT b FROM Board b"
+ " JOIN FETCH b.user")
List<Board> findAllWithUser();

// 실행되는 SQL:
// SELECT b.*, u.*
// FROM board b
// JOIN users u ON b.user_id=u.id
// → 쿼리 1번으로 다 가져옴!
Java — @EntityGraph — 어노테이션 방식 fetch join
// @EntityGraph: @Query 없이 fetch join 효과
public interface BoardRepository extends JpaRepository<Board, Long> {

@EntityGraph(attributePaths = {"user"})
// Board 조회 시 user도 함께 JOIN해서 가져옴
List<Board> findAll();

@EntityGraph(attributePaths = {"user"})
Page<Board> findAll(Pageable pageable);
}

// 실행 SQL: SELECT b.*, u.*
// FROM board b LEFT JOIN users u ON b.user_id = u.id
✅ fetch join (@Query)
JPQL로 직접 작성
복잡한 조건도 가능
가장 유연함 — 실무 표준
✅ @EntityGraph
어노테이션으로 간단 설정
기존 메서드에 추가 가능
간단한 케이스에 편리
JPA 완전 이해 시리즈 — 전체 정리
09a · 09b · 09c — 면접에서 꼭 나오는 JPA 핵심
09a
영속성 컨텍스트
1차 캐시 · 스냅샷
더티체킹 · flush
@Transactional 필수
09b
연관관계 매핑
@ManyToOne · @OneToMany
연관관계 주인 (FK 보유)
mappedBy · 양방향 주의
09c
로딩 · N+1
LAZY 기본 권장
N+1 = 쿼리 N+1번
fetch join으로 해결
면접 단골 질문 — 핵심 답변 요약
// Q. 더티체킹이란?
// A. 영속성 컨텍스트가 Entity의 스냅샷(원본)을 보관하다가
// 트랜잭션 끝날 때 현재 상태와 비교해서
// 변경된 부분이 있으면 자동으로 UPDATE SQL을 실행하는 기능
// → save() 없이 UPDATE 가능 / @Transactional 필수

// Q. N+1 문제란?
// A. 1번의 쿼리로 N개의 Entity를 조회했을 때
// 각 Entity의 연관 Entity를 조회하기 위해 N번의 추가 쿼리가 발생하는 문제
// → 게시글 100개 조회 + 각 작성자 100번 = 101번
// → fetch join / @EntityGraph 로 해결

// Q. LAZY와 EAGER 차이는?
// A. LAZY = 연관 Entity에 실제 접근할 때 DB 조회 (권장)
// EAGER = 부모 Entity 조회 시 연관 Entity도 항상 같이 조회
// → 기본적으로 LAZY 사용, 필요 시 fetch join으로 한 번에 조회
09b @OneToMany·연관관계 매핑 📚 전체 맵 10a Bean 생명주기