Redis ‐ Decoupling microservices with Redis Pub Sub - woojin-playground/Backend-PlayGround GitHub Wiki

Redis - Decoupling microservices with Redis Pub/Sub

Loose Coupling을 위한 Redis Pub/Sub Messaging Pattern

1. 구성 요소의 역할

  • Publisher : 메시지를 보내는 주체. 특정 채널(Channel)에 메시지를 던지기만 할 뿐, 누가 이 메시지를 받는지는 전혀 신경쓰지 않는다.
  • Channel : 메시지가 전달되는 통로. 발행자는 채널에 메시지를 넣고 구독자는 채널을 듣게 된다.
  • Subscriber : 특정 채널을 지켜보고 있다가 메시지가 들어오면 즉시 수신하는 주체. 여러 구독자가 하나의 채널을 동시에 구독할 수 있다.

2. 핵심 메커니즘 : 서로 모름(Decoupling)

  • 발행자는 구독자를 모른다
  • 구독자는 발행자를 모른다

3. 실시간 전달의 특징(Fire and Forget)

  • Push 방식 : 구독자가 데이터를 달라고 요청하는 것이 아니라 Redis 서버가 구독자에게 데이터를 밀어넣는 방식으로 매우 빠르며 실시간성이 높다.
  • 비영속성(휘발성) : Redis의 Pub/Sub은 메시지를 저장하지 않는다. 발행자가 메시지를 보낸 순간 구독자가 오프라인 상태라면 그 메시지는 영원히 사라진다.
  • 만약 메시지 보관이 필요하다면 Redis의 List나 Stream 자료구조를 사용해야 한다.

1. 일반 구독(SUBSCRIBE) : 정확한 타겟팅

  • 동작 : 클라이언트가 정확한 채널 이름을 지정해 구독한다.
  • SUBSCRIBE chat.room.1이라고 명령하면 정확히 chat.room.1이라는 채널로 발행된 메시지만 수신한다.

2. 패턴 구독(PSUBSCRIBE) : 광범위한 수집

  • 동작 : 와일드카드를 사용하여 여러 채널을 동시에 구독하는 방식이다.
  • PSUBSCRIBE chat.room.*이라고 명령하면 chat.room.1, chat.room.2 등 해당 패턴에 매칭되는 모든 채널의 메시지를 한꺼번에 수신한다.

3. 메시지 발행(PUBLISH) : 실시간 브로드캐스팅

  • 동작 : 특정 채널명과 메시지 내용을 함께 보낸다.
  • Redis 서버는 해당 채널을 일반 구독 중인 클라이언트를 찾아서 메시지를 보낸다.
  • 동시에 해당 채널명과 매칭되는 패턴 구독 중인 클라이언트도 찾아 메시지를 보낸다.
  • 이 과정은 메모리 상에서 즉시 일어난다.

1. pubsub_channels(Dictionary 구조)

  • Redis는 내부적으로 해시 테이블을 사용한다.
  • 동작 : PUBLISH news "Hello" 명령이 들어오면, Redis는 딕셔너리에서 news라는 키를 O(1)의 속도로 즉시 찾고 그 후 연결된 리스트를 순회하며 대기 중인 구독자들에게 메시지를 쏴준다.
  • 채널 이름을 정확히 알고 있을 때 매우 빠르게 대상자를 찾을 수 있다.

2. pubsub_patterns(List 구조)

  • 딕셔너리와 달리 리스트 형태로 관리된다.
  • 동작 : 메시지가 특정 채널로 발행되면 Redis는 pubsub_patterns 리스트를 처음부터 끝까지 O(N)의 시간복잡도로 순회해 각 패턴이 현재 발행된 채널명과 매칭되는지 일일이 검사해 일치하는 경우 해당 구독자에게 메시지를 보낸다.
  • 와일드카드 매칭이 필요하기 때문에 해시 테이블 사용이 어렵고 리스트 전체 스캔이 불가피하다.

1. At-Most-Once(최대 1번 전달)

  • 동작 : 메시지를 보내고 나면 성공 여부를 확인하지 않는다.(=Fire and Forget)
  • 특징 : 네트워크 장애가 나면 메시지가 유실될 순 있지만 절대 중복되진 않는다.
  • 처리 속도가 빠르고 시스템 부하가 적다. 데이터가 조금 누락되어도 전체 흐름에 큰 지장이 없는 경우에 적합하다.

2. At-Least-Once(최소 1번 전달)

  • 동작 : 메시지를 보낸 후 수신자로부터 ACK를 기다린다. 일정 시간 동안 ACK가 오지 않으면 메시지를 다시 보낸다.
  • 특징 : 메시지 유실은 없지만 네트워크 지연 등으로 인해 이미 받은 메시지를 또 받는 중복 문제가 발생할 수 있다.
  • 데이터 손실을 방지할 수 있어 신뢰도가 높다. 단 수신 측에서 멱등성(Idempotency)을 보장하도록 설계해야 한다.(=똑같은 메시지 2번 받아도 결과가 같아야 함)

3. Exactly-Once(정확히 1번 전달)

  • 동작 : 메시지 전송과 수신, 그리고 처리를 하나의 트랜잭션으로 묶거나 시스템적으로 중복을 원천 차단한다.
  • 특징 : 유실도 중복도 없다. 사용자에게 완벽한 신뢰를 제공한다.
  • 구현 난이도가 매우 높고 성능이 다른 방식에 비해 상대적으로 떨어질 수 있다.

1. 동작 흐름 : 전사적 방송(Broadcasting)

  • 메시지 발행 : 특정 클라이언트가 Node A에 접속하여 메시지를 발행한다.
  • 클러스터 버스 전파 : Node A는 해당 메시지를 클러스터 내부 전용 통신망인 클러스터 버스에 태운다.
  • 전체 노드 수신 : 버스를 통해 Node B, Node C를 포함한 클러스터 내의 모든 노드가 해당 메시지를 전달받는다.
  • 구독자에게 전달 : 각 노드(A, B, C)는 자신에게 연결된 구독자들 중 해당 채널을 듣고 있는 유저가 있다면 즉시 메시지를 전달한다.

레디스 클러스터의 Pub/Sub는 어느 노드에 접속하든지간에 똑같은 결과를 얻어야 한다는 원칙이 있다. 구독자가 자신이 Node A에 붙어있든 Node C에 붙어있든 상관없이 클러스터 어디선가 발행된 메시지를 실시간으로 받을 수 있다.

Redis Pub/Sub Messaging

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory,
                                                                   NotificationSubscriber notificationSubscriber,
                                                                   NewsPatternSubscriber newsPatternSubscriber) {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.addMessageListener(notificationSubscriber, new ChannelTopic("test:notification"));
    container.addMessageListener(newsPatternSubscriber, new PatternTopic("news:*"));
    return container;
}
@Component
public class NewsPatternSubscriber implements MessageListener {

    private static final Logger log = LoggerFactory.getLogger(NewsPatternSubscriber.class);

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String matchedPattern = pattern != null ? new String(pattern) : "unknown";
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        log.info("Pattern message received from pattern {}, channel {}: {}", matchedPattern, channel, body);
    }
}
@Component
public class NotificationSubscriber implements MessageListener {

    private static final Logger log = LoggerFactory.getLogger(NotificationSubscriber.class);

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        log.info("Message received from channel {}: {}", channel, body);
    }
}