게시글 목록 조회 시 서버가 터지는 N+1 문제를 실제 코드와 쿼리 로그로 분석하고, inner join fetch로 완벽 해결하는 과정을 정리한다.
사전 세팅: 게시글 5~6개 + 서로 다른 Author 등록
// 문제가 되는 DTO 변환 코드
public static PostListDto fromEntity(Post post) {
return PostListDto.builder()
.id(post.getId())
.title(post.getTitle())
.category(post.getCategory())
.authorEmail(post.getAuthor().getEmail()) // N+1 발생 지점!
.build();
}
1. select * from post where del_yn = 'NO'; // 1번 쿼리 (게시글 목록)
2. select * from author where id = 1; // 2번 쿼리 (1번글 작성자)
3. select * from author where id = 2; // 3번 쿼리 (2번글 작성자)
4. select * from author where id = 3; // 4번 쿼리 (3번글 작성자)
...
결과: 게시글 N개 → 1 + N번 쿼리 → 서버 성능 폭발적 저하
@ManyToOne (자식 → 부모): N+1 발생 ❌Post → Author (한 게시글이 한 명의 작성자)
- 게시글 목록 조회: 1번 쿼리
- 각 게시글마다 작성자 조회: N번 쿼리
@OneToMany (부모 → 자식): 1+1 쿼리 ✅Author → Posts (한 명이 여러 게시글 작성)
- 작성자 조회: 1번 쿼리
- 해당 작성자의 게시글 조회: 1번 쿼리 (where author_id=?)
핵심: 자식 엔티티에서 부모 엔티티 접근 시 N+1 발생!
inner join fetch@Query("select p from Post p " +
"inner join p.author " + // 단순 join (Post만 반환)
"where p.delYn = 'NO'")
List<Post> findAllInnerJoin();
결과:
select p.*, a.* from post p
inner join author a on p.author_id = a.id
where p.del_yn = 'NO';
+ post.getAuthor().getEmail() 호출 시 → 별도 N번 쿼리 발생!

fetch join 사용 (완벽 해결)@Query("select p from Post p " +
"inner join fetch p.author " + // Post + Author 즉시 로딩!
"where p.delYn = 'NO'")
List<Post> findAllFetchInnerJoin();
결과:
select p.*, a.* from post p
inner join author a on p.author_id = a.id
where p.del_yn = 'NO';
+ post.getAuthor().getEmail() 호출 → 추가 쿼리 0번!

| 방법 | 쿼리 수 | 실제 SQL | getAuthor() 호출 시 |
|---|---|---|---|
| 기본 | 1+N | select * from post; + N번 author | N번 추가 쿼리 |
| inner join | 2+N | Post join 후 | getAuthor()시 N번 추가 |
inner join fetch | 1번 | select p.*,a.* from post p inner join author a | 추가 쿼리 없음 |
TRACE - findAll() - 시간: 15ms, 쿼리: 7개 (1+6)
fetch join 적용 후TRACE - findAllFetchInnerJoin() - 시간: 3ms, 쿼리: 1개
성능: 15ms → 3ms (5배 향상), 쿼리 7개 → 1개 (7배 감소)
@EntityGraph 활용@EntityGraph(attributePaths = {"author"})
List<Post> findAllWithAuthor();
@OneToMany에서 지연로딩 유지@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Post> posts;
@Query("select p from Post p " +
"inner join fetch p.author a " +
"where p.delYn = 'NO' " +
"order by p.createdAt desc")
Page<Post> findAllWithAuthor(Pageable pageable);
N+1 발생 조건
- 자식(@ManyToOne) → 부모 접근
- 지연로딩(LAZY) + 반복 접근
해결 핵심
- `inner join fetch` = 즉시로딩 강제
- 1번 쿼리로 부모+자식 동시 조회
성능 효과
- 쿼리: 1+N → 1
- 시간: N배 → 1/N
fetch 키워드 하나로 N+1 해결!