11-2. Redis 를 통한 브로드캐스팅
이제 우리는, 새 포스트가 작성되었을 때 모든 접속된 클라이언트들에게 데이터를 전달해주어야합니다. 이 작업을 브로드캐스팅이라고 부릅니다. 브로드캐스팅을 구현하는 방법은 몇가지로 나뉘어질 수 있습니다.
- 각 소켓을 배열에 넣고 루프를 돌려서 데이터 전달
- EventEmitter 를 통해 이벤트를 준비하고 각 소켓에 이벤트 리스너를 적용시켜서 이벤트 발생 시 데이터 전달
- Redis / MQTT 를 통해 웹서버의 외부에서 발행/구독 (publish / subscribe) 를 통하여 데이터 전달
각 방식은 서로 장단점들이 있습니다. 1번의 경우엔 소켓들을 관리하는 로직을 구현해야합니다. 접속하면 배열 혹은 객체 안에 넣어주고, 연결이 끊기면 해당 소켓을 빼내주어야 합니다.
2번의 경우에는 소켓들을 따로 관리해야 하는 로직이 필요 없기 때문에 구현방식이 꽤나 간단합니다.
1번과 2번의 경우엔 구현하는데에 있어서 큰 어려움은 없긴 하지만, 문제점이 한가지 있습니다. 바로, 서버의 확장성(scalability)이 없다는 것 입니다. Node.js 으로 실행된 프로세스는 기본적으로 사용가능한 메모리가 약 1.5GB 로 제한이 되어있습니다. --max-old-space-size
를 통하여 메모리 제한을 늘릴 수 있긴 하지만, cluster
기능을 통하여 자식 프로세스들을 생성하여, 멀티코어 시스템을 충분히 활용하여 성능을 끌여올리는것이 권장됩니다.
또한, 어플리케이션의 규모가 정말 커진다면, 서버 컴퓨터도 여러대를 두고 로드 밸런싱을 통하여 네트워크쪽의 부하도 분산시켜줄 수 있겠죠.
1번과 2번의 경우엔 위 방법으로 작업을 분산시키는게 불가능합니다. 메모리에서 관리되고있기 때문에 단일 프로세스에서만 작동하게되지요.
만약에 서버의 규모를 확장하는것을 계획하고 있다면, 3번의 방식, redis, 혹은 mqtt 등의 서버를 통해 관리를 하는것이 적합합니다.
우리는 Redis 를 사용 할 건데요, 다행히도 구현방식은 그렇게 복잡하지 않습니다.
Redis 설치하기
macOS
# 설치
$ brew install redis
# 실행
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
# 컴퓨터 재시작마다 실행
$ ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents
Windows
윈도우용 Redis 를 https://github.com/MicrosoftArchive/redis/releases 에서 설치파일을 통하여 설치하세요.
설치 과정에서 Add the Redis installation folder to the PATH environment variable 체크박스를 체크하시면 자동으로 PATH 등록이 되어 나중에 터미널상에서 redis 를 바로 실행 할 수 있습니다.
터미널에서 접속해보기
;
설치후에, redis-cli
를 실행해서 ping 을 입력해보세요. PONG 이라고 나타나면 설치 및 실행이 정상적으로 된 것 입니다.
윈도우에서는 만약에 PATH 등록이 제대로 안되었다면, Program Files\redis 경로에 들어가서 직접 실행을 하세요.
redis npm 패키지 설치
우리의 웹서버에서 redis 에 접속하기 위해 redis
패키지를 설치하겠습니다.
$ yarn add redis
발행자와 구독자 설정
발행자(publisher) 는 Redis 서버에 접속하여 특정 채널에 데이터를 넣어주고, 구독자(subscriber)는 특정 채널을 주시하고있다가 데이터를 받게 됐을 때 리스너를 통하여 주어진 작업을합니다.
이제 우리 웹소켓쪽 코드를 조금 수정해봅시다.
src/ws/index.js
const Router = require('koa-router');
const redis = require('redis');
// 두개의 redis 클라이언트 생성
const publisher = redis.createClient();
const subscriber = redis.createClient();
subscriber.subscribe('posts'); // posts 채널 구독
const ws = new Router();
let counter = 0;
ws.get('/ws', (ctx, next) => {
// 유저가 접속하면 이 코드들이 실행됩니다
ctx.websocket.id = counter++; // 해당 소켓에 id 부여
ctx.websocket.send('Hello, user ' + ctx.websocket.id);
// 유저가 메시지를 보냈을때
ctx.websocket.on('message', function(message) {
// publisher 를 통하여 posts 채널에 메시지를 발행
publisher.publish('posts', message);
});
// 구독자가 message 받을 때 마다 해당 소켓에 데이터 전달
subscriber.on('message', (channel, message) => {
ctx.websocket.send(channel + '|' + message);
});
// 유저가 나갔을 때
ctx.websocket.on('close', () => {
console.log(`User ${ctx.websocket.id} has left.`);
});
});
module.exports = ws;
테스팅
이제 클라이언트가 웹소켓을 통하여 서버에 연결 한 다음에, 데이터를 전송하면 현재 접속되어있는 모든 유저들에게 데이터가 전달됩니다. 한번 페이지를 두개 열고, 각 개발자도구 콘솔에서 다음 스크립트를 실행해보세요.
function createSocket() {
const socket = new WebSocket('ws://localhost:4000/ws');
socket.onmessage = (msg) => console.log(msg);
socket.onopen = () => console.log('connected to socket');
return socket;
}
const s = createSocket();
양 쪽에서 위 코드를 통하여 소켓 접속을 한 다음에, 한쪽에서 다음 코드로 메시지를 전달해보세요:
s.send('hello world');
그렇게 하면 다른 한쪽에서도 hello world 가 나타나게 됩니다.
MessageEvent {isTrusted: true, data: "posts|hello", origin: "ws://localhost:4000", lastEventId: "", source: null…}
우리가 만들 흐름 프로젝트에서는, 클라이언트가 소켓을 통하여 서버로 send
하는 일은 없습니다. 그 대신에, 포스트를 새로 생성하는 API 를 실행시켰을 때 publisher 를 통하여 새 데이터를 발행하면 됩니다.