Redis GEO 공간 데이터 활용: 위치 기반 서비스 구현
Redis GEO 명령어를 활용한 위치 기반 서비스 구현 방법과 최적화 기법을 설명합니다.
Redis GEO 공간 데이터 활용
Introduction
Redis GEO는 위도와 경도 좌표를 저장하고 검색할 수 있는 공간 데이터 타입입니다. Geohash 기반 인덱싱을 사용하여 근처 장소 검색, 거리 계산, 영역 내 요소 검색 등을 효율적으로 수행할 수 있습니다. 위치 기반 서비스를 구현할 때 Redis GEO는 높은 성능과 간편한 API를 제공합니다. 이 글에서는 Redis GEO의 활용 방법을 다루겠습니다.
Environment
# Redis GEO 명령어 테스트
redis-cli
# Redis 버전 확인 (3.2 이상 필요)
redis-cli INFO server | grep redis_version// Node.js Redis 클라이언트
const Redis = require('ioredis');
const redis = new Redis();Problem
위치 기반 검색의 성능 문제:
// 기존 방식: 거리 계산을 위한 전체 스캔
async function findNearbyLocations(lat, lng, radiusKm) {
const allLocations = await redis.hgetall('locations');
const nearby = [];
for (const [id, data] of Object.entries(allLocations)) {
const location = JSON.parse(data);
const distance = calculateDistance(lat, lng, location.lat, location.lng);
if (distance <= radiusKm) {
nearby.push({ id, ...location, distance });
}
}
return nearby.sort((a, b) => a.distance - b.distance);
}
// 거리 계산 함수 (Haversine formula)
function calculateDistance(lat1, lng1, lat2, lng2) {
const R = 6371; // 지구 반지름 (km)
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng/2) * Math.sin(dLng/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
// 성능 문제: 10만 개 위치에서 1km 내 검색 = 2.3초# Redis에서 위치 데이터 저장 (비효율적)
HSET locations:1 lat 37.5665 lng 126.9780 name "서울시청"
HSET locations:2 lat 37.5512 lng 126.9882 name "남산타워"
# ... 10만 개 위치Analysis
Redis GEO의 내부 구조를 분석했습니다:
// Redis GEO 명령어 확인
await redis.geoadd('stores', 126.9780, 37.5665, 'seoul_city_hall');
await redis.geoadd('stores', 126.9882, 37.5512, 'namsan_tower');
await redis.geoadd('stores', 127.0276, 37.4979, 'gangnam_station');
// Geohash 인덱스 확인
const geohash = await redis.geohash('stores', 'seoul_city_hall');
console.log('Geohash:', geohash);
// Geohash: wydgm0
// GEO 데이터 구조
// Redis는 Sorted Set을 사용하여 GEO 데이터 저장
// 멤버: geohash + score (위도/경도 인코딩)// GEO 검색 성능 비교
console.time('geo_search');
const nearby = await redis.geosearch(
'stores',
'FROMLONLAT', 126.9780, 37.5665,
'BYRADIUS', 5, 'km',
'ASC',
'COUNT', 10
);
console.timeEnd('geo_search');
// geo_search: 0.5ms (기존 대비 4600배 빠름!)Solution
1단계: 기본 GEO 연산
// 위치 데이터 저장
async function addLocation(key, lat, lng, name, metadata = {}) {
await redis.geoadd(key, lng, lat, name);
// 메타데이터 별도 저장
await redis.hset(`metadata:${key}:${name}`, {
name: name,
lat: lat.toString(),
lng: lng.toString(),
...metadata
});
return { success: true, name: name };
}
// 예시: 매장 데이터 추가
await addLocation('stores', 37.5665, 126.9780, '서울시청점', {
address: '서울특별시 중구 세종대로 110',
phone: '02-1234-5678',
category: '카페'
});
// 근처 매장 검색
async function findNearbyStores(lat, lng, radiusKm, limit = 10) {
const results = await redis.geosearch(
'stores',
'FROMLONLAT', lng, lat,
'BYRADIUS', radiusKm, 'km',
'ASC',
'COUNT', limit,
'WITHCOORD',
'WITHDIST'
);
return results.map(([name, coordinates, distance]) => ({
name,
lat: parseFloat(coordinates[1]),
lng: parseFloat(coordinates[0]),
distance: parseFloat(distance)
}));
}
// 사용 예
const nearbyStores = await findNearbyStores(37.5665, 126.9780, 5);
console.log('5km 내 매장:', nearbyStores);2단계: 고급 GEO 기능
// 두 지점 간 거리 계산
async function calculateDistance(point1, point2) {
const distance = await redis.geodist(
'stores',
point1,
point2,
'km'
);
return parseFloat(distance);
}
// 특정 좌표 기반 매장 검색
async function findStoresInRectangle(
minLat, maxLat, minLng, maxLng
) {
// Redis GEO는 원형 검색만 지원하므로,
// 사각형 검색은 별도 구현 필요
const centerLat = (minLat + maxLat) / 2;
const centerLng = (minLng + maxLng) / 2;
const radius = Math.max(
Math.abs(maxLat - minLat),
Math.abs(maxLng - minLng)
) * 111; // 대략적인 km 변환
const candidates = await redis.geosearch(
'stores',
'FROMLONLAT', centerLng, centerLat,
'BYRADIUS', radius, 'km',
'COUNT', 1000,
'WITHCOORD'
);
// 사각형 범위로 필터링
return candidates.filter(([name, [lng, lat]]) => {
const latNum = parseFloat(lat);
const lngNum = parseFloat(lng);
return latNum >= minLat && latNum <= maxLat &&
lngNum >= minLng && lngNum <= maxLng;
});
}3단계: 실시간 위치 추적 시스템
// 실시간 위치 추적 시스템
class LocationTracker {
constructor(redis, key) {
this.redis = redis;
this.key = key;
}
async updateLocation(userId, lat, lng) {
// GEO 데이터 업데이트
await this.redis.geoadd(this.key, lng, lat, userId);
// 타임스탬프 저장 (마지막 업데이트 시간)
await this.redis.hset(`last_seen:${this.key}`, userId, Date.now());
// 위치 이력 저장
await this.redis.zadd(
`location_history:${this.key}:${userId}`,
Date.now(),
`${lng},${lat}`
);
}
async findNearbyUsers(lat, lng, radiusKm, excludeUserId = null) {
const nearby = await this.redis.geosearch(
this.key,
'FROMLONLAT', lng, lat,
'BYRADIUS', radiusKm, 'km',
'ASC',
'COUNT', 50,
'WITHCOORD',
'WITHDIST'
);
return nearby
.filter(([userId]) => userId !== excludeUserId)
.map(([userId, coordinates, distance]) => ({
userId,
lat: parseFloat(coordinates[1]),
lng: parseFloat(coordinates[0]),
distance: parseFloat(distance)
}));
}
async getUserLocation(userId) {
const coordinates = await this.redis.geopos(this.key, userId);
if (!coordinates || !coordinates[0]) {
return null;
}
return {
userId,
lat: parseFloat(coordinates[0][1]),
lng: parseFloat(coordinates[0][0])
};
}
}
// 사용 예
const tracker = new LocationTracker(redis, 'user_locations');
await tracker.updateLocation('user123', 37.5665, 126.9780);
await tracker.updateLocation('user456', 37.5512, 126.9882);
const nearbyUsers = await tracker.findNearbyUsers(37.5665, 126.9780, 1);
console.log('1km 내 사용자:', nearbyUsers);Lessons Learned
- GEO 데이터 타입: Redis GEO는 위도/경도를 Geohash로 변환하여 Sorted Set에 저장합니다
- 검색 성능:
GEOSEARCH명령은 O(log(N)+M) 시간 복잡도로 매우 빠릅니다 - 정확도 제한: Geohash는 격자 기반이므로 정확한 거리 계산은 별도로 수행해야 합니다
- 데이터 관리: GEO 데이터는 추가만 가능하고 업데이트는 불가능하므로 삭제 후 재추가가 필요합니다
- 메모리 고려: 대량의 GEO 데이터는 메모리 사용량이 많으므로 적절한 관리가 필요합니다
이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.