#0 사용하게 된 계기와 도전🏆
팀 프로젝트 기간 중에 일정 시간이 지나면 경매 글의 상태를 경매 중에서 경매 완료 상태로 바꾸고 싶어서 이벤트 기반 트리거를 걸 수 있는 것을 알아보던 도중, Redis에도 이벤트 기반으로 만료된 키를 가지고 트리거를 거는 KeySpace Notifications를 발견했다.
잘 알려지지 않은만큼 자료가 많지 않아서🥶적용하는 데 애를 좀 먹었지만 적용되고 나니 이 기능에 대해 호기심을 가지고 사용해볼 분들에게 도움이 되고 싶어 찾고 적용한 과정을 글에 담아보려고 한다.
#1 Redis Keyspace Notifications가 뭐지?🤔
아마 Redis에 익숙하신 분들도 키가 변경되거나 만료됐을 때를 이벤트로 알림을 받을 수 있다는 것은 잘 모를 것이다.
Redis에는 Keyspace Notifications라는 기능을 간단히 정리하자면 다음과 같다.
=> Keyspace에서 발생하는 주요 이벤트를 기반으로 알림을 트리거하는 기능을 통해 다양한 유형의 이벤트에 대한 실시간 업데이트가 가능하다.
...
특이한 특징이 하나가 있는데 보통 key-value 형태로 저장되어 value값도 사용되기 마련인데 이건 key값만 유효하다.
Keyspace Notifications는 Redis 2.8.0부터 지원하는 기능이며
Redis Keyspace Notifications의 경우, 눈에 띌 만큼은 아니지만 CPU 리소스를 일부 사용한다. 그래서 성능적 오버헤드가 발생할 수 있기 때문에 기본적으로 활성화되어있지 않는 기능이다.
redis.conf 설정 파일을 살펴보면 맨 아랫부분(주황색 박스)에
'notify-keyspace-events' 옵션이 비활성화의 의미인 ""(빈 문자열)로 설정되어 있는 것을 볼 수 있다.
이 옵션이 기본적으로 K(=Keyspace events)혹은 E(Keyevent events) 중 하나를 설정해야 알림이 발송되는데 의미하는 말은 아래와 같다.
☑️ Keyspace events : 이벤트의 이름을 메세지로 수신하는 것
☑️ Keyevent events : 키의 이름을 메세지로 수신하는 것
☑️ Config set 부분(하늘색 박스) 해석 - 더보기 Click🖱️
K Keyspace events, published with __keyspace@<db>__ prefix.
// 이벤트가 발생했을 때, 메세지를 주고 받을 때 사용하는 channel 유형 중, 키 중심
E Keyevent events, published with __keyevent@<db>__ prefix.
// 이벤트가 발생했을 때, 메세지를 주고 받을 때 사용하는 channel 유형 중, 명령 중심
g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
// 공통 명령
$ String commands // 스트링 명령
l List commands // 리스트 명령
s Set commands // 셋 명령
h Hash commands // 해쉬 명령
z Sorted set commands // 소트 셋 명령
t Stream commands // 스트림 명령
x Expired events (events generated every time a key expires)
// 키가 만료될 때마다 생성되는 이벤트
e Evicted events (events generated when a key is evicted for maxmemory)
// Maxmemory 정책으로 키가 퇴출(삭제)될 때 생성되는 이벤트
A Alias for g$lshztxe, so that the "AKE" string means all the events.
// 모든 이벤트 ('AKE'로 지정하면 모든 이벤트를 받는다.)
...
여기서 주목해야할 점🔍
해당 기능을 구현할 때 notify-keyspace-events 값이 🌟이벤트에 대한 알림을 사용하지 않는 ""로 설정되어 있어도 spring boot에서 KeyExpirationEventMessageListener를 통해 이벤트 알림이 수신되게할 수 있는데🌟 아래의 구현하고 적용하는 과정에서 설명하겠다.
#2 적용하는 방법 🚀
spring boot 자체적으로 redis key expired event에 대한 알림을 수실할 수 있게 구현된 것이 KeyExpirationEventMessageListener이다.
가장 먼저 KeyExpirationEventMessageListener.class를 상속받은 RedisKeyExpiredListener이라는 클래스를 구현한다.
- RedisKeyExpiredListener.class
@Component
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener {
private final ApplicationEventPublisher applicationEventPublisher;
public RedisKeyExpiredListener(
RedisMessageListenerContainer listenerContainer,
ApplicationEventPublisher applicationEventPublisher
) {
super(listenerContainer);
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
public void onMessage(Message message, byte[] pattern) {
String messageToStr = message.toString();
if (messageToStr.startsWith("auctionId:")) {
Long auctionId = Long.parseLong(messageToStr.split(":")[1]);
applicationEventPublisher.publishEvent(new AuctionKeyExpiredEvent(auctionId));
}
}
}
코드를 설명하면 아래와 같다. 더보기 Click🖱️
1. Spring 애플리케이션 이벤트를 발행하는 데 사용되는 객체 'applicationEventPublisher'는 필드에 넣어서 키 만료 이벤트가 발생하면 이를 이용해서 특정 이벤트를 발행시키는 역할을 하도록 한다.
2. 그리고 생성자(RedisKeyExpiredListener)를 통해 부모 클래스의 생성자를 호출하고 초기화한다.
3. 상속받은 KeyExpirationEventMessageListener를 타고 가면 이것 또한 KeyspaceEventMessageListener 클래스를 상속받는데 이 안에 있는 onMessage 메서드를 오버라이드해서 키 만료 이벤트가 발생할 때 호출되도록 한다. 그 안의 내용은 다음과 같다.
message.toString()를 통해 Message 객체를 문자열로 변환하고 메세지가 "auctionId :"로 시작하는지 확인한다. auctionId : 뒤에 있는 숫자 부분을 추출해서 Long type으로 변환하고 AuctionKeyExpiredEvent 객체를 생성하고 applicationEventPublisher.publishEvent를 사용해서 이벤트를 발행한다.
auctionId를 보내주기 위한 클래스도 만들어주었다.
- AuctionKeyExpiredEvent.class
@Getter
@AllArgsConstructor
public class AuctionKeyExpiredEvent {
private Long auctionId;
}
AuctionEventHandler라는 것을 구현하여 이벤트 리스너를 따로 두었다.
만료된 이벤트를 listen하면 auctionService에 경매 중에서 경매 완료로 상태 변경하는 로직이 구현된 updateStateHold에 해당 auctionId를 보내주도록 되어있다.
- AuctionEventHandler.class
@Component
@RequiredArgsConstructor
public class AuctionEventHandler {
private final AuctionService auctionService;
@EventListener
public void AuctionKeyExpiredEvent(AuctionKeyExpiredEvent auctionKeyExpiredEvent){
auctionService.updateStatusToHold(auctionKeyExpiredEvent.getAuctionId());
}
}
그리고 경매 글이 만들어질 때, 키를 만료하는 시간(TTL)을 설정하였는데
- AuctionService.class
public void createAuction(AuctionRequestDto requestDto, Long userId) {
...비즈니스 로직 이하 생략
cacheService.setAuctionExpiredKey(auction.getId());
}
이전 글에 정리해두었듯이 redisTemplate를 가져다 쓰기 위해 필드에 바로 넣는 법 대신 기술 의존적인 메소드를 PubSubService로 인터페이스화 하였다.
- RedisService.class(PubSubService.interface)
@Slf4j(topic = "RedisService")
@RequiredArgsConstructor
@Component
public class RedisService implements PubSubService {
private final RedisTemplate<String, String> redisTemplate;
@Override
public void setAuctionExpiredKey(Long auctionId) {
ValueOperations<String, String> operations = redisTemplate.opsForValue();
String redisKey = "auctionId:" + auctionId;
operations.set(redisKey, "1");
redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES);
log.info("경매 등록, " + redisKey);
}
}
i코드를 설명하면 아래와 같다. 더보기 Click🖱️
1. 필드에 redisTemplate을 넣어 redisTemplate을 통해 문자열 key와 문자열 value값을 받도록 한다
2. Redis에서 문자열 값을 조작하기 위한 ValueOperations 객체를 가져오고
3.주어진 autionId를 기반으로 Redis 키를 생성한다.
4. 여기서는 value는 의미가 없으므로 "1"로 설정(set)해두었다.
5. 설정된 키의 만료 시간을 5분으로 설정합니다. n분(5분)이 지나면 키가 자동으로 만료되도록 한다.
그리고 해당 클래스는 생성자에서 RedisMessageListenerContainer를 인자로 필요로하기에 RedisConfig에 아래와 같이 bean으로 등록해줘야 한다.
- RedisConfig.class
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
private static final String REDISSON_HOST_PREFIX = "redis://";
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + host + ":" + port);
return Redisson.create(config);
}
@Bean
public RedisTemplate<String, String> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
// 🌟 redisMessageListenerContainer를 bean으로 등록하는 부분
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory redisConnectionFactory
) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
redisMessageListenerContainer.setErrorHandler(new CustomErrorHandler());
return redisMessageListenerContainer;
}
...
이렇게 해서 spring boot에서 redis.conf 설정 파일에 이벤트 알림을 따로 설정하지 않아도 redis event notifications가 발생하고 subscribe되는 것을 확인할 수 있을텐데 이 비밀은 자체적으로 구현되어있는 KeyspaceEventMessageListener.java를 보면 알 수 있다.
KeyspaceEventMessageListener 클래스의 init() 메서드를 보면 연결된 redis에서 notify-keyspace-events properties를 가져와 hasText() 메서드를 통해 값을 확인한 뒤 notify-keyspace-events의 값을 "EA"로 세팅하는 것을 볼 수 있다.
(위에서 redis.conf 설정 파일에서 자체적으로 수동 수정해주는 게 아니라 이 클래스에 이미 구현되어 있는 것이다.)
redis를 종료시켰다가 다시 켰을 경우 notify-keyspace-events 값은 다시 ""로 초기화된다.
#3 Redis pub/sub과 Keyspace Notifications의 차이점과 특징은?
✅ Redis pub/sub : channel에 데이터를 게시하는 일반적인 방법으로 잘 알려져 있고 다른 클라이언트는 해당 channel 이름으로 동일한 channel을 구독할 수 있다. 게시된 메세지는 subscriber가 무엇인지 알지 못한 채 channel로 특성화된다.
그리고 Redis에 기본적으로 활성화되어 있는 기능이며 fire and forget이기 때문에 메세지가 유지되지 않고 일단 전달되거나 분실되면 메세지를 찾을 수 없다.
✅ Keyspace Notifications : 클라이언트의 이벤트를 수신하기 위해 pub/sub channel을 구독하는 방법이며 아까 위에서 언급했듯이 CPU를 좀 소모해서 수동으로 활성화해야한다.
#4 Redis Keyspace Notifications의 장점과 단점, 그리고 주의할 점 💻
Redis Keyspace Notifications에는 장점과 단점이 모두 있다.
먼저 요약해서 전달하면 다음과 같다.
👍 [ 장점 ]
- 실시간 보고서를 작성해야할 때 매우 유용하다.
- 일정한 시간 간격으로 DB에 접속해서 연결을 열고 쿼리를 수행함으로써 불필요한 DB 작업과 시스템 사용량을 없앨 수 있다.
👎 [ 단점 ]
- 내구성이 부족하다 : Redis에서 보낸 알림 메세지가 항상 도착하지 않을 수 있다
=> 이로 인해 데이터가 부정확하거나 불완전해질 수 있다.
=> 하지만 누락되거나 손상된 데이터에 대해 하루에 한 번 수동 작업을 실행할 수 있다고 한다.
결론 : 중요한 데이터에 Redis Keyspace Notifications를 사용하는 것을 지양해야 한다.
⚠️ 주의할 점
redis pub/sub은 'fire and forget'이기 때문에 redis 서버는 이벤트를 pub한 이후로는 관심이 없다. 만약 pub/sub 클라이언트가 한 번 연결을 끊고 나서 다시 연결을 맺을 때, 클라이언트는 접속이 끊겼던 시간 동안에 전달된 모든 이벤트들을 잃게 된다. 그리고 expired event 자체가 발생하는 조건(expired event는 레디스 서버가 키를 삭제할 때 발생되는 이벤트며 이론적으로 TTL이 0으로 도달했을 때 발생하지 X)이 이러하기 때문에 TTL이 0에 도달한 시점과 expired 이벤트가 발생한 시간에는 차이가 있을 수 있다. 즉, key expired는 실시간이 아니고 로컬로 시도하면 실시간인 것처럼 보이지만 redis expired 이벤트 논리로 인해 만료 이벤트 시에 키에 바로 알림이 전달되지 않을 수도 있다는 뜻이다.
#5 Keyspace Notifications의 실제 쓰임새에 대한 고찰 🍀
이번 프로젝트는 자동 상태변경 방법으로 TTL과 CDC와 유사한 기능이 있는 이벤트 기반 redis keysapce notifications를 채택했지만 여러 가지 공식 자료들을 살펴보니 매우 적절한 기능을 붙였는가에 대해서는 고민해봐야 한다.
여러 가지 외국 자료들을 살펴보니 Keyspace notifications를 실제로 사용했던 용도는 다음과 같았다.
- 캐시 무효화 : 해당 키가 업데이트되거나 삭제될 때마다 캐시된 데이터를 무효화하는데 사용할 수 있다.
- 실시간 분석 : 새 데이터가 데이터베이스에 추가될 때마다 실시간 분석 및 보고를 트리거하는데 사용할 수 있다.
하나라도 누락되지 말아야 하는 중요한 기능에 포함되는 것이 아니라 분석하고 DB 작업량을 감소하는 데에 초점을 맞추면 될 것으로 보인다.
+) AWS ElastiCache로 사용할 때 하는 방법
https://repost.aws/ko/knowledge-center/elasticache-redis-keyspace-notifications
이걸 참고하고 사용하면 되는데 간단히 말해 Ex를 넣어주면 된다.
<References>
https://redis.io/docs/latest/develop/use/keyspace-notifications/
https://redis.io/docs/latest/operate/oss_and_stack/management/config-file/
'Project > 땅땅땅' 카테고리의 다른 글
[땅땅땅] RedisTemplate 사용 중, 기술 변경에 유연하고 확장성을 가지도록 리팩토링해보기 (0) | 2024.04.06 |
---|---|
[땅땅땅] ObjectMapper 빼고 코드 최적화해보기 - 리팩토링 (0) | 2024.03.30 |