🧵 N + 1 문제란?
- 연관 엔티티를 조회할 때 추가 쿼리가 반복적으로 실행되어
총 N+1번의 쿼리가 나가는 문제
🐢 지연 로딩에서의 N+1 (예시: Tutor N : 1 Company)
select t from Tutor t로 튜터 N개 로드t.getCompany().getName() 접근 시, 회사 조회 쿼리가 또 실행Tutor 목록) + N(Company 조회)@Entity
@Table(name = "tutor")
public class Tutor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id")
private Company company;
public Tutor() {
}
public Tutor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setCompany(Company company) {
this.company = company;
}
}
@Entity
@Table(name = "company")
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "company")
private List<Tutor> tutors = new ArrayList<>();
public Company() {
}
public Company(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Tutor> getTutors() {
return tutors;
}
}
public class LazyMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
Company sparta = new Company("sparta");
Company etc = new Company("etc");
em.persist(sparta);
em.persist(etc);
Tutor tutor1 = new Tutor("tutor1" );
Tutor tutor2 = new Tutor("tutor2" );
Tutor tutor3 = new Tutor("tutor3" );
tutor1.setCompany(sparta);
tutor2.setCompany(etc);
tutor3.setCompany(sparta);
em.persist(tutor1);
em.persist(tutor2);
em.persist(tutor3);
em.flush();
em.clear();
String query = "select t from Tutor t";
List<Tutor> tutorList = em.createQuery(query, Tutor.class).getResultList();
for (Tutor tutor : tutorList) {
System.out.println("tutor.getName() = " + tutor.getName());
System.out.println("tutor.getCompany().getName() = " + tutor.getCompany().getName());
}
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
em.close();
}
emf.close();
}
}
🔍 실행 흐름 요약
Tutor 1 로드 → Company(sparta) 쿼리 → 회사명 출력Tutor 2 로드 → Company(etc) 쿼리 → 회사명 출력Tutor 3 로드 → Company(sparta는 1차 캐시에서) 출력👉 Tutor · Company 수가 많아질수록 N+1 폭증
💡 메모: N+1은 지연 로딩/즉시 로딩 모두에서 발생 가능
⚡
Entity Fetch Join(N:1)이란?
- JPQL에서 한 번의 SQL로 연관 엔티티까지 함께 로드하는 최적화
(SQL의JOIN과 목적이 다름: “객체 그래프”를 한 번에 로드)
🔗 일반 JOIN

🛒 fetch join

✅ N:1 fetch join (Tutor → Company)
String query = "select t from Tutor t join fetch t.company";
Tutor를 조회하면서 연관 Company까지 즉시 로딩fetch join이 우선📚
Collection Fetch Join(1:N)이란?
@OneToMany기본 fetch는 LAZY
✅ 1:N fetch join (Company → Tutors)
String query = "select c from Company c join fetch c.tutorList";
List<Company> companyList = em.createQuery(query, Company.class).getResultList();
for (Company c : companyList) {
System.out.println(c.getName());
System.out.println(c.getTutorList().size());
}
🔎 중복과 DISTINCT
JOIN 특성상 행 중복이 발생할 수 있음DISTINCT 자동 적용select distinct c from Company c join fetch c.tutorList 사용 권장DISTINCT는 PK 기준으로 엔티티 중복 제거 (DB DISTINCT와 의미 차이)🚨 페이징 주의점 (Collection Fetch Join)
String query = "select c from Company c join fetch c.tutorList";
List<Company> list = em.createQuery(query, Company.class)
.setFirstResult(0)
.setMaxResults(1)
.getResultList();
setFirstResult / setMaxResults가 SQL에 반영되지 않음🧠 요약 정리
| 구분 | 핵심 내용 | 포인트 |
|---|---|---|
| N+1 문제 | 연관 엔티티 접근 시 쿼리 반복 | 1 + N 쿼리 폭증 |
Entity Fetch Join | N:1 관계를 한 번에 로드 | join fetch t.company |
| 효과 | 지연 로딩보다 우선, 실제 엔티티 로드 | 영속성 컨텍스트 관리됨 |
Collection Fetch Join | 1\:N 컬렉션 동시 로드 | join fetch c.tutorList |
| 중복 처리 | Hibernate 6.0+ 자동 DISTINCT | JPQL DISTINCT는 PK 기준 |
| 페이징 주의 | 컬렉션 fetch join 시 DB 페이징 불가 | 메모리 페이징으로 동작 |