도입 배경
웨이팅 로직을 구현하며 웨이팅 취소, 지연, 입장에 따라 웨이팅 순서를 갱신해줘야 했다.
초기에는 웨이팅 순서를 DB에 저장하고, 아래의 상황들에서 벌크 연산으로 웨이팅 순서를 업데이트하고자 했다.
웨이팅 순서를 rank라고 했을 때
- 특정 고객이 웨이팅을 취소하거나 대기를 맨 뒤로 미룸 -> 해당 고객 뒤 고객들의 rank 1 감소시키기
- 고객 입장 -> 다른 고객들의 rank 1 감소시키기
rank는 실시간으로 변화하는 데이터라 매번 벌크 연산을 진행하고 DB에 반영하면 테이블 부하가 심해지는 문제가 있었다.
이에 Redis의 list 자료 구조를 활용해 대기열 큐를 구현하기로 하였다.
Redis는 인메모리 데이터베이스로 빠른 읽기, 쓰기 작업이 가능하다는 장점이 있다.
설정
Build.gradle
Spring Boot에서 Redis를 사용하려면 아래 의존성을 추가해줘야 한다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
RedisConfig
RedisConnectionFactory에서 yml에 정의한 host, port를 이용해 연결을 설정한다.
Spring Data Redis에서는 JedisConnectionFactory와 LettuceConnectionFactory가 일반적으로 사용된다.
이에 비동기 처리가 가능한 LettuceConnectionFactory를 활용해 Lettuce로 Redis와의 연결을 진행했다.
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
}
코드 사용
StringRedisTemplate
Redis에 값을 저장하고 접근하는 방식은 2가지이다.
하나는 RedisTemplate을 사용하는 방법과 다른 하나는 CrudRepository를 상속받은 RedisRepoository를 사용하는 방법이다.
여기서는 RedisTemplate을 활용했다.

RedisTemplate은 Redis 내부 데이터 접근을 도와주는 헬퍼 클래스이다.
Redis 타입별로 RedisTemplate에서 제공되는 메서드가 존재하는데, 리스트의 경우 opsForList()를 활용한다.
RedisTemplate은 클라이언트 대신 redis 공통 로직을 작성해 줘 redis를 편리하게 이용할 수 있다.
예를 들어, 레디스 서버와 연결 시 커넥션 풀을 대신 관리해 줘서 개발자는 직접 커넥션을 열고 닫을 필요가 없다.
또한, redis는 메모리 내에서 데이터를 byte 형식으로 저장하는데, redisTemplate이 제공하는 자바 Serializer를 통해 Redis 내 데이터를 직렬화/역직렬화할 수 있다.
StringRedisTemplate은 RedisTemplate을 확장한 클래스로 key, value가 문자열일 때 사용 가능하다.
이를 사용하면 Redis Config에 key serializer와 value serializer를 따로 설정해주지 않아도 자동으로 직렬화/역직렬화된다.
대기열 구현 로직
opsForList() Operation Interface를 활용해 리스트 연산을 수행하였다.
웨이팅 생성 시 왼쪽에 순차적으로 삽입하고, 입장(웨이팅 종료) 시 맨 오른쪽 데이터를 추출한다.

key 값은 shopId에 prefix "s"를 붙인 값이고, value는 waitingId를 문자열로 변환한 값이다.
public void save(Long shopId, Long waitingId) {
redisTemplate.opsForList().leftPush("s" + shopId, waitingId.toString());
}
public Long entry(Long shopId) {
String waitingId = redisTemplate.opsForList().rightPop("s" + shopId);
return Long.valueOf(waitingId);
}
가독성을 위해 실제 구현 코드에서 예외 처리 코드를 생략하였다.
웨이팅을 취소하는 경우 해당 키에 상응하는 리스트에서 값을 추출한다.
웨이팅 지연은 위 과정에서 추가로 추출한 값을 다시 삽입한다.
public void cancel(Long shopId, Long waitingId) {
redisTemplate.opsForList().remove("s" + shopId, 1, waitingId.toString());
}
public void postpone(Long shopId, Long waitingId){
redisTemplate.opsForList().remove(key, 1, waitingId.toString());
redisTemplate.opsForList().leftPush(key, waitingId.toString());
}
입장 순서에서 멀어질수록 리스트 앞쪽에 위치한다.
따라서 입장 순서대로 웨이팅 아이디를 추출하려면, 리스트의 요소들을 역순으로 출력해야 한다.
public List<Long> getShopWaitingIdsInOrder(Long shopId) {
List<String> waitingIds = redisTemplate.opsForList().range("s" + shopId, 0, -1);
Collections.reverse(waitingIds);
return new ArrayList<>(waitingIds.stream()
.map(Long::parseLong)
.toList());
}
public Long findRank(Long shopId, Long waitingId) {
Long index = redisTemplate.opsForList().indexOf("s" + shopId, waitingId.toString());
return getWaitingLineSize(shopId) - index;
}
스케줄러를 돌려서 매일 Redis 대기열을 비우는 로직이다.
s로 시작하는 모든 키를 검색해 set으로 반환하고 해당 키들을 통해 value값들을 삭제한다.
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public void clearRedis() {
Set<String> keys = redisTemplate.keys("s*");
redisTemplate.delete(keys);
}
아래처럼 특정 키의 값들만 삭제할 수도 있는데, 매 테스트 작성 후 데이터를 클리닝해주는 목적으로 사용했다.
redisTemplate.delete("s1");
//redisTemplate.getConnectionFactory().getConnection().flushAll(); //clear 안됨
결론
Redis를 List를 활용해 대기열을 구현해 보았다.
그 과정에서 사용되는 RedisTemplate, OpsForList 명령어를 학습할 수 있었다.
Reference
https://minholee93.tistory.com/entry/Redis-Transaction