Published on

레디스의 Pub/Sub과 실제 활용 사례

Authors
  • avatar
    Name
    ywj9811
    Twitter

Redis의 Publish / Subscribe

Redis에는 흔히 알고 있는 캐시 메모리로서의 기능 외에 메시지 브로커의 역할도 존재한다.

Untitled

이러한 메시지 큐에서 브로커의 역할을 수행하는 것이다.

하지만, Redis의 경우 흔히 말하는 메시지 큐의 메시지 브로커의 역할과는 약간 다르다.

Redis는 메시지를 저장하지 않고, 대기중인 클라이언트에게 바로 전달한 후 메시지를 삭제해버리는데, 이것이 Publish / Subscribe 이다.

이는 특정 Topic에 대해 해당 Topic을 구독한 모두에게 메시지를 발행하는 통신 방법인데, 1:N 으로 메시지를 전달할 수 있는 구조이다.

Untitled

따라서 Redis의 Pub/Sub 기능은 주로 채팅 혹은 푸시 알람 등등에 사용된다고 한다.

하지만, Redis의 Pub/Sub 은 매우 단순한 구조로 되어 있어 주의해야하는 점이 있는데 위에서 말한 것과 같이 메시지를 저장하지 않는다는 점이다.

즉, 만약 subscribe를 한 대상이 없다면 메시지를 publish 했음에도 불구하고 아무도 받지 않고 메시지는 사라져버린다.

그렇기 때문에 전송된 메시지를 따로 저장하거나 수신확인이 필요 없을 때, 그리고 100% 전송 보장은 하지 않아도 되는 경우에 이용하는 것이 좋다고 한다.

CommandSyntaxDescription
subscirbechannel [channel ...]채널을 구독하여 메시지를 수신 받는다. (동시에 여러개 구독 가능)
publishchannel message메시지를 지정한 채널로 송신
pubsubsubcommand [argument [argument ...]]서버에 등록된 채널이나 패턴을 조회
psubscribepattern [pattern ...]채널 이름을 패턴으로 등록
unsubscribe[channel [channel ...]]subscribe로 등록한 채널 구독 해제
punsubscribe[pattern [pattern ...]]psubscribe로 등록한 패턴 채널 구독 해제

이렇게 동작 방식은 간단하다.

그렇다면 실제로 어떤 방식으로 쓰이고 있을까

올리브영에서 사용하는 방식을 살펴보자.

실제 활용 사례 : 올리브영 쿠폰 발급 비동기 처리

올리브영에서는 Redis의 Pub/Sub 방식을 활용하여 쿠폰 발급을 비동기로 처리하고 이를 통해 기존의 프로세스 개선을 했다고 한다.

우선, 올리브영은 1년에 4번 올영세일을 진행하는데, 이때 선착순 쿠폰을 발급한다고 한다.

Untitled

기존의 올리브영은 위와 같은 프로세스로 동작하고 있었으며, 모든 과정이 동기적으로 처리되어 트래픽이 몰리는 경우 쿠폰 발급 과정에서 DB Connection 등의 자원을 많이 점유하고 있었다고 한다.

그렇기 때문에 접속 사용자를 제어하고 있었으며 사용자는 본인의 순서를 기다려야 했다고 한다.

Untitled

올리브영은 기존의 프로세스를 위와 같은 프로세스로 변경을 하였는데, 쿠폰 발급에 필요한 유효성 검사까지는 기존의 로직을 그대로 사용하고 발급 처리는 신규 시스템인 쿠폰발급 Worker로 위임한다.

이때 올리브영 온라인몰과 쿠폰 발급 Worker 사이에 Redis의 Pub/Sub을 이용한 것이다.

Redis를 활용하게 되면 In-Memory 기반이기 때문에 네트워크 통신이 필요한 웹 소켓을 이용하는 것보다 매우 빠르게 메시지를 주고 받을 수 있다.

이어서 더 살펴보면, Redis에서 쿠폰 발급에 대해서 Pub을 하면 쿠폰 발급 Worker는 해당 Topic을 Sub하고 이를 DB에 Insert하는 식으로 진행된다고 한다.

하지만, 이때 생각나는 문제가 있을 것이다.

위에서 언급한 것과 같이

이는 특정 Topic에 대해 해당 Topic을 구독한 모두에게 메시지를 발행하는 통신 방법인데, 1:N 으로 메시지를 전달할 수 있는 구조이다.

이러한 구조이기 때문에 만약 쿠폰 발급 Worker가 여러개라면 한명에 대해 여러개의 쿠폰이 Insert되는 일이 발생할 것이다.

실제로 올리브영에서 이 부분에 대해 언급을 하고 있다.

첫번째 이슈 : 쿠폰 과발급 현상

위에서 말한 것과 같이 만약 쿠폰 발급 Worker가 4개 존재한다면 1건의 쿠폰 발급 데이터가 Pub될 때 쿠폰 발급이 4회 수행되게 된다.

당연히 이러한 일은 발생하면 안되기 때문에 올리브영은 message를 Pub할 때 여러개의 Worker중 1개를 고르고 해당 Worker만 message를 받을 수 있도록 로직을 변경했다고 한다.

Untitled

하지만, 위에서 언급한 문제가 하나 더 있다.

그렇기 때문에 전송된 메시지를 따로 저장하거나 수신확인이 필요 없을 때, 그리고 100% 전송 보장은 하지 않아도 되는 경우에 이용하는 것이 좋다고 한다.

이 부분이다.

만약 Worker가 받지 못한다면 메시지는 저장되지 않고 사라져버리기 때문에 100% 보장이 되지 않는다.

두번째 이슈 : 쿠폰 미발급 현상

올리브영 역시 이러한 문제를 인식하고 있었고, 이에 대해서 Redis에서 제공하는 List 자료구조를 사용하여 해결했다고 한다.

하지만 내가 보기에는 여기서부터 Redis의 Pub/Sub 만을 사용해서 해결하기에는 로직이 올바르지 않기에 벌어진 일로 보인다.

우선, 올리브영은 여러 Worker중 하나의 일련번호를 획득하고, Redis에 List자료구조로 되어있는 ‘쿠폰 발급 저장소’에 쿠폰 발급 데이터(message)를 RPush 한다.

그리고 쿠폰 발급 Worker는 본인의 ‘일련번호’로 채번된 ‘쿠폰발급 저장소’의 크기를 주기적으로 체크하며 0보다 크면 LPop을 하여 쿠폰 발급을 처리한다.

이러한 방식을 통해 중간에 Message를 저장할 수 있도록 하여 Message 유실 위험을 방지하는 것 같다.

Untitled

결과

Untitled

그렇게 올리브영은 최종적으로 위와 같은 형태를 갖추고 쿠폰 발급을 진행한다고 한다.

Redis의 Pub/Sub 만으로는 한계가 있었고, Redis의 List자료구조를 결합하여 사용하는 방식으로 보인다.

1. 쿠폰 발급 Worker가 구동되면 '쿠폰발급' 이라는 Topic에 대한 '일련번호'가 생성됩니다. 
Worker마다 각각의 '일련번호'가 할당되어 있습니다.

2. 각 Worker들은 '쿠폰발급'이라는 Topic을 구독합니다.

3. 주기적으로 '쿠폰 발급 저장소' List의 크기가 0보다 큰지 확인하고, 
0보다 크면 List에서 데이터를 Pop 하여 쿠폰을 발급하는 프로세스가 실행됩니다.

위의 1~3번 과정은 쿠폰 발급을 위해 기본적으로 미리 실행되어 있어야 합니다.

아래 4~8번 과정은 실제 사용자가 쿠폰 발급을 요청했을 때 어떤 흐름으로 동작하는지 설명합니다.

4. 올리브영 온라인몰을 통해 고객이 쿠폰 발급을 요청합니다.

5. 올리브영 온라인몰에서는 각 Worker들이 가지고 있는 '일련번호' 중 1개를 골라서 
'쿠폰발급' 이라는 Topic에 publish 합니다.

6. 고객의 쿠폰 발급 데이터를 각 '일련번호'마다 할당된 '쿠폰 발급 저장소' list에 RPush 합니다.

7. 6번 과정으로 '쿠폰 발급 저장소'의 크기가 0보다 커졌으므로 
쿠폰발급 Worker는 해당 list를 LPop하여 쿠폰 발급 데이터를 가져옵니다.

8. 7번에서 가져온 쿠폰 발급 데이터를 온라인몰 Main DB에 INSERT 하여 발급 처리를 완료합니다.

결론적으로, 여러 Worker 중 어떤 Worker를 통해 쿠폰을 발급할지 결정지어 주는 역할을 Redis Pub/Sub 기능을 활용하여 구현(1개의 '일련번호' 선택)하였고, 실제 쿠폰 발급에 필요한 데이터는 각 Worker 별로 지정된 '쿠폰 발급 저장소'에 저장되었다가 쿠폰 발급이 진행됩니다.

전체적인 구조에 대해서는 위와 같이 올리브영이 소개하고 있다.

이를 통해 올리브영은 기존의 속도를 2배 이상 향상시켰다고 한다.

정리

Redis는 캐시 기능 이외에도 Pub/Sub을 통한 메시지 브로커 역할을 수행할 수 있으며, 이 또한 In-memory 기반으로 진행하기 때문에 성능은 굉장히 뛰어나다고 한다.

하지만, Kafka 혹은 RabbitMQ와 같은 다른 MessageQueue와 다르게 Publish된 메시지를 수신할 대상이 현재 존재하지 않으면 해당 메시지는 저장되지 않고 사라진다는 문제가 있고, 하나의 채널에 대해 여러개의 구독자가 있다면 모든 구독자가 메시지를 수신하기 때문에 이에 대한 적절한 처리가 필요하다는 점을 주의해야 한다.

이렇게 Redis의 Pub/Sub 기능을 살펴보고 실제로 올리브영에서 사용한 예시를 살펴보았는데,

사실 올리브영의 예시를 보며 만약 비동기 처리가 필요했고, 이에 대해 MQ를 활용할 것을 고려했다면 Redis가 아닌 Kafka혹은 RabbitMQ를 사용하는 것이 어땠을까, 하는 의문이 든다.

물론, 나는 Kafka와 RabbitMQ 뿐만 아니라 MessageQueue에 대해 전체적인 이해가 부족하기도 하고 올리브영의 당시 인프라와 상황을 모르기 때문에 자세히 알수는 없을 것 같다!

다만, 실제로 후기를 더 살펴보니 현재는 안정성을 위해 Redis가 아닌 RabbitMQ를 사용중인 것 같다.ㅎㅎ

이외에도 아래와 같이 채널톡에서 Redis의 Pub/Sub을 활용한 사례도 존재하니 참고하면 좋을 것 같다!

https://channel.io/ko/blog/real-time-chat-server-1-redis-pub-sub