Redis)Pub/Sub 근데 Notion System Design 을 곁들인

박우영·2024년 4월 13일
0

DB

목록 보기
6/6

Overview

예전에 Messsage Queue 를 공부할때 kafka, rabbitmq, SQS, SNS 의 비교는 게시글 을 참고해주세요

Redis Pub/Sub 의 장점은 다음과 같습니다.

  • 낮은 러닝커브
  • 별도의 Dependency 추가 불필요 (Redis 사용중 일때)
  • Storage 사용이 되지 않음 (Persistence 관리를 할 필요가 없다)

Storge 를 사용하지 않는 다는 것은 휘발성이라는 뜻 입니다. Kafka 처럼 최소 한번의 전송을 보장하는 것이 불가능 하다는 뜻인데 물론 Redis Streams 를 사용하여 극복하거나 Redis pub/sub 을 사용하더라도 구현 방법에 따라 극복할 수 있겠지만, 저는 실시간 전송이 필요한 Message 처리에 Redis Pub/Sub이 매우 간단하게 사용이 가능하기에 이를 소개하고자 합니다.

UseCase

3개의 Web Application Server 에서 동일한 Redis Node 를 Pub/Sub 한다고 가정한다면 간단한 아키텍처로 다음과 같이 나타낼수 있습니다.

Config

...Redis 설정 생략
@Bean
    fun messageContainer(): RedisMessageListenerContainer {
        val listener = RedisMessageListenerContainer()
        listener.setConnectionFactory(redisConnectionFactory())
        return listener
    }

Lettuce 를 사용하여 생성된 RedisConnectionFactory 에 RedisMessageListenerContainer 를 설정해줍니다.

  • Consume 을 하기 위함

Publish Message

@Service
class RedisMessagePublisher(
    private val redisTemplate: RedisTemplate<String, String>,
) : MessagePublisher {
    override fun publish(sessionId: String, message: MessageQueueDto, receiver: String) {
        val channel = "$receiver.$sessionId"
        redisTemplate.convertAndSend(channel, Jackson.writeValueAsString(message))
    }
}

interface MessagePublisher {
    fun publish(sessionId: String, message: MessageQueueDto, receiver: String)
}

Message 를 Publish 해주는 공통의 Interface 를 정의하고 구현 합니다.

Subscribe Message

@Service
class RedisMessageSubscriber(
    private val redisMessageListenerContainer: RedisMessageListenerContainer,
) : MessageListener {
    fun subscribe(sessionId: String, client: String) {
        val topic = getTopic(sessionId = sessionId, client)
        redisMessageListenerContainer.addMessageListener(this, topic)
    }

    fun unSubscribe(sessionId: String) {
    	redisMessageListenerContainer.removeMessageListener(null, it)
    }

    override fun onMessage(message: Message, pattern: ByteArray?) {
    	Consume 된 메시지를 처리하는 logic
    }
}

위 3가지 설정을 해주면 설정이 완료됩니다.

동작 방식

Redis Pub/Sub 은 구독중인 모든 Application 에게 메시지를 전송합니다.
다음은 redis-cli 를 통해 다른 redis container 를 띄워 테스트 한 결과입니다.

같은 Channel 을 Subscribe 하고 있으면 모든 메시지를 받는것을 확인할 수 있습니다.

어떤걸 다뤄야 하고 어느 서비스에 적합할까요?

Event driven architecture 에 적용하는 것은 어렵다고 생각합니다.

  • Message 전송 보장유무

Overview 에서 말씀드렸던 것처럼 Redis Pub/Sub 은 Kafka 처럼 메시지 전송을 보장하지 않습니다. 따라서 전송 보장이 필요하다면 추가적인 구현을 해주거나 Kafka 같은 Message Queue 를 사용하는것이 더 적합하다고 생각 됩니다.
대신 전송이 보장되지 않은 실시간 서비스에선 매우 적합하다고 할 수 있습니다.

매우 주관적인 예시 검증 X
많이들 사용하는 Notion 에서도 사용할 것으로 생각됩니다.

Notion

Notion 은 게시판으로 되어있지만 여러명이 동시에 수정이 가능한 구조 입니다.
게시판 처럼 보이는 실시간 채팅 Channel 의 구조라고 생각이들고 아마 Redis Pub/Sub 을 사용하여 Socket 을 관리하고 이를 전송해주는 방식으로 시스템 디자인 했을거라 예상합니다.

  • Socket Server
    WebSocket Session 은 기본적으로 고정적인 sticky session 입니다.
    Scalability 를 고려한다면 Scale up 뿐만 아닌 Scale out 을 고려하는 것이 좋은 선택지 일수 있습니다. 따라서 Socket Server 를 별도의 MicroService 로 분리합니다.

  • Redis
    Scale out 된 분리된 Socket Server를 관리하기 위한 Session Cluster 로 사용합니다.
    각 Session 정보들을 저장하고, Pub/Sub 을 활용하여 실시간 공유가능한 메시징 시스템을 구축합니다.

  • Database(MongoDB, DynamoDB)
    NoSQL 을 활용할 것이라 생각듭니다. NoSQL 을 다루는 Session 은 아니기에 다음기회에 다뤄보겠습니다.

  • Channel Gateway
    Redis Pub/Sub 으로 Subscribe(게시글 ID) 하고있는 Session 들을 관리합니다. 지속적으로 Ping/Pong 혹은 Instance(application) 의 Health Check 를 하며 갑작스럽게 ShutDown 된 서버가 없는지 확인하고 메시지를 받을수 있는 상태인지 확인하며 Session 과 Subscribe 하고있는 Client 들을 관리합니다.

Notion System Design 을 간단히 해봤는데 실제는 이것보다 더 복잡하겠지만 Redis Pub/Sub 을 활용한 공동 게시글 작성하는 것과 같이 실시간 서비스에 활용할 수 있을 것 같다는 생각이 듭니다.

고민해야 할 부분은?

  • Applicaiton 과 Redis 의 Trade off

구현 방법에 따라 Application 에 좀 더 부하가 생길 수 있고, Redis 에 부하가 생길 수 있습니다.
서비스 요구사항 을 확인하고 APM 을 구축하여 상황에 알맞게 튜닝 할 수 있도록 해야 합니다.

Reference


Redis Docs
Spring Docs

0개의 댓글