❄️ 트러블 슈팅

[트러블 슈팅] SOS 상세 조회 API의 Row Explosion 문제 해결

le2donguk 2026. 3. 22. 21:41

모든 연관 데이터를 단일 LEFT JOIN 쿼리로 조회하던 구조에서 OneToMany 관계인 SosImage를 별도 쿼리로 분리하여 Row Explosion 문제를 해결하고 조회 안정성을 확보했습니다.

그 과정에서 알게 된 내용을 정리해 보겠습니다


문제 상황

SOS 상세 조회 API를 구현하면서 아래 모든 데이터를 단일 QueryDSL 쿼리에서 LEFT JOIN으로 한 번에 조회하도록 설계했습니다.

  • Member 정보 (badge)
  • ProfileImage
  • Business 정보 (name, address)
  • BusinessCode
  • Sos 정보
  • SosImage 리스트

문제는 SosImage가 OneToMany 관계라는 점이었습니다. 이미지가 5개라면 결과 Row도 5개가 생성됩니다. 데이터는 하나인데 이미지 수만큼 행이 복제되는 Row Explosion 현상이 발생했습니다.


원인 분석

근본적인 원인은 두 가지였습니다.

첫째,

Collection(OneToMany) 데이터를 JOIN으로 함께 조회하려 한 점입니다.

QueryDSL에서 Collection JOIN을 사용하면 부모 Row가 자식 데이터 수만큼 증가하고, DB 실행 계획이 복잡해지며, 네트워크 전송량도 늘어납니다. 이를 해결하려면 distinct나 groupBy가 추가로 필요한데, 이 역시 DB CPU 사용량을 높이는 원인이 됩니다

.

둘째,

데이터를 "한 번에 가져오는 것"이 항상 성능적으로 유리하다는 잘못된 전제입니다. 단건 조회와 컬렉션 조회를 목적에 따라 분리하지 않고 동일한 방식으로 접근한 것이 문제였습니다. 동시 접속자가 많아질수록 이 구조는 DB Connection Pool에 직접적인 압박을 가하고, 서비스 확장 시 장애로 이어질 수 있는 구조였습니다.

 


 해결 방법 — 쿼리 분리 전략 (Query Separation)

상세 조회 쿼리를 역할에 따라 두 개로 분리했습니다.

 

1. Base 정보 조회 (fetchOne)

Sos, Member, Business, ProfileImage, BusinessCode를 한 번에 조회합니다. 이 데이터들은 모두 OneToOne 또는 ManyToOne 관계이므로 JOIN을 사용해도 Row가 증가하지 않습니다.

 

2. 이미지 리스트 별도 조회

SosImage 테이블만 단독으로 조회하고, 필요한 칼럼(storageKey)만 가져옵니다.

 
 
java
SosDetailResponse base = baseQuery.fetchOne();

List<String> imageKeys = imageQuery.fetch();

base.setImageKeys(imageKeys);

두 쿼리 결과를 애플리케이션 레벨에서 조합하는 방식으로, DB에는 단순한 쿼리 두 번만 요청됩니다.


 이 구조를 선택한 이유

JOIN으로 한 번에 가져오는 방식(1 Query)이 직관적으로 더 효율적으로 느껴질 수 있지만, Collection이 포함되는 순간 이야기가 달라집니다. Row Explosion이 발생하면 DB가 처리해야 하는 실제 데이터 양이 늘어나고, 이를 정리하기 위한 추가 연산까지 발생합니다.

쿼리 횟수를 줄이는 것보다 각 쿼리를 단순하게 유지하는 것이 고트래픽 환경에서 훨씬 안정적입니다.

추가로, 상세 조회는 ID 기반 단건 조회이기 때문에 이 구조는 향후 Redis 캐싱을 적용하기에도 매우 용이합니다.


개선 결과

항목변경 전변경 후
쿼리 구조 단일 쿼리 + 다중 LEFT JOIN 쿼리 분리 (Base + Image)
Row Explosion 이미지 수만큼 Row 증가 없음
DB 실행 계획 복잡 단순
캐시 적용 가능성 어려움 용이함

 정리

Collection을 포함하는 상세 조회에서 단일 JOIN 쿼리는 오히려 독이 될 수 있습니다. OneToOne / ManyToOne 관계는 JOIN으로 함께 조회하고, OneToMany 컬렉션은 별도 쿼리로 분리하는 패턴이 실무에서 성능과 안정성 모두를 확보할 수 있는 구조입니다. 쿼리 횟수가 아니라 각 쿼리의 복잡도와 반환 Row 수를 기준이 중요합니다.

이전 김영한 강사님의 강의를 보면서 Collection을 조인 시 무엇보다 조심하라던 말이 있었는데 그 말을 직접 체험했었던 것 같습니다