[Spring] Spring Data Redis 로 Redis Pub/Sub 구현하기
Redis PubSub
Redis PubSub 기능은 Redis 에서 Publish-Subscribe pattern 을 사용할 수 있도록 구현한 것이다.
Publish-Subscribe pattern은 발행자(Publisher)가 메시지를 특정 채널에 발행하면 해당 채널을 구독(Subscribe)하고 있는 수신자(Subscriber)들이 메시지를 받는 구조를 의미한다.
Redis PubSub vs RabbitMQ
RabbitMQ와 Redis PubSub의 차이점은 무엇인가요?
PubSub 구조로 유명한 다른 도구로는 RabbitMQ가 있다. 같은 PubSub 을 사용하지만 용도와 기능 면에서 차이가 있다.
내가 생각하기에 가장 중요한 차이는 메시지 전송 보장과 지속성에서의 차이 인 것 같다.
RabbitMQ | Redis PubSub | |
---|---|---|
메시지 전송 보장 | 소비자의 확인을 통해 메시지 전송 보장 | 별도의 보장을 하지 않으며, 구독자가 연결되어 있어야 메시지 수신 가능 |
지속성 | 영구 및 일시적 메시지 지원, 디스크에 저장 가능 | 기본적으로 지속성 없음 |
Redis PubSub 을 사용하기로 결정한 이유
서비스 내에서 다양한 캐시를 사용하고 있는 중인데, 그 중 일부 캐시는 각 서버에서 인메모리로 관리하고 있다. 서버 어플리케이션은 k8s 위에서 여러개의 팟으로 관리되고 있다보니, 전체적으로 캐시를 초기화 하려면 각 서버 어플리케이션에 직접 포트포워딩을 하여 캐시 초기화 api 를 호출해주거나 해야하는 이슈가 있었다.
prod 환경에서는 그럴일이 크게 없겠지만, 개발 환경에서는 캐시를 초기화 해야할 일이 자주 발생되기도 한다. 이에 한쪽에서 캐시를 초기화를 호출하면 별도의 수동 작업 없이 각 pod 에서 캐시 초기화를 진행되도록 하고 싶었다.
이 시스템에는 이미 Redis 도, Rabbit MQ 도 구성되어 있다. 하지만, 데이터가 지속되어야 할 필요가 없었고, 수신을 검증할 필요도 없는 작업이라고 생각하였기 때문에 Redis PubSub로 빠르게 결정하고 진행하였다. 설정도 간단하게 가능한 것도 선택에 영향을 주었다.
기존의 architecture 는 다음과 같은 구조였다.
- Service A 에서 Domain A 에 대한 데이터를 redis 캐시로 관리하고 있다.
- Service B 에서 Service A 에서 Domain A에 대한 부분을 grpc로 받아온다. 그리고 메모리에 cache 처리한다. 잘 변하지 않는 데이터라 그런지, 최초에 데이터를 조회한 후 cache 처리는 하는데, 이 후 cache를 수정/삭제 하는 방법은 제공하고 있지 않았다. 그래서 해당 데이터에 대한 캐시를 초기화 하려면 Service B 의 Pod들을 재실행 해야만 했다.
그래서 redis pubusb을 통해 Service A 에서 Domain A 에 대해 evict 처리를 하였을 때 Service B 에서도 함께 evict 처리를 하도록 아래와 같이 개선을 해보기로 하였다.
Spring Data Redis 로 Redis Pubsub 구현하기
다음은 spring-data-redis 를 이용한 pub/sub 코드이다. pub/sub 을 위한 channel 명을 동일하게 설정해줘야 한다.
Publisher
RedisPublisher.java
@RequiredArgsConstructor
@Component
public class RedisPublisher {
private static final String EVICT_CACHE_OF_SOMETHING = "evict-cache-of-something";
private final StringRedisTemplate redisTemplate;
public void publishEvictCacheOfSomething(int id) {
redisTemplate.convertAndSend(EVICT_CACHE_OF_SOMETHING, String.format("{ \"id\": %d }", id));
}
}
Subscriber
RedisConfig.java
@Bean
public MessageListenerAdapter messageListenerAdapter(RedisSubscriber redisSubscriber) {
return new MessageListenerAdapter(redisSubscriber, "onMessage");
}
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, MessageListenerAdapter listener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listener, PatternTopic.of("*"));
return container;
}
공식 Document 에서는 Pattern 을 사용할 수도 있다고만 언급되어있는데, Pattern 을 사용하려면 ChannelTopic 이 아닌 PatternTopic 을 사용해야 한다. Document 에는 PatternTopic 에 대한 언급이 없다. pattern 방식이 동작하지 않아 코드를 직접 확인해보고 알게되었다.
여기서는 모든 channel 의 데이터를 한 곳에서 받을 수 있도록 *
로 처리하였다.
RedisSubscriber.java
@Component
public class RedisSubscriber implements MessageListener {
private static final String EVICT_CACHE_OF_SOMETHING = "evict-cache-of-something";
@Override
public void onMessage(Message message, byte[] bytes) {
String channel = new String(message.getChannel());
switch (channel) {
case EVICT_CACHE_OF_SOMETHING:
evictCacheOfSomeThing(new String(message.getBody()));
break;
// ...
default:
log.warn("Unknown channel: " + channel);
break;
}
}
public void evictCacheOfSomeThing(String message) {
// evict cache
}
}
마무리
말로만 들어왔던 Redis Pub/Sub 을 이용하여 직접 사용 사례를 구현해보았다. 들어왔던 대로 간단하게 설정 가능해서 필요할 경우에 빠르게 도입할 수 있겠다 생각이 들었다. 물론 장점만 있지는 않고 단점도 존재한다. 상황에 따라서 적절한 도구를 선택하여 활용하면 다양한 시스템 요구사항에 효과적으로 대응할 수 있을 것이다.
기타
컨트리뷰트
위에서 다음과 같은 이야기를 하였다.
공식 Document 에서는 Pattern 을 사용할 수도 있다고만 언급되어있는데, Pattern 을 사용하려면 ChannelTopic 이 아닌 PatternTopic 을 사용해야 한다. Document 에는 PatternTopic 에 대한 언급이 없다. pattern 방식이 동작하지 않아 코드를 직접 확인해보고 알게되었다.
그래서 문서에 PatternTopic에 대한 언급을 추가하는 PR을 작성하였다.
spring-data-redis 의 멤버인 Mark Paluch(mp911de) 가 PR을 확인하고는 문서에 추가하는 것 뿐 아니라 ChannelTopic 과 PatternTopic 의 공통 인터페이스인 Topic
interface 에 factory 메소드를 추가해주었다.
그래서 추후에는 아래와 같은 형태로 사용 가능하게 될 예정이다.
Topic.channel("chatroom")
Topic.pattern("*room")
색다른 컨트리뷰트 경험이였어서 공유해본다.