RoomPick 위치 검색 최적화

임선구·2026년 8월 28일

스파르타

목록 보기
21/28

📍 5만 건 위치 검색, 573ms를 91ms로 줄인 과정 — Bounding Box와 Elasticsearch 비교

숙박 예약 서비스 RoomPick을 개발하면서 사용자의 현재 위치를 기준으로 일정 반경 안의 숙소를 검색하는 기능을 구현했다.

기능 자체는 어렵지 않았다.

문제는 데이터가 많아졌을 때였다.

약 5만 건의 숙소 데이터를 넣고 테스트하자 위치 검색의 평균 응답시간이 573.62ms까지 증가했다.

이번 글에서는 단순 거리 계산 방식에서 시작해

  • Bounding Box
  • 복합 인덱스
  • ST_Distance_Sphere()
  • Elasticsearch geo_point

까지 비교하고, 최종적으로 왜 Elasticsearch가 아닌 MySQL 방식을 Production에 선택했는지 정리해보려고 한다.


🔥 문제 상황

처음 위치 검색은 MySQL의 ST_Distance_Sphere()를 이용했다.

사용자의 위도와 경도를 기준으로 각 숙소와의 거리를 계산한 뒤 일정 거리 이내의 숙소만 조회하는 방식이었다.

개념적으로는 간단했다.

사용자 위치
   ↓
모든 숙소와 거리 계산
   ↓
5km 이하 숙소 필터링

하지만 숙소가 약 5만 건까지 증가하자 문제가 발생했다.

ST_Distance_Sphere()를 모든 숙소에 적용해야 했기 때문에 DB는 사실상 전체 데이터를 대상으로 거리 계산을 수행했다.

실행 계획을 확인했을 때 실제 조회 대상은 약 50,040건이었다.

위도와 경도에 인덱스가 존재하더라도 모든 행을 대상으로 거리 함수를 계산하는 구조에서는 인덱스의 장점을 충분히 활용하기 어려웠다.


🤔 모든 숙소의 거리를 계산할 필요가 있을까?

예를 들어 서울 노원구 주변 5km 숙소를 검색한다고 생각해보자.

부산이나 제주도에 있는 숙소까지 정확한 거리를 계산할 필요는 없다.

그래서 먼저 생각한 방법이 Bounding Box였다.

현재 위치를 중심으로 검색 반경을 포함하는 사각형을 먼저 만든다.

┌────────────────────┐
│                    │
│       ● 사용자      │
│      ( 5km )        │
│                    │
└────────────────────┘

그리고 사각형 밖에 있는 숙소는 정확한 거리를 계산하기 전에 제외한다.

즉 검색을 두 단계로 나눴다.

1. 위도·경도 범위로 후보 숙소 검색
                ↓
2. 후보 숙소만 정확한 거리 계산

✅ Bounding Box + 정확한 거리 계산

최종 검색 과정은 다음과 같이 변경했다.

전체 숙소 약 50,000건
        ↓
Bounding Box
        ↓
후보 숙소 약 6,384건
        ↓
ST_Distance_Sphere()
        ↓
실제 반경 5km 숙소 약 5,018건

정확한 거리 계산 대상이 약 5만 건 → 6천 건 수준으로 줄어든 것이다.

여기에 (latitude, longitude) 복합 인덱스를 적용했다.

Bounding Box 검색은 다음처럼 범위 조건을 사용할 수 있다.

latitude BETWEEN :minLatitude AND :maxLatitude
AND longitude BETWEEN :minLongitude AND :maxLongitude

먼저 인덱스를 활용해 후보를 줄인 뒤, 후보 데이터에 대해서만 ST_Distance_Sphere()를 사용해 실제 반경 안에 들어오는 숙소인지 판별했다.


🧐 경계에 있는 숙소가 후보에서 빠질 수도 있었다

Bounding Box를 계산하면서 한 가지 더 고려해야 할 부분이 있었다.

Bounding Box를 계산할 때 사용하는 지구 반지름과 MySQL의 ST_Distance_Sphere()가 내부적으로 사용하는 기본 반지름이 다르면, 검색 반경 경계에 위치한 숙소의 포함 여부가 미세하게 달라질 가능성이 있었다.

그래서 Bounding Box 계산에 사용하는 반지름을 MySQL SRID 0 ST_Distance_Sphere()의 기본 반지름과 일치시켰다.

이를 통해 Bounding Box 단계에서 실제 검색 반경에 포함되는 숙소가 후보에서 누락될 가능성을 줄였다.


🚀 성능 결과

k6를 이용해 동일한 데이터와 조건에서 비교했다.

기존 MySQL 거리 계산

평균 응답시간 : 573.62ms
p50          : 508.77ms
p95          : 1133.05ms
p99          : 1438.76ms
처리량        : 17.75 RPS

Bounding Box + Index

평균 응답시간 : 91.69ms
p50          : 90.98ms
p95          : 123.69ms
p99          : 180.91ms
처리량        : 109.04 RPS

결과적으로

평균 응답시간
573.62ms → 91.69ms
84.01% 감소

처리량
17.75 RPS → 109.04 RPS
6.14배 향상

이라는 결과를 얻었다.


🔍 그런데 Elasticsearch가 더 빠르지 않을까?

위치 검색을 구현하면서 Elasticsearch의 geo_point를 이용한 위치 검색도 별도로 구현해 비교했다.

결과는 예상대로 Elasticsearch가 훨씬 빨랐다.

Elasticsearch 위치 검색

평균 응답시간 : 6.12ms
처리량        : 1586.91 RPS

세 가지 방식을 비교하면 다음과 같다.

MySQL Baseline     573.62ms
Bounding Box        91.69ms
Elasticsearch        6.12ms

성능만 본다면 Elasticsearch를 선택하는 것이 당연해 보였다.

하지만 실제 서비스에 기술을 적용할 때는 속도만 볼 수는 없었다.


🤔 가장 빠른 기술이 항상 가장 좋은 선택일까?

Elasticsearch를 Production에 도입하면 검색 성능은 크게 향상된다.

하지만 그만큼 관리해야 하는 요소도 늘어난다.

MySQL 데이터 변경
        ↓
Elasticsearch 동기화
        ↓
두 저장소 사이 정합성 관리

Elasticsearch 자체 장애 대응과 인덱스 운영 역시 추가로 필요하다.

반면 현재 RoomPick의 데이터 규모에서는 Bounding Box를 적용한 MySQL만으로도 평균 응답시간을 약 91ms까지 낮출 수 있었다.

그래서 현재 규모에서는

새로운 검색 엔진을 추가하기보다 기존 MySQL을 충분히 최적화하는 것이 적절하다.

고 판단했다.

최종적으로 Production에서는

Bounding Box
+
(latitude, longitude) 복합 인덱스
+
ST_Distance_Sphere() 정확 거리 계산

구조를 선택했다.

Elasticsearch 구현 자체는 제거하지 않고 향후 데이터 규모가 커질 경우 검색 엔진을 변경할 수 있도록 구조를 남겨두었다.


💡 이번 개선에서 배운 점

처음에는 단순히

"Elasticsearch를 사용하면 훨씬 빠르지 않을까?"

라는 생각에서 시작했다.

실제로 테스트 결과 Elasticsearch가 가장 빨랐다.

하지만 이번 경험을 통해 가장 빠른 기술과 현재 서비스에 가장 적절한 기술은 다를 수 있다는 것을 배웠다.

약 5만 건 규모에서는 MySQL Bounding Box만으로도

573.62ms → 91.69ms

까지 충분히 개선할 수 있었다.

성능 수치만 비교하는 것이 아니라 서비스 규모, 운영 복잡도, 데이터 정합성, 향후 확장성까지 함께 고려해 기술을 선택해본 경험이었다.

profile
끝까지 가면 내가 다 이겨

0개의 댓글