기존 개발용으로 간단히 구현했던 채팅 서비스를 이번에 redis로 scale-out 가능하게 수정할 일이 생겼다.
이 참에 구현했던 과정을 기록해놔야겠다 생각해서 정리해봤다.
- 웹소켓을 이용해 클라이언트와 서버 사이에서 채팅 메시지 송수신
- 메시지를 전송할 수 있는 API
- 채팅 메시지를 db에 저장
- 사용량에 따라 스케일 아웃 가능
위 기능들이 필요한데 express와 socket.io, redis 채팅 서비스를 만들고 Terraform으로 aws ecs에 배포까지 할 예정이다.
websocket 서버의 scale-out


채팅 서비스는 웹 소켓으로 클라이언트와 통신하고, socket-io로 구현되어있다. Client A, B가 웹소켓 서버를 통해 데이터를 주고 받으면서 서로에게 메시지를 전달할 수 있다.
http는 통신의 비연결성 덕분에 스케일 아웃에 큰 문제가 없지만 소켓통신은 그렇지 않기 때문에 문제가 생긴다.
http 서버가 세션을 사용할 때도 마찬가지 문제가 있다. 이때는 세션 클러스터링을 사용하기도 하는데 소켓서버도 redis를 이용해 비슷하게 문제 해결이 가능하다.
위 그림처럼 2개 이상의 websocket 서버가 존재하고 어떤 식의 loadbalancer로 부하분산을 하고 있는 상태라면 Client A와 B는 각자 다른 서버에 연결되어 서로 메시지를 주고 받지 못하게 된다.
이 때 redis pub/sub을 이용해서 서로 다른 소켓서버에 연결 된 두 클라이언트의 메시지 교환 역할을 한다.
각 서버가 redis로부터 메시지를 subscription하여 클라이언트에게 전달하고, 클라이언트에게 받은 메시지를 publish하여 서버 간에 메시지를 공유할 수 있다.
Redis 인스턴스 생성
redis는 aws elasticache를 사용했다. 개발을 위해 사용할 목적이라 최소사양으로 생성한다.
서브넷 그룹은 vpc를 새로 생성해서 지정했다. 이 서브넷들은 나중에 ecs 노드들이 생성될 곳이기도 하다.


간단한 채팅 서비스 구현
chat-test 란 이름의 node package를 생성하고 index.ts 파일을 작성한다.
.env 파일에 호스트와 포트, 토큰을 적어준다.
#chat-test 생성
mkdir chat-test
cd chat-test
npm init
#node package 설치
npm i express socket.io ioredis
#ts package 설치
npm i --save-dev @types/cors @types/express
npm i ts-node
//index.ts
require('dotenv').config();
import { Server } from "socket.io"
import express from 'express';
import http from 'http'
import Redis from 'ioredis'
//redis 채널과 옵션
const CHANNEL = "user-msg"
const redisOptions = {
host: process.env.REDIS_HOST!,
port: +process.env.REDIS_PORT!,
password: process.env.REDIS_PASSWORD!,
tls: {}
}
//socket.io 클라이언트와 redis 클라이언트 생성
const io = new Server()
const pub = new Redis(redisOptions);
const sub = new Redis(redisOptions);
io.of('/chat').on('connection', (socket) => {
console.log(`connection from ${socket.handshake.address}`);
socket.emit('message', "connected!")
//메시지 수신하면 redis로 메시지 publish
socket.on("message", async (data) => {
await pub.publish(CHANNEL, JSON.stringify(data));
});
});
async function listen() {
await sub.subscribe(CHANNEL)
//redis로 메시지 수신하면 소켓으로 메시지 emit
sub.on("message", (channel, message) => {
const msg = `got message ${message} from "${channel}" channel`
io.of('/chat').emit(
"message",
msg
)
})
return http.createServer(express()).listen(3001);
}
listen().then(server => {
io.attach(server);
console.log(`Server listening`);
})
3000, 3001 두 개의 포트로 서버를 시작하고 포스트맨으로 접속해서 테스트를 해봤다.


서로 다른 서비스 사이에서 메시지를 주고받는 걸 확인할 수 있다.