Backend
Coroutine Async 로 지도보기 API 성능 개선하기
Ung여기어때
2025년 12월 22일
원문에서 보기 ↗안녕하세요, 여기어때 전시개발팀 백엔드 개발자 엉입니다.
과거 웹 서비스 개편 프로젝트를 진행하면서 지도보기 기능의 성능 문제를 해결한 경험을 공유드리고자 합니다.
시스템 아키텍처
먼저 우리 시스템 구조를 이해해야 합니다.
[클라이언트]
↓
[Web API] ← 우리가 개선한 부분
↓
├─→ [Search API] : 키워드, 필터 등을 기반으로한 검색팀 API
└─→ [표준 상품 API] : 상품 정보 조회 API (최저가 계산)
각 서버의 역할:
- Web API: 프론트엔드와 통신하는 BFF(Backend For Frontend) 역할
- Search API: 키워드, 필터 등을 기반으로한 검색팀 API
- 표준 상품 API: 상품 정보를 가공하고 최저가를 계산하는 서버
중요한 점:
Web API는 표준 상품 API를 호출 하는 클라이언트 입장입니다. 표준 상품 API 내부 로직을 수정할 수 없고, 오직 어떻게 호출하느냐만 제어 가능합니다.
문제 상황: 8초 걸리는 지도 API
지도보기 기능은 사용자의 현재 위치 주변 숙박 시설을 지도에 표시하는 기능입니다.
요구사항:
- 사용자 위치 기준 주변 100개 제휴점 조회
- 각 제휴점의 최저가 정보 표시
- 지도에 핀으로 표시
기존 구현 (동기 방식)
kotlin
fun getPlacesMap(param: PlaceSearchListRequest): PlpResponse {
// 1. Search API 호출: 주변 제휴점 검색
val searchResponse = searchApiService.requestSearch(param, SearchApiType.SEARCH)
val placeList = searchResponse.data.list // 100개의 제휴점 ID
if (placeList.isEmpty()) {
return PlpResponse(emptyList())
}
// 2. 표준 상품 API 호출: 100개 제휴점의 상품 정보 조회
val builderParam = builderParamMapper.toBuilderApiParam(param, placeList)
val products = builderApiService.requestPlp(builderParam) // 💥 여기서 8~10초 소요
// 3. 지도 핀 데이터 생성
return PlpResponse(
items = products.data?.contents?.map { it.toMapItem() } ?: emptyList()
)
}
플로우:
Web API
↓ 1. 검색 (0.2초)
Search API (제휴점 100개 반환)
↓ 2. 상품 조회 (8초) ← 병목!
표준 상품 API (100개 순차 처리)
↓ 3. 응답
Client
성능 측정 결과:
- 평균 응답 시간: 8~10초
- TPS (Transactions Per Second): 30
- 캐시 미스 시 거의 사용 불가능한 수준
처음 스테이지 환경에서 테스트했을 때 제 눈을 의심했습니다. “현대 애플리케이션에서 이게 맞는가…?”
원인 분석: 표준 상품 API의 순차 처리
문제는 표준 상품 API 내부의 동작 방식이었습니다.
표준 상품 API는 우리가 제어할 수 없는 외부 서버입니다. 내부 로직을 수정할 수 없고, 요청을 받으면 다음과 같이 처리합니다:
// 표준 상품 API 내부
final var appPLPModels = Optional.ofNullable(plpDocumentList)
.filter(ObjectUtils::isNotEmpty)
.map(List::stream)
.orElse(Stream.empty())
.map(document -> {
/* PLP 제휴점 단위 처리 로직 */
}
왜 이렇게 느렸을까?
100개의 제휴점의 최저가를 계산하는데 약 8s가 걸렸습니다:
- 해당 제휴점의 모든 객실 조회
- 각 객실의 가격 계산 (할인, 쿠폰 적용)
- PLP 정책에 따른 최저가 선택
왜 표준 상품 API를 수정하지 않았나?
- 표준 상품 API는 현재 운영중인 앱 전시에서도 사용하는 API입니다.
- 내부 로직 수정은 큰 영향 범위와 리스크
성능이 안 좋은 API를 100개씩 한 번에 조회하는 게 맞을까?
지도보기 API에서 사용하는 API는 PLP(상품 리스트 페이지)에서 사용하던 API와 동일한 API 입니다. PLP에서는 30개 정도만 조회하니까 그나마 괜찮게 사용했던 겁니다. (사실 PLP 성능도 좋지는 않습니다..)
하지만 지도는 달랐습니다:
- PLP: 30개 조회 → 2.4초 (30 × 80ms)
- 지도: 100개 조회 → 8초 (100 × 80ms)
같은 API인데 사용처에 따라 성능이 극명하게 차이 났습니다.
병렬 처리 전, 중요한 질문
“표준 상품 API호출을 여러 번으로 나눠서 병렬로 처리하면 어떨까?”
생각하던중, 먼저 확인해야 할 게 있었습니다.
고민 : 좌측 리스트의 순서
우리 지도 화면은 다음과 같은 구조였습니다:

PO 답변:
“최초 지도보기 진입시 LP에서 보여주던 20개 + 지도보기 100개 노출이며 순서보장은 필요없다.”
기획 의도에서도 순서보장이 필요 없긴 했지만, 순서 보장이 필요하더라도 크게 문제되지는 않았습니다.
val results = listOf(
async { delay(1000); "A" }, // 1초 소요
async { delay(100); "B" }, // 0.1초 소요
async { delay(500); "C" } // 0.5초 소요
).awaitAll()
// results = ["A", "B", "C"] ← 입력 순서 유지
// B가 0.1초에 끝나지만, A를 기다렸다가 순서대로 반환
- 병렬 실행: 모든 async 작업은 동시에 시작됨
- 순서 보장: 결과는 입력한 순서대로 반환됨
- 전체 대기: 가장 느린 작업이 끝날 때까지 대기
해결 방안: 동적 Window 분할 + Async
표준 상품 API 호출을 여러 번으로 나눠서 병렬로 처리하는 전략입니다.
표준 상품 API 내부는 수정할 수 없지만, Web API에서 표준 상품 API를 호출하는 방식은 우리가 제어할 수 있습니다.
Before: 단일 호출
Web API
↓ [100개]
표준 상품 API (순차 처리 8초)
↓
응답
After: 병렬 호출
Web API
↓ [33개] ⟍
↓ [33개] → 표준 상품 API (각각 2.4초, 병렬 처리)
↓ [34개] ⟋
응답 (최대 3.2초)
동적 Window 분할 전략
제휴점 개수에 따라 window 개수를 조정합니다:
- 30개 이하: 굳이 나눌 필요 없음
- 31–60개: 2개 window로 충분
- 61개 이상: 최대 3개 window (과도한 병렬 호출 방지)
val windows = when {
placeList.size <= 30 -> placeList.divideIntoWindows(1) // 30개 이하: 1번 호출
placeList.size in 31..60 -> placeList.divideIntoWindows(2) // 31-60개: 2번 병렬
else -> placeList.divideIntoWindows(3) // 61개 이상: 3번 병렬
}
구현: Kotlin Coroutine Async
실제 개선한 코드입니다:
suspend fun getPlacesMap(
param: PlaceSearchListRequest
): PlpResponse {
// 1. Search API 호출: 주변 제휴점 검색
val searchResponse = searchApiService.requestSearch(param, SearchApiType.SEARCH)
val searchPlaceList = searchResponse.data.list
if (searchPlaceList.isEmpty()) {
return PlpResponse(emptyList())
}
// 2. 동적 window 분할
val windows = when {
searchPlaceList.size <= 30 -> searchPlaceList.divideIntoWindows(1)
searchPlaceList.size in 31..60 -> searchPlaceList.divideIntoWindows(2)
else -> searchPlaceList.divideIntoWindows(3)
}
// 3. 각 window별로 표준 상품 API 병렬 호출
val mapItems = coroutineScope {
windows.map { window ->
async(Dispatchers.IO) {
val builderParam = builderParamMapper.toBuilderApiParam(param, window)
builderApiService.requestPlp(builderParam).data?.contents?.map {
it.toMapItem()
} ?: emptyList()
}
}.awaitAll().flatten()
}
return PlpResponse(items = mapItems)
}
플로우 :
Web API
↓ 1. 검색 (0.2초)
Search API
↓ 2. window 분할 (메모리 작업)
├─→ 표준 상품 API (window 1: 33개) ─┐
├─→ 표준 상품 API (window 2: 33개) ─┤ 병렬 처리 (2.6초)
└─→ 표준 상품 API (window 3: 34개) ─┘
↓ 3. 결과 병합 및 응답
Client
성능 측정 결과

100개 제휴점 1회 호출

비동기 호출
개선 효과
- ✅ 응답 시간: 8~10초 → 2초 (사용자 대기 시간 1/4 이상 단축)
- ✅ 처리량: TPS 30 → 215 (동시 처리 능력 약 7배 향상)
- ✅ 사용자 경험: 거의 사용 불가능 → 실용적인 수준으로 개선
마치며
외부 서버를 직접 수정할 수 없는 상황에서도, 클라이언트(BFF) 입장에서 선택할 수 있는 최적화 지점은 분명히 존재합니다.
이번 개선은 표준 상품 API 자체의 연산 로직을 변경한 것이 아니라, 호출 구조를 재설계해 병목을 분산한 사례였습니다.
동적 window 분할과 병렬 호출을 적용한 결과, 스테이지 환경에서는 응답 시간이 대부분 2초 이내로 수렴했고, 현재 상용 환경에서는 이보다 더 빠른 응답 시간을 보이고 있습니다.
과거 8~10초까지 소요되던 지도보기 기능은 현재는 체감상 즉시 반응하는 수준으로 개선되었으며, 실제 운영 지표에서도 안정적인 응답 시간을 유지하고 있습니다.
물론 이번 개선은 표준 상품 API 내부 로직을 직접 개선한 것은 아니며, 호출 방식과 요청 단위를 조정한 구조적 최적화에 해당합니다.
그럼에도 불구하고, 사용처에 맞지 않게 사용되던 API 호출 구조를 재설계하는 것만으로도 사용자 경험과 시스템 처리량을 크게 개선할 수 있음을 확인할 수 있었습니다.
2025년, 전시개발팀은 표준 상품 API 전면 개선 작업을 진행 중입니다. 웹·앱 전시 서비스를 운영하면서 동시에 성능을 개선하는, 말 그대로 달리는 기차의 바퀴를 갈아끼우는 작업을 이어가고 있습니다.
다음 글에서는 지도·PLP·상세 페이지까지 공통으로 확장 가능한 상품 API 구조와 Builder API 성능 개선 방향에 대해 공유해보려 합니다. 전시개발팀의 표준 상품 API 성능 개선기도 기대해 주세요.