PUB/SUB, 잘 알고 쓰자!

Gunwoo Kim
FRIP
Published in
7 min readAug 10, 2021

Kafka와 Redis 사이에서 고민하는 당신을 위한 글

이 글은 KafkaRedis의 시스템에 대한 자세한 분석이 아니라, 상황에 따라 어떤 서비스를 사용하는 것이 유리할 지에 대해 초점이 맞춰져 있습니다.

Photo by Kristina Tripkovic on Unsplash

MSA를 이용하여 서비스를 개발하다 보면, 한 API에서 다른 API로 정보를 전달하고 싶을 때가 있다. 프립을 예로 들면, User 서비스에서 회원가입이 발생했을 때, Coupon 서비스에서는 회원가입 기념 쿠폰을 발행하고, Point 서비스에서는 회원가입 기념 Point를 유저에게 제공한다. 이러한 상황에서 쓸 수 있는 것이 바로 PUB/SUB 모델이다.

PUB/SUB

PUB/SUB의 구조는 아래와 같다.

PUB/SUB 모델 구조
  1. 이벤트(메시지)를 발행하는 Publisher가 존재하며, Publisher는 특정 Channel(혹은 Topic)에 이벤트를 전송한다.
  2. 특정 Channel(혹은 Topic)을 구독하는 Subscriber가 존재하며, Publisher에 관계없이 발행된 이벤트를 받을 수 있다.

이때 구체적인 발행/구독 방식이 각 서비스마다 다른데, 대표적으로 Kafka와 Redis가 있다.

KAFKA

Kafka의 PUB/SUB 모델

Kafka에서는 Producer/Consumer라는 개념이 등장하는데, 각각 Publisher/Subscriber와 그 기능이 동일하다. Producer는 Topic에 이벤트를 보내고, 이 이벤트는 Topic의 각 Partition에 분산되어 저장된다. Topic을 구독하고 있는 Consumer group 내의 Consumer는 각각 1개 이상의 partition으로부터 이벤트를 가져온다. 만약 partition 개수보다 consumer 개수가 많다면, 아무 일도 하지 않는 consumer가 생기기 때문에, 항상 partition 수를 consumer보다 같거나 크게 해주는 것이 좋다.

Kafka의 topic은 우체통이라고 생각하면 될 것이다. 철수(Producer)는 편지를 써서 영희네 우체통(Topic)에 넣었다. 이때, 편지는 누군가에 의해 발견될 때까지 우체통에 얌전히 있을 것이다. 편지는 영희가 먼저 발견할 수도, 영희네 오빠가 먼저 발견할 수도 있다, 그들은 가족이니까(Consumer Group). 만일 영희네 오빠가 편지를 먼저 발견한다면? 영희는 편지의 존재조차 모를 것이다.

REDIS

Redis의 PUB/SUB 모델

Redis에는 그룹이라는 개념이 존재하지 않고, 각 subscriber가 channel을 구독하고 있다. 이때 중요한 점은, Channel은 이벤트를 저장하지 않는다는 것이다. 만일 Channel에 이벤트가 도착했을 때, 해당 채널의 Subscriber가 존재하지 않는다면, 이벤트는 사라진다.

Redis의 Channel은 말 그대로, TV의 Channel을 생각하면 된다. 하루 종일 TV에서는 수백 개의 채널에서 방송이 방영된다. 각 방송사(Publisher)에서 방영하는 라이브 방송은, 해당 채널을 시청 중일 때만 볼 수 있다. 또한 같은 시간대에 같은 채널의 시청자(Subscriber)들은 모두 같은 방송을 볼 수 있다.

물론 위의 예시에서 다 담지 못하는 부분도 있다. subscriber는 동시에 여러 Channel을 구독할 수 있으며, 특정한 채널을 지정하지 않고 ‘패턴’을 설정하여 해당 패턴에 맞는 채널을 구독할 수도 있다.

KAFKA vs REDIS

내가 생각하는 Kafka와 Redis의 주된 차이점은 다음과 같다.

  1. 이벤트의 저장 여부
  2. 한 이벤트를 받을 수 있는 Subscriber(Consumer) 개수

이벤트의 저장 여부

Kafka는 발행된 이벤트가 각 Partition에 저장된다. 하지만 Redis는 발행된 이벤트를 저장하지 않기 때문에, 구독자가 없다면 해당 이벤트는 사라지고 만다. 따라서, 이벤트의 구독과 발행이 실시간으로 이루어져야 되는 상황인지, 혹은 언제든 발행된 이벤트를 읽으면 되는 상황인지에 따라 선택이 달라질 것이다.

한 이벤트를 받을 수 있는 Subscriber(Consumer) 개수

프립은 AWS를 통해 서버를 배포하고 있다. 이때, 한 API에 대해, Scale-out 등의 이유로, 여러 서버 인스턴스가 작동될 수도 있다. 앞선 예시를 생각해보자. Coupon 서비스는 유저 회원 가입에 대한 Topic을 구독하고 있고, 유저가 회원가입 했다는 이벤트를 받으면 회원가입 기념 쿠폰을 발행한다.

그리고 이때, Coupon 서비스에 대해 2개의 서버를 사용한다고 하자.

Kafka를 통한 쿠폰 발행

만약 Kafka를 사용한다면, Consumer의 수와 관계없이, 유저 회원가입당 하나의 쿠폰이 발행될 것이다.

하지만, Redis 를 사용한다면?

Redis PUB/SUB을 통한 쿠폰 발행

유저는 한 번의 회원가입으로 두 개의 쿠폰을 얻게 된다!

즉, 한 이벤트에 대해 한 번의 기능만 작동되어야 한다면 Kafka를 사용하는 것이 유리하다.

반대로 Redis의 PUB/SUB 기능이 필요할 때도 있다.

예를 들어, 내가 개발한 Gateway의 권한 플러그인의 경우, 존재하는 모든 권한 목록을 서버가 ‘시작될 때’ 가져온다.

이때 한 가지 문제가 있다. 새로운 권한이 추가되거나, 삭제되었을 때에도, 서버를 재시작하기 전까지는 권한 목록을 갱신할 수가 없는 것이다.

이러한 상황에서 Kafka를 위 예시와 동일한 방식으로 사용한다면 다음과 같은 문제가 있다.

Kafka를 통한 권한 목록 갱신

gateway 서버가 두 개 이상 뜨더라도, 각각의 서버는 같은 Consumer Group에 속하기 때문에, Permission Service에서 보낸 이벤트를 단 한 서버만 받을 수 있다. 결국 권한 목록의 갱신은 하나의 서버 인스턴스에만 이루어지게 되고, 이는 synchronization 문제를 낳는다.

물론 Kafka에서도, 각 서버 인스턴스마다 다른 group ID를 부여하여 사용할 수도 있다. 예를 들면, group ID를 ${api명}-${서버 instance의 ID} 와 같은 방식으로 부여할 수 있다. 동적으로 서버 instance의 ID를 group ID 로 설정하는 좋은 방법이 있다면, 해당 방법을 통해 해결하는 것이 나을 수도 있다.

Redis를 통한 권한 목록 갱신

Redis를 쓰게 되면 얘기가 다르다. Redis는 Group이라는 개념이 없기 때문에, Gateway Server가 여러 개 뜬다고 하더라도, 각각이 이벤트를 받을 수 있다.

이처럼 발행된 이벤트에 대해, 특정 작업이 한 번만 발생하여야 하는지, 모든 Subscriber에서 발생되어야 하는지에 따라 다른 선택을 해야 한다.

마치며

앞서 말했듯, 이 포스트에서 모든 것을 다루지는 못했다. 예를 들면, Kafka의 Broker 개념이라든지, partition에서 이벤트를 어떻게 읽어오는지… 등등, 그 모든 것을 다 다루기에는 여백이 충분하지 않아 옮기지 않는다(정말이다). 해당 시스템이 궁금한 사람은 KafkaRedis의 링크로 들어가 보는 것을 추천한다.

다만, 내가 첫 개발 포스트를 쓰며 바랐던 것은, ‘보편적으로 읽힐 글’이 아니라, 나와 같은 상황에서 헤매고 있는 이들을 위한 ‘가려운 곳을 긁어주는 글’이었다. 이벤트 시스템으로 어떤 것을 사용할지 고민하고 있을지도 모를, 당신에게 이 글이 도움 되길 바라며 글을 마친다.

--

--

Gunwoo Kim
FRIP
Writer for

KAIST CS undergraduate Software Engineer of Toss