OhDelviery 프로젝트의 라이더 알림 기능을 구현하던 중 주변의 라이더를 조회하는 로직에 대한 성능 테스트를 진행해보기로 하였다. 해당 로직은 배달 이벤트 발생 시 가게 반경 10km이내의 라이더들을 조회해오는 로직으로 1. DB에서 현재 상태가 배달 가능인 라이더들을 모두 조회한 뒤, 가게에서 라이더의 현재 위치까지의 거리를 각각 계산하고 필터를 걸어 최종 결과 리스트를 반환하는 방식과 2. 배달 가능한 라이더만 올려놓은 Redis에서 Redis GEO를 통해 설정한 거리 이내의 라이더 리스트를 반환하는 방식으로 구현하였다.
유의미한 결과의 테스트를 위해서 가게 위치 주변에 3000명의 라이더가 있는 것으로 가정하고 mock 데이터를 만들어서 테스트하였다.
private List<Rider> getRidersWithDB(Double sLat, Double sLon) {
// 상태가 AVAILABLE인 라이더 DB에서 전체 조회
List<Rider> riders = riderRepository.findAllByStatus(RiderStatus.AVAILABLE);
// 조회해온 라이더 중 현재 위치가 가게에서 반경 10KM 이내인 라이더 필터링 (위경도로 계산)
riders = riders.stream()
.filter(rider -> {
double dis = calculateDistance(sLat, sLon, rider.getLatitude(), rider.getLongitude());
return dis <= 10.0;
})
.toList();
return riders;
}
Redis Geo로 반경 10km 이내의 라이더 조회 후, 조회한 라이더 ID 리스트로 DB에서 라이더 정보 조회
private List<Rider> getRidersWithRedis(Double sLat, Double sLon) {
// Redis GEO로 라이더 조회
GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisRiderLocRepository
.searchByLoc(sLon, sLat);
List<Long> riderIds = results.getContent().stream()
.map(geoLocation -> geoLocation.getContent().getName())
.map(Long::valueOf)
.toList();
// 조회한 라이더 ID 리스트로 DB에서 라이더 정보 조회
return riderRepository.findAllByRiderIdIn(riderIds);
}
public GeoResults<GeoLocation<String>> searchByLoc(Double lon, Double lat) {
return redisTemplate.opsForGeo().search(
RIDER_LOC_PREFIX,
GeoReference.fromCoordinate(lon, lat),
new Distance(10.0, RedisGeoCommands.DistanceUnit.KILOMETERS)
);
}
Redis Geo를 활용하는 것이 DB 조회보다 빠를 것이라고 예상했는데 반대의 결과가 나옴. 라이더 id 리스트로 다시 DB에서 조회해오는 로직 때문일 것이라고 생각하고 라이더의 정보도 레디스에 저장한 뒤 테스트를 진행해보기로 함.
Redis Geo로 반경 10km 이내의 라이더 조회 후, 조회한 라이더 ID 리스트로 Redis에서 라이더 정보 조회
private List<Rider> getRidersWithRedis(Double sLat, Double sLon) {
GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisRiderLocRepository
.searchByLoc(sLon, sLat);
List<String> riderIds = results.getContent().stream()
.map(geoLocation -> geoLocation.getContent().getName())
.toList();
// 라이더 정보를 DB가 아닌 Redis에서 조회
List<Rider> riders = new ArrayList<>();
for (String riderId : riderIds) {
Rider rider = redisRiderLocRepository.getRiderInfo(riderId);
riders.add(rider);
}
return riders;
}
라이더 테이블을 한 번 밀고 다시 저장했더니 위치가 조금 달라져서 조회해오는 라이더 수도 좀 달라졌지만 30명 차이라 별로 상관없을듯
근데 이 로직은 조회 시간이 엄청나게 늘어버리는 문제 발생...
로직을 다시 살펴보면
public Rider getRiderInfo(String riderId) {
String riderInfo = redisTemplate.opsForValue().get(RIDER_INFO_PREFIX + riderId);
라이더 한 명마다 매번 조회를 하고 있어서 사실상 3000번의 조회가 일어나고 있으므로 당연히 성능 저하가 발생한다...
mget을 통해 멀티 조회를 하는 방식으로 변경해보자
public List<Rider> getRidersInfo(List<String> riderIds) {
List<String> keys = riderIds.stream()
.map(id -> RIDER_INFO_PREFIX + id)
.toList();
List<String> riderInfos = redisTemplate.opsForValue().multiGet(keys);
List<Rider> riders = new ArrayList<>();
for (String riderInfo : riderInfos) {
if (riderInfo != null) {
try {
riders.add(objectMapper.readValue(riderInfo, Rider.class));
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to deserialize rider info", e);
}
}
}
return riders;
}
이렇게 하니 시간이 줄긴했는데 결론적으로 맨 처음의 DB조회랑 별로 차이가 안남.
왜 이런 결과가 나타날까 궁금해서 Redis GEO로 라이더의 id리스트만 조회하는 부분을 속도 측정했더니 평균 10ms 정도로 빠른 속도를 보였다. 결국 라이더 정보 조회를 위해 한 번 더 레디스나 DB에 왔다 갔다 하면서 총 두 번의 I/O가 일어나기 때문에, 한 번의 I/O로 처리하는 맨 처음의 로직과 성능의 차이가 별로 없었던 듯 하다.
다만, 지금의 테스트는 라이더의 위치 정보가 고정적인 상태로 진행되었기 때문에, 실제의 서비스 환경과는 좀 다를 것 같아서 라이더의 위치 정보가 실시간으로 변하고 있는 상황에서 2차 테스트를 다시 진행해보도록 하자.