Geospatial — 위치 기반 검색 구현하기
"내 주변 1km 이내의 식당을 찾아줘" — 이 기능을 데이터베이스 없이 Redis만으로 구현할 수 있을까요?
개념 정의
Redis Geospatial은 **위도(latitude)**와 경도(longitude) 좌표를 저장하고, 반경 검색이나 거리 계산을 수행하는 기능입니다. 내부적으로는 Sorted Set에 Geohash 인코딩 값을 score로 저장하는 방식으로 구현되어 있습니다.
왜 필요한가
위치 기반 검색은 많은 서비스에서 핵심 기능입니다.
- 배달 앱: 내 주변 음식점 검색
- 택시 호출: 가까운 기사 매칭
- 소셜 서비스: 근처 사용자 탐색
- 부동산: 특정 지역 내 매물 검색
RDBMS의 공간 인덱스보다 간단하고 빠른 위치 검색이 필요할 때 Redis Geospatial이 유용합니다.
기본 명령어
GEOADD — 위치 추가
# GEOADD key longitude latitude member
GEOADD restaurants 126.977 37.5665 "서울시청"
GEOADD restaurants 127.0276 37.4979 "강남역"
GEOADD restaurants 126.9784 37.5696 "광화문"
# 여러 위치 한 번에 추가
GEOADD restaurants \
127.0596 37.5085 "삼성역" \
126.9368 37.5553 "홍대입구" \
127.0146 37.5052 "역삼역"
# 옵션 (6.2+)
GEOADD restaurants NX 127.0 37.5 "new_place" # 없을 때만 추가
GEOADD restaurants XX 127.0 37.5 "서울시청" # 있을 때만 업데이트
GEOPOS — 좌표 조회
GEOPOS restaurants "서울시청" "강남역"
# 1) 1) "126.97700"
# 2) "37.56650"
# 2) 1) "127.02760"
# 2) "37.49790"
# 존재하지 않는 멤버
GEOPOS restaurants "없는곳"
# 1) (nil)
GEODIST — 두 지점 간 거리
# 기본 단위: 미터
GEODIST restaurants "서울시청" "강남역"
# "8043.1234" (약 8km)
# 단위 지정: m(미터), km(킬로미터), mi(마일), ft(피트)
GEODIST restaurants "서울시청" "강남역" km
# "8.0431"
GEODIST restaurants "서울시청" "광화문" m
# "358.9876" (약 359m)
GEOHASH — Geohash 문자열 조회
GEOHASH restaurants "서울시청" "강남역"
# 1) "wydm6de0460" (11자 Geohash)
# 2) "wydm78xf0z0"
GEOSEARCH — 범위 검색 (6.2+)
GEOSEARCH는 이전의 GEORADIUS/GEORADIUSBYMEMBER를 대체하는 통합 명령어입니다.
원형 검색 (BYRADIUS)
# 서울시청 기준 5km 반경 내 검색
GEOSEARCH restaurants FROMMEMBER "서울시청" BYRADIUS 5 km ASC
# 좌표 기준 검색
GEOSEARCH restaurants FROMLONLAT 126.977 37.5665 BYRADIUS 5 km ASC
# 옵션
GEOSEARCH restaurants FROMMEMBER "서울시청" BYRADIUS 10 km \
ASC \ # 가까운 순 정렬 (DESC: 먼 순)
COUNT 5 \ # 최대 5개
WITHCOORD \ # 좌표 포함
WITHDIST # 거리 포함
# 결과 예시:
# 1) "광화문"
# 1) "0.3590" (거리 km)
# 2) 1) "126.97840" (경도)
# 2) "37.56960" (위도)
# 2) "홍대입구"
# 1) "2.8430"
# 2) ...
사각형 검색 (BYBOX)
# 서울시청 중심 가로 10km × 세로 5km 사각형 검색
GEOSEARCH restaurants FROMMEMBER "서울시청" BYBOX 10 5 km ASC \
WITHCOORD WITHDIST COUNT 10
GEOSEARCHSTORE — 결과를 새 키에 저장
# 검색 결과를 새로운 Sorted Set에 저장
GEOSEARCHSTORE nearby restaurants \
FROMLONLAT 126.977 37.5665 BYRADIUS 3 km ASC COUNT 10
# 결과 확인 (일반 Sorted Set 명령어 사용)
ZRANGE nearby 0 -1 WITHSCORES
# STOREDIST: score에 거리를 저장
GEOSEARCHSTORE nearby restaurants \
FROMLONLAT 126.977 37.5665 BYRADIUS 3 km ASC STOREDIST
Geohash 인코딩의 원리
공간 분할
Geohash는 지구 표면을 재귀적으로 2등분하여 인코딩합니다.
1단계: 경도를 2등분
서쪽(-180~0): 0, 동쪽(0~180): 1
→ 서울(127°E) → 1
2단계: 위도를 2등분
남쪽(-90~0): 0, 북쪽(0~90): 1
→ 서울(37.5°N) → 1
3단계: 경도를 다시 2등분
(0~90): 0, (90~180): 1
→ 서울(127°) → 1
... 52비트까지 반복
결과 비트열: 경도 비트와 위도 비트를 교대로 배치
경도: 1 1 0 1 ...
위도: 1 0 1 1 ...
Geohash: 1 1 1 0 0 1 1 1 ...
^ ^ ^ ^
경 위 경 위
핵심 특성: 공간적 지역성
Geohash prefix가 같을수록 가까운 지점
wydm6 → 서울 시청 부근 (~5km × 5km 영역)
wydm → 서울 전체 (~20km × 20km 영역)
wyd → 서울/경기 (~80km × 80km 영역)
이 특성 덕분에 Sorted Set의 범위 쿼리로 근접 검색이 가능
Redis에서의 Geohash
Redis는 52비트 Geohash를 Sorted Set의 score(double)로 저장
→ ZRANGEBYSCORE로 특정 Geohash 범위를 빠르게 검색
→ O(log N + M) — N: 전체 멤버 수, M: 결과 수
Sorted Set과의 호환
Geospatial은 Sorted Set이므로 일반 명령어를 그대로 사용할 수 있습니다.
# 멤버 수 확인
ZCARD restaurants
# 멤버 삭제
ZREM restaurants "삼성역"
# 멤버 존재 확인
ZSCORE restaurants "서울시청"
# Geohash 값(score)이 반환됨
# SCAN
ZSCAN restaurants 0 MATCH "*역"
실전 패턴
패턴 1: 주변 가게 검색 API
def add_store(store_id, lng, lat, name):
"""가게 위치 등록"""
r.geoadd('stores', lng, lat, f"{store_id}:{name}")
def search_nearby(lng, lat, radius_km=3, count=20):
"""주변 가게 검색"""
results = r.geosearch(
'stores',
longitude=lng,
latitude=lat,
radius=radius_km,
unit='km',
sort='ASC',
count=count,
withcoord=True,
withdist=True
)
stores = []
for item in results:
member = item[0] # "store_id:name"
dist = item[1] # 거리 (km)
coord = item[2] # (lng, lat)
store_id, name = member.split(':', 1)
stores.append({
'id': store_id,
'name': name,
'distance_km': float(dist),
'lng': float(coord[0]),
'lat': float(coord[1])
})
return stores
패턴 2: 실시간 위치 추적 (택시, 배달)
def update_driver_location(driver_id, lng, lat):
"""기사 위치 업데이트"""
r.geoadd('drivers:active', lng, lat, driver_id)
r.expire('drivers:active', 300) # 5분 TTL (전체 키)
def find_nearest_drivers(lng, lat, count=5):
"""가장 가까운 기사 찾기"""
results = r.geosearch(
'drivers:active',
longitude=lng,
latitude=lat,
radius=10,
unit='km',
sort='ASC',
count=count,
withdist=True
)
return [(driver_id, float(dist)) for driver_id, dist in results]
def get_driver_distance(driver_id, customer_lng, customer_lat):
"""기사와 고객 간 거리"""
# 임시로 고객 위치를 추가하여 거리 계산
r.geoadd('temp:dist', customer_lng, customer_lat, 'customer')
r.geoadd('temp:dist', *r.geopos('drivers:active', driver_id)[0], driver_id)
dist = r.geodist('temp:dist', driver_id, 'customer', 'km')
r.delete('temp:dist')
return float(dist) if dist else None
패턴 3: 지오펜싱 (Geofencing)
def setup_geofence(fence_id, lng, lat, radius_km):
"""지오펜스 등록"""
r.hset(f"geofence:{fence_id}", mapping={
'lng': lng, 'lat': lat, 'radius': radius_km
})
def check_geofence(fence_id, user_lng, user_lat):
"""사용자가 지오펜스 안에 있는지 확인"""
fence = r.hgetall(f"geofence:{fence_id}")
if not fence:
return False
# 임시 키에 두 지점을 추가하여 거리 계산
temp_key = f"temp:fence:{fence_id}"
r.geoadd(temp_key,
float(fence['lng']), float(fence['lat']), 'center',
user_lng, user_lat, 'user')
dist = r.geodist(temp_key, 'center', 'user', 'km')
r.delete(temp_key)
return float(dist) <= float(fence['radius'])
성능과 제약
시간 복잡도
| 명령어 | 시간 복잡도 | 비고 |
|---|---|---|
| GEOADD | O(log N) | Sorted Set 삽입 |
| GEODIST | O(1) | score에서 좌표 복원 후 계산 |
| GEOSEARCH | O(N+log(N)) | N은 결과 수 |
| GEOPOS | O(1) | score에서 좌표 복원 |
제약사항
1. 좌표 범위: 경도 -180~180, 위도 -85.05~85.05
→ 남/북극 근처 지원 불가
2. 거리 계산: Haversine 공식 (지구를 구로 가정)
→ 최대 ~0.5% 오차 (지구는 완벽한 구가 아님)
3. 52비트 Geohash 정밀도
→ 약 0.6mm 수준의 좌표 정밀도
→ 실용적으로는 충분
4. 고도(altitude) 미지원
→ 2D 좌표만 저장 가능
대용량 처리 시 주의
# 멤버가 수백만 개일 때 넓은 반경 검색은 비용이 큼
# COUNT로 결과 수를 제한하는 것이 중요
GEOSEARCH stores FROMLONLAT 127 37.5 BYRADIUS 100 km COUNT 50 ASC
# ANY 옵션 (6.2+): COUNT에 도달하면 즉시 반환
# 정렬하지 않으므로 가장 가까운 순서가 아닐 수 있음
GEOSEARCH stores FROMLONLAT 127 37.5 BYRADIUS 100 km COUNT 50 ANY
정리
Redis Geospatial은 위치 기반 검색을 간단하게 구현할 수 있는 도구입니다.
- 내부적으로 Sorted Set + Geohash를 사용하여 일반 Sorted Set 명령어와 호환됩니다
- Geohash의 공간적 지역성 덕분에 범위 검색이 O(log N + M)으로 효율적입니다
- GEOSEARCH로 원형/사각형 범위 검색, 거리순 정렬, 좌표/거리 포함 등 다양한 옵션을 제공합니다
- 거리 계산은 Haversine 공식을 사용하며 약 0.5% 오차가 있습니다
- 주변 가게 검색, 실시간 위치 추적, 지오펜싱 등에 적합합니다
- 복잡한 공간 쿼리(다각형 검색, 경로 탐색 등)가 필요하면 PostGIS 같은 전용 공간 DB를 고려해야 합니다
댓글 로딩 중...