앞선 개요 글에서 Redis pub/sub의 목적과 Redis만의 설계 방식을 살펴보았습니다. 이번 글에서는 noderedis(Node.js 클라이언트)를 활용해 Redis pub/sub의 핵심 개념인 채널, 메시지 발행, 구독, 패턴 매칭을 하나씩 실습하며 실제 사용 방법을 알아보겠습니다.
채널(Channel) 이해하기
채널(channel)은 pub/sub 시스템에서 발행되는 메시지를 분류하기 위한 이름입니다. 채널 이름은 system-health:i-36a44b83, trade-prices:RAX, temp-reading:living-room처럼 시스템에 특화된 형태일 수도 있고, events처럼 아주 일반적인 형태일 수도 있습니다. 특정 채널에서 전달되는 데이터에 관심이 있는 모든 구독자는 해당 채널을 리스닝할 수 있으며, 시스템이 성장함에 따라 새로운 발행자와 구독자도 손쉽게 추가할 수 있습니다.
Redis 서버에서 현재 활성화된 채널을 확인하려면 PUBSUB CHANNELS 명령어를 사용하면 됩니다. 아직 pub/sub이 사용되지 않는 시스템에서는 아무것도 반환하지 않습니다:
$ redis-cli pubsub channels
(empty list or set)
채널은 구독자가 해당 채널의 메시지를 리스닝하고 있을 때만 존재합니다. 따라서 채널을 직접 "생성"하거나 "삭제"할 필요가 전혀 없으며, 구독자가 리스닝하는 동안에만 존재합니다. 이를 확인해 보겠습니다. 한 콘솔 창에서 redis-cli로 구독자 역할을 하고, 다른 창에서 PUBSUB CHANNELS를 다시 실행합니다:
콘솔 1:
$ redis-cli subscribe events
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "events"
3) (integer) 1
콘솔 2:
$ redis-cli pubsub channels
1) "events"
콘솔 1의 연결이 끊기는 즉시 시스템에는 다시 어떤 채널도 존재하지 않게 됩니다.
메시지 발행하기
명령어 문법
메시지를 발행할 때는 PUBLISH 명령어를 사용합니다.
PUBLISH channel message
해당 채널을 리스닝 중인 모든 구독자에게 메시지가 전달됩니다. 메시지를 받을 구독자가 없다면 그 메시지는 그대로 버려집니다.
Node.js 예제
Redis에 일반적인 연결을 생성한 후에는 publish를 다른 명령어와 똑같이 사용할 수 있습니다:
var client = require("redis").createClient();
client.publish("temp-reading:living-room", "37.0");
다른 Redis 명령어와 함께 사용하기
채널에 데이터를 발행하는 것은 매우 빠른 작업이므로, 다른 연산과 조합해서 사용하는 경우가 많습니다. 이렇게 연산을 조합하면 Redis의 진정한 힘이 발휘됩니다:
var client = require("redis").createClient();
client.multi()
.publish("temp-reading:living-room", "37.0")
.lpush("recent-temperatures", "37.0")
.ltrim("recent-temperatures", 0, 99)
.exec();
위 코드는 앞선 예제와 동일하게 pub/sub 시스템에 온도를 발행하지만, 동일한 MULTI/EXEC 트랜잭션 안에서 최근 100개의 온도 값을 유지하는 리스트에도 온도를 추가(push)합니다.
이 짧은 예제는 Redis pub/sub을 메시지 히스토리를 포함하는 더 큰 시스템 설계에 통합하는 방법을 보여줍니다. 위와 같은 연산을 사용하면 다른 클라이언트들이 새로운 값들을 구독하는 것은 물론, 최근 값들도 조회할 수 있게 됩니다.
메시지 구독하기
명령어 문법
채널을 구독할 때는 SUBSCRIBE 명령어를 사용합니다. 이 명령어는 클라이언트를 특수한 "구독(subscribed)" 상태로 전환시키며, 이 상태에서는 추가적인 SUBSCRIBE나 UNSUBSCRIBE 명령어 외에는 다른 명령어를 보낼 수 없습니다.
SUBSCRIBE channel [channel ...]
Node.js 예제
다음 구독자는 앞서 살펴본 발행자 예제와 함께 사용할 수 있습니다:
var subscriber = require("redis").createClient();
subscriber.on("message", function(channel, message) {
console.log("A temperature of " + message + " was read.");
});
subscriber.subscribe("temp-reading:living-room");
여기서는 간단한 이벤트 이미터(event emitter)를 사용하여 메시지가 도착할 때마다 함수를 호출합니다. 현재는 처리 함수가 콘솔에 온도를 기록할 뿐이지만, 원하는 어떤 작업이든 수행할 수 있습니다.
구독자와 함께 다른 명령어 사용하기
앞서 언급했듯이, 구독 중인 클라이언트는 특수 모드에 있기 때문에 다른 용도로 사용할 수 없습니다. 다른 명령어를 사용하려면 Redis에 별도의 클라이언트 연결을 생성해야 합니다:
var redis = require("redis")
, subscriber = redis.createClient()
, client = redis.createClient();
subscriber.on("message", function(channel, message) {
console.log("A temperature of " + message + " was read.");
client.incr("temp-count");
});
subscriber.subscribe("temp-reading:living-room");
이 핸들러는 메시지를 콘솔에 기록하고 Redis 카운터를 증가시킵니다. 경우에 따라 결과를 계산한 후 그 결과를 별도의 채널에 발행하여 다른 구독자들이 읽도록 만들 수도 있습니다 — 단, 자신이 리스닝 중인 채널에 다시 발행하지 마세요. 무한 루프가 발생합니다 :)
패턴 매칭
명령어 문법
채널 패턴 매칭에는 PSUBSCRIBE 명령어를 사용합니다.
PSUBSCRIBE pattern [pattern ...]
동작 방식은 일반적인 SUBSCRIBE 명령어와 같지만, 패턴과 일치하는 이름의 채널을 매칭할 수 있다는 점이 다릅니다. 덕분에 발행자는 자신이 발행하는 정보를 매우 구체적으로 지정할 수 있고, 구독자는 정확한 채널 이름을 몰라도 여러 채널을 리스닝할 수 있습니다.
지원되는 패턴은 간단합니다. *는 임의의 문자열에 일치하고, ?는 단일 문자에 일치하며, 대괄호([acd])를 사용하면 허용 가능한 문자 집합을 매칭할 수 있습니다.
여러 방의 온도 측정값 매칭:
PSUBSCRIBE temp-reading:*
site-link:logo:a:clickrate, site-link:logo:b:clickrate 같은 A/B 테스트 이벤트 매칭:
PSUBSCRIBE site-link:logo:?:clickrate
system-health:us-east-1a:i-36a44b83, system-health:us-east-1c:i-73657420636f636f6처럼 발행되는 AWS 인스턴스 전반의 이벤트 매칭:
PSUBSCRIBE system-health:us-east-1[acd]:i-*
Node.js 예제
다음 예제 역시 앞선 발행 예제와 함께 사용할 수 있으며, 온도와 함께 온도가 측정된 방(room)도 함께 기록합니다.
var subscriber = require("redis").createClient();
subscriber.on("pmessage", function(pattern, channel, message) {
var room = channel.split(":")[1];
console.log("A temperature of " + message + " was read in " + room);
});
subscriber.psubscribe("temp-reading:*");
패턴 기반 구독자의 콜백은 좀 더 많은 정보를 받습니다. 구독자는 여러 채널이나 패턴을 동시에 리스닝할 수 있으므로, 채널과 메시지뿐 아니라 매칭된 특정 패턴까지 함께 전달됩니다.
참고: PUBLISH의 성능 특성
Redis의 모든 명령어에는 공식적으로 문서화된 시간 복잡도(time complexity)가 있으며, 대부분 직관적으로 이해할 수 있습니다. PUBLISH 연산은 매우 단순해 보이기 때문에(거의 SET과 비슷하게 느껴짐) 복잡도가 O(1)이라고 생각하기 쉽습니다. 하지만 실제로 PUBLISH의 시간 복잡도는 구독자의 동작에 따라 선형적으로 증가합니다.
PUBLISH 명령어가 실행되면 해당 채널과 매칭될 수 있는 모든 구독 패턴과 메시지를 받아야 하는 모든 구독자를 순회해야 하므로, 시간 복잡도는 O(N+M)이 됩니다.
대부분의 배포 환경에서는 PUBLISH의 복잡도 때문에 성능 문제를 겪을 일이 없지만, 그래도 이 명령어의 성능을 지속적으로 추적하는 것이 현명합니다. 특히 코드가 자동으로 리스닝할 채널 패턴을 생성하는 경우라면 각별히 주의하세요.
여러분의 코드를 소개해 주세요
Redis pub/sub으로 만든 훌륭한 애플리케이션 사례를 소개하는 워크스루를 게재하고 싶습니다. 좋은 예제를 알고 계시다면 support@redisgreen.net으로 알려주세요. 확인한 코드 중 최대한 많이 소개해 드리겠습니다.