Node.js에서 Redis Pub/Sub를 활용한 WebSocket 서비스 확장
Ethan Miller
Product Engineer · Leapcell

소개
현대 웹 개발에서 WebSocket을 통해 제공되는 실시간 기능은 채팅 플랫폼부터 협업 편집 도구에 이르기까지 다양한 애플리케이션에 필수적이 되었습니다. 애플리케이션의 인기가 높아지고 복잡성이 증가함에 따라, 일반적인 단일 인스턴스 WebSocket 서버는 확장성 및 내결함성 측면에서 빠르게 한계에 부딪힙니다. 단일 서버가 다운되면 모든 연결된 클라이언트가 연결이 끊어지며, 로드 밸런싱은 중요한 문제가 됩니다. 이러한 문제에 대한 해결책은 종종 WebSocket 서비스의 여러 인스턴스를 배포하는 것을 포함합니다. 그러나 중요한 문제가 발생합니다. 이러한 독립된 인스턴스들이 서로 통신하여 해당 클라이언트가 어느 인스턴스에 연결되어 있든 모든 관련 클라이언트에게 메시지를 브로드캐스트하려면 어떻게 해야 할까요? 이때 Redis Pub/Sub가 프로세스 간 통신을 위한 간결하고 효율적인 솔루션으로 빛을 발하며, 강력하고 확장 가능한 다중 인스턴스 WebSocket 아키텍처를 구축할 수 있게 해줍니다.
핵심 개념 이해
구현 세부 사항에 들어가기 전에, 논의의 중심이 되는 몇 가지 기본 개념을 명확히 해 보겠습니다.
- WebSocket: 단일 TCP 연결을 통해 전이중 통신 채널을 제공하는 통신 프로토콜입니다. 클라이언트와 서버 간의 실시간 양방향 데이터 교환을 허용합니다.
- Node.js: Chrome의 V8 JavaScript 엔진을 기반으로 구축된 JavaScript 런타임입니다. 이벤트 기반의 비차단 I/O 모델은 WebSocket 서버와 같은 실시간 애플리케이션에 매우 효율적입니다.
- Redis: 데이터베이스, 캐시 및 메시지 브로커로 사용되는 오픈 소스 인메모리 데이터 구조 저장소입니다. 번개처럼 빠른 성능과 Pub/Sub를 포함한 다양한 데이터 구조 지원은 높은 처리량의 메시징에 이상적입니다.
- Pub/Sub (Publish/Subscribe): 송신자(게시자)가 수신자(구독자)에게 직접 메시지를 보내는 것이 아니라, 구독자가 누구인지 알지 못한 채 게시된 메시지를 클래스로 분류하는 메시징 패턴입니다. 구독자는 하나 이상의 메시지 카테고리에 대한 관심을 표현하며 관심 있는 메시지만 수신합니다.
- 다중 인스턴스 배포: 로드 밸런서 뒤에서 동일한 애플리케이션의 여러 복사본을 실행하는 것입니다. 클라이언트 요청을 인스턴스별로 분산하여 확장성을 향상시키고, 한 인스턴스가 실패하더라도 서비스가 계속 가용하도록 하여 내결함성을 강화합니다.
다중 인스턴스 WebSocket의 과제
사용자가 채팅 애플리케이션에 연결된 시나리오를 생각해 봅시다. 두 개의 Node.js WebSocket 서버 인스턴스(Instance A
및 Instance B
)가 있는 경우, Instance A
에 연결된 사용자가 메시지를 보냅니다. Instance B
가 이 메시지에 대해 어떻게 알 수 있을까요? 그러면 이 메시지를 자신에게 연결된 사용자에게 브로드캐스트할 수 있을까요? 공유 통신 메커니즘 없이는 Instance A
는 해당 클라이언트에만 메시지를 보낼 수 있습니다. 이때 Redis Pub/Sub가 격차를 해소합니다.
Redis Pub/Sub를 이용한 WebSocket 확장
핵심 원리는 Redis를 중앙 메시지 버스로 사용하는 것입니다. WebSocket 서버 인스턴스가 클라이언트로부터 메시지를 받으면, 해당 로컬 클라이언트에만 브로드캐스트하는 대신 Redis 채널에 이 메시지를 게시합니다. 동일한 Redis 채널을 구독하는 다른 모든 WebSocket 서버 인스턴스는 이 메시지를 수신하고, 각 연결된 클라이언트에게 브로드캐스트합니다. 이를 통해 클라이언트가 어느 인스턴스에 연결되어 있든 모든 메시지가 관련 클라이언트 모두에게 도달하도록 보장합니다.
구현 세부 정보
WebSocket을 위한 ws
라이브러리와 Redis 클라이언트를 위한 ioredis
를 사용한 Node.js에서 실용적인 예제를 통해 이를 설명해 보겠습니다.
먼저 필요한 패키지를 설치합니다:
npm install ws ioredis
이제 WebSocket 서비스에 대한 간소화된 server.js
파일을 만들어 보겠습니다:
// server.js const WebSocket = require('ws'); const Redis = require('ioredis'); // Redis 서버가 로컬에서 실행 중이거나 해당 연결 문자열을 제공하십시오. const redisPublisher = new Redis(); // 기본값은 localhost:6379 const redisSubscriber = new Redis(); // 구독을 위한 별도의 클라이언트 const wss = new WebSocket.Server({ port: 8080 }); console.log('WebSocket server started on port 8080'); // 이 특정 인스턴스의 모든 연결된 WebSocket 클라이언트를 보유할 배열 const clients = new Set(); wss.on('connection', ws => { console.log('Client connected'); clients.add(ws); // 이 인스턴스에서 클라이언트로부터 메시지가 수신될 때 ws.on('message', message => { console.log(`Received message from client: ${message}`); // Redis 채널에 메시지 게시 redisPublisher.publish('chat_messages', message.toString()); }); ws.on('close', () => { console.log('Client disconnected'); clients.delete(ws); }); ws.on('error', error => { console.error('WebSocket error:', error); }); }); // 다른 인스턴스에서 오는 메시지에 대한 Redis 채널 구독 redisSubscriber.subscribe('chat_messages', (err, count) => { if (err) { console.error("Failed to subscribe to Redis channel:", err); } else { console.log(`Subscribed to ${count} Redis channel(s).`); } }); // Redis에서 메시지가 수신될 때 (모든 인스턴스에서 게시됨) redisSubscriber.on('message', (channel, message) => { console.log(`Received message from Redis channel "${channel}": ${message}`); // 이 메시지를 이 특정 인스턴스에 연결된 모든 클라이언트에게 브로드캐스트 clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(message.toString()); } }); }); // 정상 종료 처리 process.on('SIGINT', () => { console.log('Shutting down WebSocket server...'); wss.close(() => { console.log('WebSocket server closed.'); redisPublisher.quit(); redisSubscriber.quit(); process.exit(0); }); });
이것을 테스트하려면 다른 포트에서 이 서버의 여러 인스턴스를 실행할 수 있습니다(예: 포트 8080의 경우 node server.js
, 다른 인스턴스의 경우 포트를 8081로 수정). 일반적으로 이러한 인스턴스 앞에 로드 밸런서를 사용하지만, 기본적인 테스트의 경우 직접 연결로 충분합니다.
예제 클라이언트 (HTML/JavaScript)
<!-- index.html --> <!DOCTYPE html> <html> <head> <title>WebSocket Chat</title> </head> <body> <h1>WebSocket Chat</h1> <input type="text" id="messageInput" placeholder="Type your message..."> <button onclick="sendMessage()">Send</button> <ul id="messages"></ul> <script> // WebSocket 인스턴스 중 하나에 연결, 예: 포트 8080 또는 8081 const ws = new WebSocket('ws://localhost:8080'); // 또는 다른 인스턴스의 경우 ws://localhost:8081 ws.onopen = () => { console.log('Connected to WebSocket server'); }; ws.onmessage = event => { const messagesList = document.getElementById('messages'); const listItem = document.createElement('li'); listItem.textContent = event.data; messagesList.appendChild(listItem); }; ws.onclose = () => { console.log('Disconnected from WebSocket server'); }; ws.onerror = error => { console.error('WebSocket error:', error); }; function sendMessage() { const input = document.getElementById('messageInput'); const message = input.value; if (message) { ws.send(message); input.value = ''; } } </script> </body> </html>
애플리케이션 시나리오
이 아키텍처는 다음을 위해 유익합니다:
- 실시간 채팅 애플리케이션: 메시지가 서버 인스턴스 간의 모든 참가자에게 전달되도록 보장합니다.
- 라이브 대시보드: 서버 연결에 관계없이 모든 연결된 뷰어에게 데이터 포인트를 업데이트합니다.
- 협업 편집: 동일한 문서를 작업하는 여러 사용자 간의 변경 사항을 동기화합니다.
- 게임: 게임 상태 업데이트를 모든 플레이어에게 브로드캐스트합니다.
결론
Node.js WebSocket 서비스에 Redis Pub/Sub를 통합함으로써 단일 인스턴스 배포의 한계를 효과적으로 극복합니다. 이 강력한 패턴은 수평 확장을 가능하게 하고, 내결함성을 개선하며, 분산 시스템 전반에 걸쳐 원활한 실시간 통신을 보장합니다. Redis를 중앙 메시지 브로커로 활용하면 각 WebSocket 인스턴스가 독립적으로 작동하면서도 전역적으로 통신할 수 있어, 실시간 애플리케이션을 매우 확장 가능하고 복원력 있게 만들 수 있습니다. 현대적이고 고성능의 웹 서비스를 구축하기 위한 강력한 조합입니다.