이 파일의 학습 목적
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;
⚠️ EAGER — 즉시로딩
@ManyToOne(fetch = FetchType.EAGER)
private User user;
Java — LAZY 프록시 객체 동작 원리
Board board = boardRepository.findById(1L).get();
String nickname = board.getUser().getNickname();
⚠️ 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] 작성자
총 쿼리: 101번 😱
Java — N+1 문제가 발생하는 코드
List<Board> boards = boardRepository.findAll();
for (Board board : boards) {
String nickname = board.getUser().getNickname();
}
💡 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 발생
List<Board> findAll();
✅ fetch join — 쿼리 1번
@Query("SELECT b FROM Board b"
+ " JOIN FETCH b.user")
List<Board> findAllWithUser();
Java — @EntityGraph — 어노테이션 방식 fetch join
public interface BoardRepository extends JpaRepository<Board, Long> {
@EntityGraph(attributePaths = {"user"})
List<Board> findAll();
@EntityGraph(attributePaths = {"user"})
Page<Board> findAll(Pageable pageable);
}
✅ fetch join (@Query)
JPQL로 직접 작성
복잡한 조건도 가능
가장 유연함 — 실무 표준
✅ @EntityGraph
어노테이션으로 간단 설정
기존 메서드에 추가 가능
간단한 케이스에 편리
📌 우리 프로젝트 N+1 발생 가능 위치
게시글 목록 → 각 게시글의 작성자 닉네임 표시 시 N+1 발생 가능
→ @EntityGraph(attributePaths = {"user"}) 또는 fetch join으로 해결
→ 또는 DTO 변환 시 필요한 값만 조회하는 JPQL 사용
JPA 완전 이해 시리즈 — 전체 정리
09a · 09b · 09c — 면접에서 꼭 나오는 JPA 핵심
09a
영속성 컨텍스트
1차 캐시 · 스냅샷
더티체킹 · flush
@Transactional 필수
09b
연관관계 매핑
@ManyToOne · @OneToMany
연관관계 주인 (FK 보유)
mappedBy · 양방향 주의
09c
로딩 · N+1
LAZY 기본 권장
N+1 = 쿼리 N+1번
fetch join으로 해결