Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 이것이 자바다
- R
- 혼자 공부하는 C언어
- 이스케이프 문자
- Stack
- datastructure
- Serialization
- list 컬렉션
- JSON
- C programming
- C 언어 코딩 도장
- 윤성우의 열혈 자료구조
- buffer
- insertion sort
- coding test
- Selection Sorting
- Algorithm
- s
- 메모리구조
- stream
- 윤성우 열혈자료구조
- Graph
- 알기쉬운 알고리즘
Archives
- Today
- Total
Engineering Note
[SW Engineering] JPQL에서 FETCH JOIN과 DTO 생성자 패턴 충돌 해결하기 본문
SW Engineering
[SW Engineering] JPQL에서 FETCH JOIN과 DTO 생성자 패턴 충돌 해결하기
Software Engineer Kim 2025. 9. 23. 23:37장바구니 조회 기능을 구현하던 중 JOIN FETCH와 DTO 생성자 패턴을 함께 사용했을 때 발생하는 Hibernate 에러를 해결한 과정을 공유하고자 합니다. 이 에러는 JPA를 사용하면서 성능 최적화를 시도할 때 자주 마주치는 문제입니다.
문제 상황
@Query("select new com.shop.dto.CartDetailDto(ci.id, i.itemNm, i.price, ci.count, im.imgUrl) "+
"from CartItem ci " +
"join fetch ci.item i " + // 문제가 된 부분
"join ItemImg im on im.item.id = i.id " +
"where ci.cart.id = :cartId " +
"and im.repImgYn = 'Y' " +
"order by ci.regTime desc"
)
List<CartDetailDto> findCartDetailDtoList(Long cartId);장바구니 조회 쿼리에서 N+1 문제를 해결하기 위해 JOIN FETCH를 사용했는데, 애플리케이션 시작 시 다음과 같은 에러가 발생했습니다.
에러 메시지 분석
핵심 에러 내용
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'cartController' defined
---중략---
Caused by: org.hibernate.query.SemanticException:
Query specified join fetching, but the owner of the fetched association was not present
in the select list
[SqmSingularJoin(com.shop.entity.CartItem(ci).item(i) : item)]
- "join fetching": JOIN FETCH를 사용했음
- "owner of the fetched association was not present in the select list": FETCH된 연관관계의 소유자(CartItem)가 SELECT 절에 없음
- 문제의 핵심: JOIN FETCH로 가져온 데이터를 SELECT 절에서 사용하지 않음
근본 원인 분석
FETCH JOIN의 동작 원리
// FETCH JOIN의 올바른 사용 예시
@Query("SELECT ci FROM CartItem ci JOIN FETCH ci.item WHERE ci.cart.id = :cartId")
List<CartItem> findCartItemsWithItem(Long cartId);
FETCH JOIN의 목적:
- 연관된 엔티티를 한 번의 쿼리로 함께 조회
- 지연 로딩 방지 및 N+1 문제 해결
- 조회된 엔티티 객체 전체를 반환해야 함
DTO 생성자 패턴의 특징
// DTO 생성자 패턴
@Query("SELECT new com.shop.dto.CartDetailDto(ci.id, i.itemNm, i.price) " +
"FROM CartItem ci JOIN ci.item i")
List<CartDetailDto> findCartDetailDtos();
DTO 생성자 패턴의 목적:
- 필요한 컬럼만 선택적으로 조회
- 메모리 사용량 최적화
- 엔티티 객체가 아닌 DTO 객체 반환
왜 충돌하는가?
FETCH JOIN: "엔티티 전체를 가져와서 연관관계를 미리 초기화해줄게"
DTO 생성자: "엔티티는 필요 없고 특정 값들만 뽑아서 DTO로 만들어줘"
이 두 가지 접근 방식은 서로 상반된 목적을 가지고 있어 함께 사용할 수 없습니다.
해결 방법
방법 1: FETCH JOIN 제거 (채택한 방법)
@Query("select new com.shop.dto.CartDetailDto(ci.id, i.itemNm, i.price, ci.count, im.imgUrl) "+
"from CartItem ci " +
"join ci.item i " + // FETCH 제거
"join ItemImg im on im.item.id = i.id " +
"where ci.cart.id = :cartId " +
"and im.repImgYn = 'Y' " +
"order by ci.regTime desc"
)
List<CartDetailDto> findCartDetailDtoList(Long cartId);
장점:
- 필요한 컬럼만 조회하여 메모리 효율적
- DTO로 바로 매핑되어 변환 과정 불필요
- JOIN은 여전히 한 번의 쿼리로 처리되므로 N+1 문제는 발생하지 않음
단점:
- DTO 생성자 의존성
- DTO 구조 변경 시 쿼리도 함께 수정 필요
- 생성자 파라미터 순서와 타입이 정확히 일치해야 함
- 재사용성 부족
- 특정 DTO에만 특화된 쿼리
- 다른 용도로 활용하기 어려움
방법 2: Entity 조회 후 DTO 변환 (JOIN FETCH 사용)
@Query("SELECT ci FROM CartItem ci " +
"JOIN FETCH ci.item i " +
"JOIN FETCH ci.item.itemImgs im " +
"WHERE ci.cart.id = :cartId " +
"AND im.repImgYn = 'Y' " +
"ORDER BY ci.regTime DESC")
List<CartItem> findCartItemsWithDetails(Long cartId);
서비스에서 DTO로 변환:
public List<CartDetailDto> getCartList(String email) {
// ... 기존 로직
List<CartItem> cartItems = cartItemRepository.findCartItemsWithDetails(cart.getId());
return cartItems.stream()
.map(ci -> new CartDetailDto(
ci.getId(),
ci.getItem().getItemNm(),
ci.getItem().getPrice(),
ci.getCount(),
ci.getItem().getRepresentativeImage().getImgUrl()
))
.collect(Collectors.toList());
}
장점:
- FETCH JOIN으로 N+1 문제 완전 해결
- 엔티티 객체 활용 가능
단점:
- 메모리 사용량 증가 (엔티티 전체 로드)
- 추가 변환 과정 필요
방법 3: 네이티브 쿼리 사용
@Query(value = "SELECT ci.cart_item_id, i.item_nm, i.price, ci.count, im.img_url " +
"FROM cart_item ci " +
"JOIN item i ON ci.item_id = i.item_id " +
"JOIN item_img im ON im.item_id = i.item_id " +
"WHERE ci.cart_id = :cartId AND im.rep_img_yn = 'Y' " +
"ORDER BY ci.reg_time DESC",
nativeQuery = true)
List<Object[]> findCartDetailNative(@Param("cartId") Long cartId);
쿼리 실행 계획 확인
방법 1 (JPQL JOIN):
-- 생성되는 SQL
SELECT ci.cart_item_id, i.item_nm, i.price, ci.count, im.img_url
FROM cart_item ci
JOIN item i ON ci.item_id = i.item_id
JOIN item_img im ON im.item_id = i.item_id
WHERE ci.cart_id = ? AND im.rep_img_yn = 'Y'
방법 2 (FETCH JOIN):
-- 생성되는 SQL (더 많은 컬럼 조회)
SELECT ci.*, i.*, im.*
FROM cart_item ci
JOIN item i ON ci.item_id = i.item_id
JOIN item_img im ON im.item_id = i.item_id
WHERE ci.cart_id = ? AND im.rep_img_yn = 'Y'
해결 방법 비교
| 방법 | 메모리 효율성 | 개발 편의성 | 성능 | 권장 사항 |
| JPQL JOIN | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | 단순 조회, 대용량 데이터 |
| FETCH JOIN | ⭐ | ⭐⭐ | ⭐⭐ | 복잡한 비즈니스 로직, 엔티티 조작 필요 |
| Native Query | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | 복잡한 쿼리, DB 최적화 기능 사용 |
배운 점
1. JPA 최적화 전략의 트레이드오프
- FETCH JOIN: 객체 그래프 탐색 최적화에 특화
- DTO 프로젝션: 메모리 사용량 최적화에 특화
- 둘 다 N+1 문제를 해결하지만 접근 방식이 다름
2. 에러 메시지 읽기의 중요성
owner of the fetched association was not present in the select list
이 메시지만 제대로 이해했어도 문제를 빠르게 파악할 수 있었습니다.
3. DTO 패턴 사용 방법과 FETCH JOIN Entity 조회 최적화 차이
DTO 프로젝션을 선택할 때:
- 단순 조회 or 불필요한 Entity 컬럼 제외하고 화면에 필요한 데이터만 조회
- 특정 화면/API 전용 데이터
- 대용량 조회 (메모리 효율성 중요)
- 집계/통계 데이터
엔티티 조회를 선택할 때:
- 비즈니스 로직 실행 필요
- 엔티티 상태 변경 필요
- 연관관계 탐색 필요
- 여러 용도로 메서드 재사용
단순 조회용 DTO 패턴 예시
// 장바구니 목록 - 화면에 보여주기만 함
SELECT new CartDetailDto(ci.id, i.itemNm, i.price, ci.count)
FROM CartItem ci JOIN ci.item i결론
FETCH JOIN과 DTO 생성자 패턴은 각각 다른 목적의 최적화 기법입니다. 함께 사용할 수 없다는 제약을 이해하고, 프로젝트 상황에 맞는 적절한 방법을 선택하는 것이 중요합니다.
이번 경험을 통해 JPA의 다양한 조회 전략과 그 특성을 더 깊이 이해할 수 있었습니다. 비슷한 문제를 겪고 계신 분들에게 도움이 되기를 바랍니다.
'SW Engineering' 카테고리의 다른 글
| [SW Engineering] 이커머스 장바구니 조회 기능 구현: 복잡한 연관관계를 단일 쿼리로 해결하고, 복합 인덱스로 쿼리 최적화 (0) | 2025.09.24 |
|---|---|
| [SW Engineering] JPA @Transactional 어노테이션의 동작방법과 JPA 영속성 컨테스트 (0) | 2025.09.24 |
| [SW Engineering] JPQL join, fetch join (0) | 2025.09.23 |
| [SW Engineering] Spring Boot 배포 최적화 (0) | 2025.09.18 |
| [SW Engineering] 동시성 문제 테스트 환경 구축기 (0) | 2025.09.09 |
Comments