어떤 하나의 서비스를 할 때 하나의 서버를 두는 것이 아닌 여러 대의 서버를 두는 경우가 있다.
해당 서비스가 소켓 통신을 통한 채팅 서비스를 제공한다고 생각해보자. A와 B 두 사람이 채팅을 한다고 할 때 서버가 한대라면 문제가 없다. 그런데 서버가 여러 대이고 각자 다른 서버에 연결이 되어 있다면? 그럴 때는 채팅을 어떻게 할 수 있을까?
이럴 때 redis의 pub/sub 기능을 사용해 이를 해결할 수 있다.
Redis
redis는 인메모리 데이터 베이스로 거대한 맵 데이터 저장소 형태를 가지고 데이터를 메모리에 저장하여 빠른 읽기와 쓰기를 지원한다. 싱글 스레드로 동시성 이슈가 발생하지 않고 해시 테이블을 사용하기 때문에 매우 빠른 속도로 데이터 검색이 가능하다. 이러한 특징으로 캐시 서버로 많이 쓰이지만 pub/sub 기능도 제공하여 상술한 문제를 해결할 수 있다.
redis의 pub/sub은 쉽게 말해 메세지를 발행하고 구독하는 서비스이다. 해당 시스템에서 동일한 채널을 여러 구독자가 구독하면 해당 채널로 발행된 메세지가 모든 구독자에게 발송된다. 따라서 서버가 여러 대여도 redis의 pub/sub 기능을 통해 메세지를 주고 받을 수 있다.
이를 로컬에서 실습해보기 위해 도커를 활용해보자. express 서버를 구현해본적 있다고 가정하고 디렉토리 구조를 먼저 보자.
server 디렉토리에는 express 서버 코드가 작성될 것이다.
docker-compose.yml 파일을 먼저 보면
version: "3.8"
services:
redis:
image: redis:latest
container_name: redis-server
restart: always
ports:
- "6379:6379"
app1:
build: ./server
container_name: express-app1
restart: always
ports:
- "3001:3000"
depends_on:
- redis
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
- SERVER_NAME=Server1
app2:
build: ./server
container_name: express-app2
restart: always
ports:
- "3002:3000"
depends_on:
- redis
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
- SERVER_NAME=Server2
이 docker-compose.yml 파일은 Redis와 2개의 Express 서버 (app1,app2)를 컨테이너로 실행하여 pub/sub을 테스트 할 수 있는 환경을 구성한다. 제일 처음엔 redis 컨테이너를 구축하여 pub/sub의 중간 허브 역할을 하게 한다.
그 다음 app1, 첫 번째 express 서버이다. depends_on: redis를 통해 Redis가 실행된 후 app1 이 실행되게 했고 redis를 통해 다른 서버와 메세지를 주고 받게 한다. 포트는 3001이다.
마지막은 app2, 두 번째 express 서버이다. 위와는 대부분 동일하고 포트는 3002이다.
이제 express 서버를 구현해보자. server.js 파일이다.
require("dotenv").config();
const express = require("express");
const Redis = require("ioredis");
const app = express();
const redisPub = new Redis({
// Publisher 용 Redis
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});
const redisSub = new Redis({
// Subscriber 용 Redis
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});
const CHANNEL_NAME = "chat";
const SERVER_NAME = process.env.SERVER_NAME || "Unknown";
app.use(express.json());
// 메시지 발행 (Publisher)
app.post("/publish", (req, res) => {
const { user, text } = req.body;
const message = { server: SERVER_NAME, user, text, timestamp: new Date() };
redisPub.publish(CHANNEL_NAME, JSON.stringify(message)); // redisPub 사용!
console.log(`[${SERVER_NAME}] Published:`, message);
res.json({ status: "Message sent", message });
});
// 메시지 수신 (Subscriber)
redisSub.subscribe(CHANNEL_NAME, (err, count) => {
if (err) {
console.error("Subscription failed:", err);
} else {
console.log(`[${SERVER_NAME}] Subscribed to ${count} channel(s).`);
}
});
redisSub.on("message", (channel, message) => {
const parsedMessage = JSON.parse(message);
// 자신이 보낸 메시지는 무시 (서버 이름 비교)
if (parsedMessage.server === SERVER_NAME) return;
console.log(`[${SERVER_NAME}] Received from ${channel}:`, parsedMessage);
});
// 서버 실행
app.listen(3000, () => {
console.log(`[${SERVER_NAME}] Server running on port 3000`);
});
일단 redis 클라이언트를 pub용, sub용 두 개를 만들어준다. 이렇게 해주는 이유는 redis 클라이언트 (ioredis)는 subscribe()를 호출하면 Subscriber 모드로 전환이 된다. 이 상태에서는 일반적인 redis 명령어 (set, publish, get 등)를 사용할 수 없게 된다. 즉 pub/sub을 수신하는 redis 인스턴스(subscriber)에서 publish()를 실행하면 다음 에러가 발생한다.
Error: Connection in subscriber mode, only subscriber commands may be used
따라서 publisher와 subscriber를 분리한 redis 인스턴스로 관리해야 한다. publish용 redis 클라이언트는 일반 명령어를 실행하고(publish()) subscriber용 redis 클라이언트는 메세지만 수신하게 해야 한다.(subscirbe())
그 다음 채널 이름을 정의해준다.
그러고서 route를 정의해준다. '/publish' post 요청으로 http request 요청을 하게 되면 해당 body 값을 redis의 publish 클라이언트로 메세지를 발행하게 된다.
그 다음에는 redis.subscribe로 해당 채널을 구독하게 한다. chat 채널에 메세지가 들어오면 redis.on("message")가 실행되게 된다. "message"라는 문자열은 redis의 pub/sub 이벤트 리스너에 정해진 이벤트 타입으로 redis에서 메세지를 수신할 때 반드시 "message" 이벤트를 사용해야 한다.
그리고 server 디렉토리의 Dockerfile
FROM node:18
WORKDIR /app
# 패키지 파일 복사 및 설치
COPY package.json package-lock.json ./
RUN npm install
# 소스 코드 복사
COPY . .
# Express 서버 실행
CMD ["node", "server.js"]
docker-compose up --build 명령어로 컨테이너를 빌드하고 실행해보자.
이제 3001번 서버로 request를 보내보자.
console.log를 확인해보자.
app1(3001 포트)에서 메세지를 발행했고 app2(3002 포트)에서 메세지를 받아 로그가 찍힌 것을 볼 수 있다. 반대로 한번 보내보자.
이번에는 app2에서 발행한 메세지를 app1이 받아 로그가 찍힌 것을 볼 수 있다.
다만 redis의 pub/sub에는 한가지 단점이 있는데 바로 한번 발송된 메세지가 저장이 되지 않는다는 것이다. 그래서 redis에서는 streams 라는 기능을 제공하여 메세지를 저장하고 소비자가 나중에라도 읽을 수 있게 하지만 이럴거면 메세지 큐를 쓰는게 더 좋다고 한다.
전체 코드는 아래 링크에서 확인 가능하다.
https://github.com/chobkyu/node-redis-multiserver
공부하면서 정리한 내용입니다. 틀린 내용이 있다면 댓글로 남겨주세요 감사합니다:)
'CS > DataBase' 카테고리의 다른 글
[DB] MVCC(다중 버전 동시성 제어) (0) | 2025.03.26 |
---|---|
[SQL] SQL 실행 순서에 대해서 (0) | 2025.02.18 |
[DataBase] 외래키(Foreign Key)에 관한 고찰... (0) | 2025.01.06 |
[DataBase] 관계형 데이터 베이스 (2) | 2024.08.06 |
[MySQL] UPDATE 쿼리 시 에러코드 1175 처리 (0) | 2024.06.19 |