[내 주변 사용자 찾기] 2. Redis Geospatial로 가까운 사용자 찾기

joheera·2024년 2월 6일
1
post-thumbnail

이번에는 Redis의 Geospatial을 활용하여 위치를 저장하고 가까운 사용자를 탐색하는 방법에 대해 알아보자.

Geospatial Index

먼저, Redis.io에서는 Redis Geospatial을 이렇게 설명하고 있다.

Redis Geospatial 인덱스를 사용하면 좌표를 저장하고 검색할 수 있습니다. 이 자료 구조는 주어진 반경이나 경계 상자 내에서 가까운 지점을 찾는데 유용합니다.

Redis Geospatial은 Redis의 sorted set 자료 구조를 사용하여 위치 정보를 저장한다. 이 때 내부적으로는 위도, 경도를 Geohash로 인코딩한 값을 저장하게 된다. Geohash에 대한 설명은 이전 포스팅에서 참고할 수 있다.

sorted set 자료 구조는 score을 활용하여 내부적으로 정렬하는 특징을 가지고 있다. Geospatial에서는 GeoHash 값을 Redis에서 별도로 변환하여 score로 저장한다.

Redis에서 Geohash를 직접 사용하여 위치를 저장하고 가까운 사용자를 탐색하는 로직을 작성할 수도 있지만, Redis Geospatial을 사용하면 제공되는 명령어를 사용하여 훨씬 간단하게 구현할 수 있다. 물론 In-memory인 Redis를 사용하게되므로 RDB를 사용했을 때보다 속도적인 면에서 성능적으로 이점도 얻을 수 있다.

Redis Cli

GEOADD

Redis Cli에서는 GEOADD 명령어를 사용하여 Key를 지정하고 경도, 위도, 이름을 저장할 수 있다.

GEOADD KEY longitude latitude "member"
GEOADD location 126.823162 35.191378 "Vino Cafe"

location이라는 key의 sorted set에 vino 카페의 geospatial 정보를 저장하는 명령어이다.

GEOSEARCH

프로젝트를 진행할 때는 GEORADIUSBYMEMBER 명령어를 사용해서 사용자 100m 반경 내에 있는 사용자를 조회했다. Redis 6.2.0부터는 GEORADIUSBYMEMBER이 deprecated 되었고, GEOSEARCH로 대체되었다고 한다.GEOSEARCH 명령어와 함께 FROMMEMBER, BYRADIUS 옵션을 사용하면 저장된 member 위치에서 주어진 반경 내에 있는 값들을 찾을 수 있다.

GEOSEARCH KEY FROMMEMBER "member" BYRADIUS 100 m  [ASC | DESC]
GEOSEARCH location FROMMEMBER "Vino Cafe" BYRADIUS 100 m ASC

명령어에 그 외 여러 가지 옵션을 붙여 추가적인 정보를 얻을 수 있다.

  • WITHDIST : 지정한 member 위치에서 조회된 데이터의 위치까지의 거리를 함께 반환한다.
  • WITHCOORD : 조회된 데이터의 경도와 위도를 함께 반환한다.
  • WITHHASH : 조회된 데이터의 GEOHASH 값을 함께 반환한다.
  • COUNT : COUNT N 옵션을 붙이면 지정된 member 위치에서 가까운 순으로 N개의 데이터를 반환한다. ANY를 함께 사용하면 거리와 상관없이 N개의 데이터를 반환한다.

Spring Data Radis

Spring에서는 Spring Data Radis를 통해 Geospatial을 사용할 수 있다.
Redis Geo를 활용해서 사용자 위치 저장100m 반경 안에 있는 사용자 탐색하는 로직을 작성해보자.

사용자 위치 저장하기

먼저 Redis에 Geo를 사용해서 사용자 위치를 저장하는 로직이다.

public void saveUserLocation(UserLocation userLocation) {
	// redis geo 자료구조 설정
    GeoOperations<String, String> geoOperations = redisTemplate.opsForGeo();

	String key = "geoPoints";
    Point point = new Point(userLocation.longitude(), userLocation.latitude());

	// redis에 geohash로 인코딩한 값 저장
   	geoOperations.add(key, point, userLocation.userName());
}

Spring Data Radis에서는 사용하고자 하는 자료구조에 맞게 RedisTemplate의 opsForXXX() 메서드를 호출하면 해당하는 XXXOperations 객체가 생성된다. XXXOperations 객체를 사용하여 Redis 명령어를 사용할 수 있다.

GEOADD 명령어는 GeoOperations의 add() 메서드를 호출하여 사용할 수 있다. 위도와 경도 값은 Point형으로 넘겨줘야 한다.

100m 반경 안에 있는 사용자 탐색

GEOSEARCH KEY FROMMEMBER "member" BYRADIUS 100 m  [ASC | DESC]

여러 옵션을 붙인 GEOSEARCH 명령어를 Java에서 사용하기 위해서 Spring 문서를 찾아보았다.

필요한 매개변수를 하나씩 살펴보자.

GeoReference : Java에서 GEOSEARCH 명령어에 사용되는 Reference Point를 제공하는 Interface이다. 탐색의 중심 위치 정보를 저장하는 객체라고 생각하면 쉽다.

Geo의 member를 매개변수로 GeoReference 객체를 생성하는 팩토리 메서드를 제공한다.

Distance : 지정한 측정 단위로 거리를 표현하는 값 객체이다.

GeoRadiusCommandArgs : COUNT, WITHDIST 등 추가적인 옵션을 함께 지정할 수 있다.

전체 구현 로직을 보면 이해하기가 더 쉬울 것 같다.

public List<NearbyUsers> findNearbyUsers(String userName) {
   
	// Geo에 저장된 사용자 이름으로 GeoReference 객체 생성
	GeoReference reference = GeoReference.fromMember(userName);

	// 사용자의 위치를 중심으로 반경 100m 범위 설정
	Distance radius = new Distance(100, RedisGeoCommands.DistanceUnit.METERS);

	// redis geo 명령어 옵션 설정, limit 13
	RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
                .newGeoRadiusArgs()
                .includeDistance()
                .includeCoordinates()
                .sortAscending()
                .limit(13);
                
    // redis geo 자료구조 설정
	GeoOperations<String, String> geoOperations = redisTemplate.opsForGeo();            

	// 설정한 위치 범위 안에 있는 사용자들 검색
	GeoResults<RedisGeoCommands.GeoLocation<String>> results = geoOperations
                .search("geoPoints", reference, radius, args);

	List<NearbyUsers> list = new ArrayList<>();

	// 범위 안에 있는 사용자에 대해 최근 재생 곡 조회
	for(GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) {
		RedisGeoCommands.GeoLocation<String> location = result.getContent();

		String nearbyUserName = location.getName();

		// 중심 좌표로부터의 거리를 기준으로 level 지정
		double distance = result.getDistance().getValue();

        list.add(NearbyUsers.builder()
                        .userName(nearbyUserName)
                        .distance(distance)
                        .build());
	}
    return list;

}

이렇게 해서 Spring Data Radis를 통해서 Radis Geo에 위치 정보를 저장하고, 주변 사용자들을 탐색하는 로직까지 정리해봤다! 단, Geospatial Index에서는 expire 기능을 제공하지 않으므로 서비스 요구사항에 맞게 위치 정보의 유효기간을 별도로 구현할 필요가 있다.

참고 자료

잘못 설명하고 있는 부분이 있다면 댓글로 남겨주세요!

0개의 댓글