N+1 문제는 뭘까?
JPA의 N+1 문제는 "처음 1번 조회한 결과 (N개)에 대해,
연관 엔티티를 꺼내는 순간 추가 쿼리가 N번 더 나가는 현상" 입니다.
왜 발생하는 걸까?
JPA(실질적으론 Hibernate 같은 구현체)는
연관 관계를 보통 Lazy(지연 로딩)으로 두고,
연관 엔티티에 접근 -> 쿼리를 날려서 가져옵니다.
코드 예시
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY) // 핵심: 지연 로딩
private Team team;
// getter...
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
// getter...
}
문제가 되는 코드
List<Member> members = memberRepository.findAll(); // 1번 쿼리
for (Member m : members) {
System.out.println(m.getTeam().getName()); // 여기서 N번 쿼리 추가 발생 가능
}
실제로 나가는 SQL의 흐름
select * from member;
getTeam() 접근할 때마다 팀 조회 (N번)select * from team where id = ?; -- 멤버 1
select * from team where id = ?; -- 멤버 2
...
select * from team where id = ?; -- 멤버 N
한번에 다 가져오면 안되는 걸까?
이 문제를 어떻게 해결해야 할까?
// Java
@Query("select m from Member m join fetch m.team")
List<Member> findAllWithTeam();
-- sql
select m.*, t.*
from member m
join team t on m.team_id = t.id;
❇️ 장점: 쿼리 하나만 작성하면 되기에 간편하고, N+1을 직접 제거합니다.
⚠️ 주의
1:N (@OneToMany) 을 fetch join 하면 중복된 row가 발생 가능fetch join + 페이징 은 컬렉션에서 위험 / 제약이 큽니다.@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
❇️ 장점: 레포지토리 메서드 이름을 유지하면서 fetch 전략만 바꿈
⚠️ 주의: 복잡한 조건/조인 최적화는 fetch join이 더 직관적일 때가 많음
엔티티를 다 가져오지 않고, 필요한 데이터만 SELECT 해서 DTO로 받습니다.
public record MemberTeamDto(Long memberId, String memberName, String teamName) {}
@Query("""
select new MemberTeamDto(m.id, m.name, t.name)
from Member m join m.team t
""")
List<MemberTeamDto> findMemberTeamDtos();
❇️ 장점: DTO로 딱 필요한 것만 가져오니깐 성능/전송량 최적화, API 응답에 깔끔
⚠️ 주의: "도메인 로직을 엔티티에 두고 활용"하는 흐름과는 맞지 않을 수 있음
Hibernate 설정으로 "LAZY 로딩을 할 때, 한 건씩 말고 IN 쿼리로 묶어서 부탁해"를 적용
hibernate.default_batch_fetch_size=100@BatchSize(size=100)select * from team where id in (?, ?, ?, ...);
설정을 사용했다면 getTeam()이 여러 번 호출이 되어도
Hibernate가 어느 시점에 team을 모아서 묶음 조회를 시도합니다.
❇️ 장점: 코드 변경 적고, 컬렉션 로딩에도 도움
⚠️ 주의: “N+1 제거”가 아니라 “N+1 완화”입니다.
상황에 따라선 쿼리가 여러 번 나갈 수도 있습니다.