2025. 11. 20. 16:33ㆍJava/JPA
오늘은 JPA를 사용할 때 반드시 알아야 하는 N+1 문제와 이를 해결하는 fetch join, 그리고 JPQL Join 문법을 실습 중심으로 정리한 날이다.
실제 Hibernate 로그가 어떻게 출력되는지 직접 보면서 이해하는 “핵심 실습”이었기 때문에 내용을 정확히 정리해두면 향후 프로젝트/JPA 최적화에 큰 도움이 된다.
❶ N+1 문제란?
기본 엔티티 목록을 조회하는 SELECT(1) 후, 각 엔티티가 가지고 있는 연관 엔티티를 N번 추가로 조회하며 발생하는 비효율이다.
String jpql = "select o from Orders o";
List<Orders> list = em.createQuery(jpql, Orders.class)
.getResultList();
Hibernate 로그는 아래처럼 찍힌다.
# 1) Orders 목록 조회
select * from orders;
# 2) Orders 각각에 대해 Product/Customer 즉시 로딩(EAGER)
select * from product where id=?
select * from customer where id=?
select * from product where id=?
select * from customer where id=?
...
즉 1번 + N번 = 총 N+1개의 SQL이 실행되어 성능이 치명적으로 떨어진다.
❷ 왜 발생하는가? (핵심 원리)
JPA의 연관 관계 기본 로딩 전략 때문.
- LAZY → “필요한 순간 조회” → 목록 조회 시 거의 100% N+1
- EAGER → “즉시 조회”이지만 JOIN을 강제하는 게 아니라 엔티티 로딩 순간 추가 SELECT를 실행
그래서 EAGER도 목록 조회에서는 N+1이 발생한다.
❸ 해결 방법: Fetch Join
핵심 해결책은 join fetch 한 줄이다.
String jpql =
"select o from Orders o
join fetch o.customer
join fetch o.product";
List<Orders> list = em.createQuery(jpql, Orders.class).getResultList();
Hibernate 출력:
select o.*, c.*, p.*
from orders o
join customer c on o.customer_id = c.id
join product p on o.product_id = p.id;
→ 단 1번의 SELECT로 Orders + Product + Customer를 모두 로딩
→ N+1 완벽 제거

❹ JPQL Join 문법 2가지 방식
1) 현대적인 join 문법 (추천)
select o, p
from Orders o
join o.product p
관계 필드를 기준으로 JOIN하는 정석 JPQL 문법이다.
2) 전통 스타일 (SQL 스타일)
select o, p
from Orders o, Product p
where o.product = p
이는 SQL의 고전 조인 방식이며 내부적으로는 아래와 동일한 의미이다.
from orders o
cross join product p
where o.product_id = p.id
실제로는 INNER JOIN처럼 동작한다.
하지만 실무/강의에서는 관계 필드 기반 JOIN을 훨씬 더 많이 사용한다.
❺ 엔티티 두 개를 동시에 Select하는 경우
select o, p
from Orders o
join o.product p
결과 타입은 무조건 List<Object[]> 형태.
for(Object[] row : result){
Orders o = (Orders) row[0];
Product p = (Product) row[1];
}
❻ UnknownEntityException 원인 정리
JPA/Hibernate 수동 환경에서는 엔티티를 직접 등록해야 한다.MyPersistenceUnitInfo 안에 아래가 반드시 있어야 한다.
@Override
public List<String> getManagedClassNames() {
return List.of(
"entity.Orders",
"entity.Customer",
"entity.Product"
);
}
등록을 빠뜨리면 Hibernate는 엔티티를 모른다 → UnknownEntityException: Could not resolve root entity ‘Orders’
❼ 실무적 결론
JPA 사용할 때 반드시 기억해야 하는 3줄 결론:
- 1) 목록 조회 시는 무조건 LAZY + fetch join 조합
- 2) EAGER는 예상보다 더 쉽게 N+1을 유발
- 3) JPQL join은 join o.product 형태가 가장 안전하고 정석
오늘 내용은 JPA 성능 최적화의 핵심이기 때문에 나중에 스프링 부트 프로젝트에서도 그대로 적용된다.
마무리
Hibernate 로그를 직접 확인하며 N+1 → fetch join → join 문법을 모두 체득한 하루였다.
이제부터 JPQL 목록 조회가 보이면 자동으로 “여기 fetch join 필요하겠는데?” 하고 감이 올 것이다.
'Java > JPA' 카테고리의 다른 글
| [LG U+ 유레카 3기] Spring Data JPA find() 메서드 실습 (0) | 2025.11.26 |
|---|---|
| [LG U+ 유레카 3기] Spring Data JPA CRUD + Lombok + 패턴 실습 정리 (0) | 2025.11.25 |
| [LG U+ 유레카 3기] 순수 JPA 복합키 실습(@IdClass & @EmbeddedId) (1) | 2025.11.17 |
| [LG U+ 유레카 3기]순수JPA 실습 — JPQL (Java Persistence Query Language) (0) | 2025.10.20 |
| [LG U+ 유레카 3기]순수 JPA 실습 엔티티 생명주기 (persist / find / merge / detach / remove) (0) | 2025.10.20 |