[Spring] Redis Sorted Set, ZSet 을 이용한 매칭 시스템 구현하기

2025. 10. 15. 17:21·Backend/Spring

1. 배경 및 목표

고객과 매니저를 매칭하는 서비스를 개선하고자 했습니다.
매칭 시스템은 고객의 상황에 맞는 매니저를 추천하여 연결해주는 기능을 담당합니다.
고객은 매칭 페이지에서 새로고침 형태로 여러 매니저를 추천받으며, 그 중 적절한 매니저를 선택할 수 있습니다.


기존 구현에서는 새로고침 시 같은 매니저가 다시 노출되지 않도록 매칭 정보를 DB에 저장했습니다.
하지만 이 방식에는 두 가지 문제가 있었습니다.

  1. 한 번이라도 추천된 매니저는 다시는 추천되지 않기에, 새로운 매니저가 추가되기 전까지 고객이 매칭을 다시 시도할 수 없음
  2. 매칭 기록이 DB에 계속 누적되어 불필요한 데이터가 쌓임

저는 이러한 서비스를 캐시 + 우선순위 큐로 개선할 수 있겠다고 생각하였습니다. 

그 이유는 다음과 같습니다.

  • 매칭이 종료되면 기록을 휘발시켜 다음 매칭 시에도 새롭게 추천 가능
  • 고객 조건에 맞는 매니저를 우선순위 기반으로 정렬하여 먼저 추천

이에 캐시용도로 자주 사용하던 Redis의 자료구조를 살펴보았고, Sorted Set(ZSet)이 매칭 시스템에 적합하다고 판단해 적용하였습니다.

2. 개념

1) Redis Sorted Set의 특징

Redis의 Sorted Set(ZSet)은 member와 score 쌍으로 구성된 정렬 가능한 집합입니다.

각 요소는 추가되면 score 기준으로 자동 정렬되며, 같은 score를 가진 경우에는 member의 사전순으로 정렬됩니다.

이러한 구조 덕분에 Sorted Set에는 다음과 같은 장점이 있습니다.

  • 자동정렬: 별도의 로직 없이 score 기준으로 자동 정렬 -> 우선순위 큐처럼 활용 가능
  • 범위 조회: 특정 순위 범위(ZRANGE)나 점수 범위(ZRANGEBYSCORE)로 빠르게 조회
  • 순위 계산: 특정 member의 순위를 바로 조회 가능

Sorted Set은 내부적으로 SkipList + HashTable 로 구현되어 있습니다. (zip 리스트는 일단 넘어가겠습니다.)

SkipList는 score 순으로 빠른 탐색과 정렬을 담당하고 HashTable은 member의 빠른 접근을 담당합니다.

이에 Sorted Set의 대부분 연산은 O(logN) 수준으로 수행됩니다.

SkipList: 정렬된 상태를 유지하면서 데이터를 삽입, 삭제하고 탐색할 수 있는 데이터 구조체

 

이러한 특징들을 활용하여 매니저에게 각 고객 맞춤형 우선순위를 주입하여 추천하고, TTL을 통해 만료시켜서 매칭시스템을 개선하고자 하였습니다.

 


2) 주요 명령어

redis에서 활용 가능한 주요 명령어들은 공식 문서에 자세하게 나와있습니다.

 

Redis sorted sets

Introduction to Redis sorted sets

redis.io

 



저는 제가 적용한 Spring에서 사용하는 메서드를 간단하게 살펴보겠습니다.



3. 구현

매칭 시스템에서는 Redis의 SortedSet 연산을 단일 클래스에서 관리하기 위해 RedisZSetRepository라는 이름의 Repository를 구현하였습니다.

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Repository;

import java.time.Duration;
import java.util.Set;

@RequiredArgsConstructor
@Repository
public class RedisZSetRepository {

    private final RedisTemplate<String, Object> redisTemplate;

    public void createSortedSet(String key, Set<ZSetOperations.TypedTuple<Object>> tuples) {
        redisTemplate.opsForZSet().add(key, tuples);
    }

    public boolean existsSortedSet(String key) {
        Long size = redisTemplate.opsForZSet().size(key);
        return size != null && size > 0;
    }

    public Double getHighestScore(String key) {
        Set<ZSetOperations.TypedTuple<Object>> topOne = redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 0);
        if (topOne != null && !topOne.isEmpty()) {
            return topOne.iterator().next().getScore();
        }
        return null;
    }

    public Set<Object> getSortedSetByRange(String key, long endIndex) {
        return redisTemplate.opsForZSet().reverseRange(key, 0, endIndex);
    }

    public void removeFromSortedSet(String key, Object value) {
        redisTemplate.opsForZSet().remove(key, value);
    }

    public void setExpire(String key, long seconds) {
        redisTemplate.expire(key, Duration.ofSeconds(seconds));
    }
}

 

redisTemplate.opsForZSet()으로 ZSet 연산을 수행할 수 있지만, 매번 같은 코드가 반복되는게 싫어서 별도의 클래스로 추출했습니다.

위 코드의 주요역할은 다음과 같습니다.

  • 기존 Redis 관련 로직과 분리하여 Redis ZSet 관련 연산을 한곳에서 관리
  • MatchingService에서 redisZSetRepository만 호출하면 생성, 조회, 삭제 모두 처리 가능

사실 이 부분은 취향 차이라고 생각하기에 편하신대로 구현하면 될 것 같습니다.


아래는 MatchingService에 적용한 예시입니다.

private void initializeManagerPool(String key, MatchingRequest request) {
        List<AccountManager> managerList = matchingQueryRepository.getManagerForMatching();
        Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
        for (AccountManager accountManager : managerList) {
            tuples.add(new DefaultTypedTuple<>(accountManager.getAccount().getId(), calculateManagerScore(accountManager, request)));
        }

        redisZSetRepository.createSortedSet(key, tuples);
        redisZSetRepository.setExpire(key, POOL_TTL_SECONDS);
    }

 

여기서 조회한 값들은 적절한 처리를 통해 사용할 수 있습니다.

 

 

참고

 

Redis sorted sets

Introduction to Redis sorted sets

redis.io

 

Redis Sorted Set을 이용한 랭킹 관리

서비스에서 어떤 종류의 랭킹 정보를 제공해야 한다면 기술적으로는 어떤 방법들이 있을까? 가장 먼저 떠오르는 방법은 RDB에 쿼리로 조회해서 제공하는 방법이다.

medium.com

 

 

Redis SKIP List of ZSETS(SORTED SETS)

internal_skiplist Redis SKIP List of  ZSETS (SORTED SETS) SKIP LIST Real Time Sorting Algorithm 이 글은 이런 의문에서 시작했습니다. Sorted Set은 데이터가 정렬되어 저장된다.   그래서 키에 저장되는 멤버수가 많아

redisgate.kr

 

'Backend > Spring' 카테고리의 다른 글

[Spring] 1 대 1 실시간 채팅 구현하기 - Stomp, MongoDB, Redis  (0) 2026.01.24
[Spring] 로컬, AWS LightSail에서 AWS parameter store로 환경변수 관리하기  (0) 2025.07.19
[Spring] Spring AOP의 동작원리, JDK Dynamic Proxy와 CGLIB  (1) 2025.04.28
[Spring] Apache.commons.exec 사용, 외부 명령어 실행 API 만들기, Java에서 Shell 사용  (0) 2024.05.26
[WebSocket] Spring, React, Stomp로 실시간 채팅, 저장 구현하기  (12) 2024.02.19
'Backend/Spring' 카테고리의 다른 글
  • [Spring] 1 대 1 실시간 채팅 구현하기 - Stomp, MongoDB, Redis
  • [Spring] 로컬, AWS LightSail에서 AWS parameter store로 환경변수 관리하기
  • [Spring] Spring AOP의 동작원리, JDK Dynamic Proxy와 CGLIB
  • [Spring] Apache.commons.exec 사용, 외부 명령어 실행 API 만들기, Java에서 Shell 사용
단군왕건영
단군왕건영
널리 세상을 이롭게 하고 싶은 개발자
  • 단군왕건영
    홍익인간 개발자
    단군왕건영
  • 전체
    오늘
    어제
    • 분류 전체보기 (81) N
      • TroubleShooting (14)
      • Backend (11) N
        • Java (2)
        • Spring (7) N
        • JPA (2)
      • DB (1)
      • Algorithm (7)
        • 백준 (4)
      • Frontend (0)
        • React (0)
      • Infra (3)
      • CS (37)
        • 컴퓨터구조 (25)
        • 네트워크 (12)
      • Git (3)
      • Mac (2)
      • 회고 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    MariaDB
    docker
    java
    네트워크
    컴퓨터 구조
    백준
    spring
    컴퓨터구조
    Jenkins
    springboot
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
단군왕건영
[Spring] Redis Sorted Set, ZSet 을 이용한 매칭 시스템 구현하기
상단으로

티스토리툴바