Spring Fetch Join과 여러 엔티티 Join의 주의사항
스프링 부트에서 Fetch Join과 여러 엔티티 Join의 주의사항
스프링 부트에서 Fetch Join과 여러 엔티티 Join은 데이터베이스 성능을 최적화하고, 특히 N+1 문제를 해결하는 데 유용하다. 하지만 이 기능들을 잘못 사용하면 성능 문제나 중복 데이터 발생 같은 문제가 생길 수 있으므로 주의해야 한다. 이 글에서는 Fetch Join을 사용할 때 주의할 점, 여러 엔티티를 Join할 때의 문제점, 그리고 페이징과 관련된 추가 고려 사항들을 설명한다.
1. Fetch Join이란?
Fetch Join은 JPA에서 N+1 문제를 해결하기 위해 관계된 엔티티를 한 번의 쿼리로 함께 조회하는 방법이다. 기본적으로 Lazy Loading을 대체하여 성능을 최적화하는 데 유용하지만, 잘못 사용하면 오히려 성능 저하가 발생할 수 있다.
Fetch Join 기본 예시
SELECT m FROM Member m JOIN FETCH m.team
이 예시는 Member
를 조회하면서 Team
을 함께 가져오는 방식이다. 한 번의 쿼리로 두 개의 엔티티 데이터를 동시에 로딩할 수 있어 효율적이다.
2. Fetch Join 사용 시 주의사항
(1) 중복 데이터 문제
Fetch Join은 일대다(OneToMany) 관계에서 중복 데이터를 발생시킬 수 있다. 예를 들어, Team
과 Member
가 1:N 관계일 때 Team
을 기준으로 Fetch Join을 사용하면 동일한 팀 데이터가 여러 번 반복 조회될 수 있다.
해결 방법
- JPQL의
DISTINCT
키워드를 사용해 중복 데이터를 필터링할 수 있지만, 이는 SQL의 DISTINCT와 달리 애플리케이션 레벨에서 중복을 필터링하므로 성능에 영향을 줄 수 있다.
SELECT DISTINCT t FROM Team t JOIN FETCH t.members
(2) 페이징과 Fetch Join
일대다 관계에서 Fetch Join을 사용할 때는 페이징 처리가 제대로 동작하지 않는다. JPA는 Fetch Join과 페이징을 함께 사용할 때 쿼리를 수정하지 않고 모든 데이터를 메모리에 로드한 다음 페이징 처리를 시도한다. 그 결과 메모리 사용량이 급격히 증가하고, 성능이 저하될 수 있다. 특히 대량 데이터를 다룰 때 문제가 된다.
해결 방법
- 서브쿼리를 사용해 페이징할 엔티티의 ID 목록을 먼저 가져오고, 그 후에 해당 엔티티와 관계된 데이터를 다시 로드하는 방식으로 페이징 문제를 해결할 수 있다.
- 또는 배치 패치(batch fetch) 전략을 사용하여 연관된 데이터를 효율적으로 조회할 수 있다.
서브쿼리 예시
// 서브쿼리로 일(1)쪽 엔티티의 ID만 페이징 처리
SELECT t FROM Team t WHERE t.id IN (SELECT t2.id FROM Team t2 ORDER BY t2.name)
이 방식은 ID만 가져오므로 페이징이 SQL 레벨에서 처리되고, 메모리 부담이 줄어든다. 이후 연관된 데이터를 추가로 로딩한다.
배치 패치 예시
@Entity
@BatchSize(size = 10)
public class Team {
//...
}
@BatchSize
를 적용하면 N+1 문제를 해결하면서도 대량 데이터를 효율적으로 로드할 수 있다.
(3) 여러 컬렉션 Fetch Join의 제한
JPA는 한 쿼리에서 여러 컬렉션을 Fetch Join하는 것을 허용하지 않는다. 여러 컬렉션을 동시에 Fetch Join하려 하면 JPA 예외가 발생할 수 있다.
// 잘못된 예시: 두 개의 컬렉션을 Fetch Join
SELECT t FROM Team t JOIN FETCH t.members JOIN FETCH t.projects
이 경우, 컬렉션 중 하나만 Fetch Join으로 처리하고 나머지는 Lazy Loading으로 처리하는 것이 좋다.
(4) 대량 데이터 조회 시 성능 저하
Fetch Join은 관계된 데이터를 한 번에 모두 가져오기 때문에 대량의 데이터를 처리할 때 성능이 저하될 수 있다. 특히 일대다 관계에서 Fetch Join을 사용하면 데이터가 기하급수적으로 증가할 수 있다.
3. 여러 엔티티 Join 시 발생할 수 있는 문제들
(1) 카테시안 곱 문제
여러 엔티티를 Fetch Join하게 되면 카테시안 곱 문제가 발생할 수 있다. 이는 데이터가 곱셈 구조로 증가해 대량의 중복 데이터가 조회되면서 성능에 큰 영향을 미친다.
(2) 중복 데이터 발생
여러 엔티티를 Join할 때 중복 데이터가 발생할 가능성이 크다. 특히 일대다 관계에서 Fetch Join을 사용할 경우 다(N)쪽 엔티티 수만큼 일(1)쪽 엔티티가 중복 조회될 수 있다.
예시
SELECT t FROM Team t
JOIN FETCH t.members
JOIN FETCH t.projects
각 팀에 여러 멤버와 여러 프로젝트가 있을 때 중복된 팀 데이터가 여러 번 조회될 수 있다.
4. 페이징과 관련된 추가 고려 사항
Fetch Join과 페이징을 함께 사용할 때 발생하는 성능 문제를 피하기 위해 몇 가지 추가적인 고려가 필요하다.
(1) 페이징을 위한 서브쿼리와 조합
일대다 관계에서 Fetch Join과 페이징을 함께 쓰면 메모리 과부하가 발생할 수 있다. 이를 피하기 위해 ID만을 조회하는 서브쿼리를 먼저 실행하고, 그 후에 Fetch Join을 적용해 데이터를 가져오는 방식이 유용하다.
- 먼저 페이징에 필요한 ID만 조회:
SELECT t.id FROM Team t ORDER BY t.name
- 이후 ID 목록을 이용해 데이터를 로드:
SELECT t FROM Team t JOIN FETCH t.members WHERE t.id IN :ids
(2) 배치 사이즈 조정
@BatchSize
어노테이션을 통해 배치 사이즈를 조정하면 페이징을 JPA가 알아서 최적화하게 할 수 있다. 배치 사이즈를 적절히 조절하여 필요한 만큼의 데이터만 가져오도록 조정하는 것이 중요하다.
(3) 카운트 쿼리 활용
Fetch Join을 사용할 때 페이징을 하려면 전체 데이터의 개수를 알기 위한 카운트 쿼리가 별도로 필요하다. 이때 Fetch Join을 적용하지 않고 단순히 총 개수만 조회하는 쿼리를 작성하여 페이징 성능을 최적화할 수 있다.
5. 문제 해결 방안 요약
(1) 다대일(N:1) 또는 일대일(1:1) 관계에서 Fetch Join 사용
다대일 또는 일대일 관계에서는 Fetch Join을 사용하는 것이 안전하다. 중복 데이터 문제 없이 효율적으로 데이터를 로드할 수 있다.
SELECT m FROM Member m JOIN FETCH m.team
(2) 일대다(1:N) 관계에서는 Fetch Join 최소화
일대다 관계에서는 Fetch Join을 최소화하고, 필요 시 Lazy Loading을 통해 데이터를 조회하는 것이 좋다.
(3) 배치 패치(batch fetch) 사용
Batch Fetching을 통해 여러 엔티티를 한 번에 가져오면 페이징 문제를 해결하면서도 성능을 유지할 수 있다.
(4) 서브쿼리와 카운트 쿼리 활용
서브쿼리로 ID만 조회하여 페이징을 처리하고, 필요한 경우 카운트 쿼리를 별도로 작성해 전체 데이터 개수를 구하는 방식이 유용하다.
- Fetch Join은 성능 최적화에 유용하지만, 특히 일대다(1:N) 관계에서는 중복 데이터와 페이징 문제가 발생할 수 있으므로 주의가 필요하다.
- 페이징을 위해서는 서브쿼리와 카운트 쿼리를 활용하거나, 배치 패치를 통해 성능을 최적화하는 것이 좋다.
- 여러 엔티티를 Join할 때 카테시안 곱 문제와 중복 데이터 발생 가능성을 염두에 두고 쿼리를 설계해야 한다.
Team
과Member
같은 관계에서는 Member를 기준으로 Fetch Join을 사용하는 것이 중복 문제를 피하는 방법이다.
적절한 쿼리 설계와 최적화 전략을 통해 성능 저하를 방지하고 데이터를 효율적으로 조회할 수 있다.
B
u
y
M
e
A
C
o
f
f
e
e
☕
️