[트러블슈팅] JPA 다중 Fetch Join과 List 사용 시 발생하는 데이터 중복 (Cartesian Product)

2026. 8. 4. 12:53·Project/YAPP 27기

이 글은 Velog에서 이전한 글입니다. Velog 원문 보기

JPA를 사용하여 연관된 엔티티들을 한 번에 조회(Fetch Join)할 때, 의도치 않게 데이터가 중복되어 반환되는 현상을 분석하고 해결한 기록입니다.

1. 문제 상황 (Symptoms)

모임(Meeting) 정보를 조회할 때, 연관된 날짜(dates), 참여자(participants), 투표 날짜(voteDates)를 한 번에 가져오기 위해 JOIN FETCH를 사용했습니다.

하지만 응답 결과(JSON)를 확인해 보니, 동일한 참여자(participants) 정보가 중복되어 리스트에 담기는 현상이 발생했습니다.

오류 로그 (Response Body)
participants 배열에 동일한 id: 193 객체가 8번이나 반복되고 있습니다.

{
  "responseBody": {
    "id": "tgqQEwZyd52B",
    "title": "행복한 하루",
    "dates": ["2026-01-24", ... ],
    "participants": [
      {"id":193, "name":"ㄹㄹㄹ", "hasVoted":false},
      {"id":193, "name":"ㄹㄹㄹ", "hasVoted":false},
      {"id":193, "name":"ㄹㄹㄹ", "hasVoted":false},
      ... (총 8개 중복)
    ],
    "hostName": "ㄹㄹㄹ"
  }
}

1-1. 문제 코드

Repository

@Query("""
    SELECT DISTINCT m FROM MeetingJpaEntity m
    LEFT JOIN FETCH m.dates
    LEFT JOIN FETCH m.participants p
    LEFT JOIN FETCH p.voteDates
    WHERE m.meetId = :meetId
""")
fun findByMeetIdWithParticipants(@Param("meetId") meetId: String): MeetingJpaEntity?

Entity

// 컬렉션 타입이 MutableList(List)로 선언됨
@OneToMany(mappedBy = "meeting", cascade = [CascadeType.ALL], orphanRemoval = true)
var participants: MutableList<ParticipantJpaEntity> = mutableListOf()

2. 원인 분석 (Root Cause)

이 문제의 핵심 원인은 다중 컬렉션 Fetch Join과 List 타입 사용의 조합으로 인한 카르테시안 곱(Cartesian Product) 발생입니다.

2-1. 데이터 뻥튀기 (Cartesian Product)

RDB 관점에서 1:N 관계를 조인하면 데이터 Row 수가 늘어납니다. 여기에 또 다른 1:M 관계를 조인하면 Row 수는 기하급수적으로 증가합니다.

예를 들어 데이터가 다음과 같다면:

  • dates: 3개
  • participants: 2명
  • voteDates: 각 2개

SQL 실행 결과 Row 수는 3(dates) × 2(participants) × 2(voteDates) = 12개가 됩니다.

2-2. List의 중복 허용

JPA 엔티티 조회 시 SELECT DISTINCT를 사용했으나, 이는 SQL 레벨에서의 중복 제거를 의미합니다.
하지만 조인된 결과 Row 자체가 다르기 때문에(다른 컬럼 값의 조합) SQL단에서는 중복이 제거되지 않은 채로 모든 Row를 가져옵니다.

JPA(Hibernate)는 이 결과셋을 객체로 매핑할 때, participants가 List 타입이라면 중복된 엔티티 식별자를 걸러내지 않고 조회된 Row 수만큼 리스트에 그대로 추가합니다. (만약 Set이었다면 자료구조 특성상 중복이 제거됩니다.)


3. 해결 방법 (Solutions)

✅ 적용한 해결책: 컬렉션을 Set으로 변경

중복 데이터가 담기는 것을 방지하기 위해 엔티티의 컬렉션 타입을 List에서 Set으로 변경했습니다. Set은 자료구조 특성상 중복을 허용하지 않으므로, 다중 조인으로 인해 발생한 중복 엔티티가 자동으로 제거됩니다.

// MutableList -> MutableSet 변경
@OneToMany(mappedBy = "meeting", cascade = [CascadeType.ALL], orphanRemoval = true)
var participants: MutableSet<ParticipantJpaEntity> = mutableSetOf()
  • 결과: 중복이 제거되어 정상적인 개수의 participants만 반환됨.
  • 참고: 순서가 중요하다면 LinkedHashSet을 사용하거나, 조회 후 애플리케이션(Service) 레벨에서 정렬해야 합니다.

밑에 나오지만 더 좋은 대안이 존재합니다.
저는 현재 구조를 유지하며 최대한 빠르게 이슈를 해결해야 하는 사항이었기에 Set으로 처리를 했지만, 추후 Fetch Join 분리 & BatchSize 활용하는 방식으로 리팩토링 할 예정입니다.

💡 그 외 대안 (Best Practices)

상황에 따라 더 나은 성능이나 구조를 위해 고려할 수 있는 방법들입니다.

  1. Fetch Join 분리 & BatchSize 활용 (권장)
    • To-One 관계만 Fetch Join으로 가져오고, 컬렉션(To-Many)은 지연 로딩(Lazy Loading)으로 둡니다.
    • @BatchSize 설정(또는 default_batch_fetch_size)을 통해 IN 쿼리로 한 번에 조회하여 N+1 문제를 해결합니다.
  2. 쿼리 분리
    • 부모 엔티티 조회 쿼리와 자식 컬렉션 조회 쿼리를 명시적으로 나누어 실행합니다.
  3. DTO 직접 조회
    • 엔티티를 거치지 않고 필요한 데이터 구조에 맞춘 DTO로 바로 조회하여 불필요한 데이터 로딩을 막습니다.

4. 마무리 및 요약

다중 컬렉션 Fetch Join은 얼핏 보면 쿼리 한 번으로 데이터를 다 가져오는 효율적인 방법 같지만, 실제로는 데이터 폭발(Cartesian Product)과 메모리 이슈를 일으키는 주범이 될 수 있습니다.

  • 핵심: 2개 이상의 컬렉션에 대해 동시에 Fetch Join을 사용하는 것은 피하자.
  • 주의: 부득이하게 사용해야 한다면 컬렉션 타입을 Set으로 하여 중복을 방지하자.
  • 추천: 가장 안전하고 성능 예측이 쉬운 방법은 Fetch Join 최소화 + 지연 로딩 + BatchSize 최적화 조합이다.

'Project > YAPP 27기' 카테고리의 다른 글

ECS(EC2) 환경에서 Grafana Cloud로 모니터링 구축하기 (Prometheus + Loki)  (0) 2026.08.04
Terraform 상태 관리를 로컬에서 S3 + S3 Native State Locking으로 이전한 이유와 대안 비교  (0) 2026.08.03
Terraform으로 구축한 초기 서비스 인프라: 비용 최적화를 위한 단일 AZ 아키텍처 설계 과정  (0) 2026.08.03
'Project/YAPP 27기' 카테고리의 다른 글
  • ECS(EC2) 환경에서 Grafana Cloud로 모니터링 구축하기 (Prometheus + Loki)
  • Terraform 상태 관리를 로컬에서 S3 + S3 Native State Locking으로 이전한 이유와 대안 비교
  • Terraform으로 구축한 초기 서비스 인프라: 비용 최적화를 위한 단일 AZ 아키텍처 설계 과정
pp8817
pp8817
공부한 내용, 개발 관련 지식, 트러블 슈팅 등을 기록합니다. 이전 블로그: https://velog.io/@pp8817/posts
  • pp8817
    끄적이는 개발 log
    pp8817
  • 전체
    오늘
    어제
    • 분류 전체보기 (273)
      • Project (71)
        • 척척학사 (29)
        • Book (23)
        • Saynow (3)
        • YAPP 27기 (4)
        • 나의 작은 프로젝트 (10)
        • Landit (2)
      • Backend (88)
        • Spring (13)
        • Spring MVC (13)
        • Spring Security (4)
        • JPA (26)
        • Database (19)
        • HTTP·Web (13)
        • Architecture (0)
      • Language·CS (59)
        • Java·Kotlin (5)
        • CS Interview (17)
        • Backend Interview (5)
        • Concepts (32)
      • Algorithm (25)
        • Algorithm (24)
        • 소마 알고리즘 스터디 (1)
      • Infra (8)
      • Troubleshooting (11)
      • Retrospective (6)
      • Etc (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Spring MVC
    척척학사
    Spring
    Algorithm
    interview
    Project
    http
    java
    OS
    게시판
    나의 작은 프로젝트
    Python
    object
    jpa
    CS Interview
    개념 정리!
    HTTP WEB 기본 지식
    트러블슈팅
    Book
    BOJ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
pp8817
[트러블슈팅] JPA 다중 Fetch Join과 List 사용 시 발생하는 데이터 중복 (Cartesian Product)
상단으로

티스토리툴바