부제 : 다중 인스턴스 환경에서 웹소켓 통신 문제를 해결하기 위한 접근 방법
🔍 문제 상황
나는 카카오톡 채널 상담 서비스를 웹소켓 기반으로 개발 중이었다.
로컬 개발 환경에서는 웹소켓 통신이 완벽하게 동작했으며,
동시에 10,000건의 메시지를 보내도 유실 없이 전달되었다.
하지만 QA 서버에 배포하자 웹소켓 데이터가 간헐적으로 유실되는 문제가 발생했다.
처음에는 비동기 처리나 네트워크 속도 문제로 의심했지만,
웹소켓은 TCP 기반이라 신뢰성이 보장되는 프로토콜이기 때문에
단순한 네트워크 이슈는 아니라고 판단했다.
🔎 원인 분석
문제의 원인을 파악하기 위해 검색해보니,
비슷한 사례가 해외 개발자 커뮤니티에서도 논의되고 있었다.
📌 결론:
배포 환경에서는 PM2를 사용한 다중 인스턴스 구조로 운영되고 있었고,
각 웹소켓 서버가 서로 다른 프로세스에서 실행되면서 클라이언트 연결 정보가 공유되지 않았던 것이 원인이었다.

즉, 클라이언트가 웹소켓을 통해 서버에 연결되었을 때,
서버 A에 연결된 클라이언트는 서버 B에 있는 클라이언트와 직접 통신할 수 없었다.
이 때문에 웹소켓을 통한 메시지가 일부 유실되는 문제가 발생했다.
💡 해결 방법: Redis Pub/Sub 도입
웹소켓 메시지를 모든 인스턴스가 공유할 수 있도록
Redis Pub/Sub을 활용하기로 결정했다.
📌 Redis Pub/Sub의 역할
• 각 웹소켓 서버가 메시지를 Redis에 발행(Publish)
• Redis가 메시지를 모든 웹소켓 서버에 전달(Subscribe)
• 각 서버가 받은 메시지를 클라이언트에게 전송

이 방식이면 모든 인스턴스가 동일한 메시지를 받아 처리할 수 있기 때문에
서로 다른 서버에 연결된 클라이언트 간에도 데이터 유실 없이 웹소켓 통신이 가능하다.
🛠️ 구현 코드: Redis Pub/Sub 설정
NestJS에서 ioredis 와 graphql-redis-subscriptions 라이브러리를 사용하여
Redis Pub/Sub을 설정했다.
import { ConfigService } from '@nestjs/config';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
/**
* Redis Pub/Sub 설정 함수
* @param configService - 환경변수 설정 서비스
* @returns RedisPubSub 인스턴스
*/
export const pubSub = async (
configService: ConfigService,
): Promise<RedisPubSub> => {
const redisHost = configService.get<string>('REDIS_HOST') || 'localhost';
const redisPort = configService.get<number>('REDIS_PORT') || 6379;
const options = {
host: redisHost,
port: redisPort,
retryStrategy: (times: number): number => {
console.error(`🔄 Retrying Redis connection... Attempt: ${times}`);
return Math.min(times * 100, 3000); // 0.1초 ~ 3초 사이 재연결 시도
},
reconnectOnError: (err: Error): boolean => {
console.error('⚠️ Redis encountered an error:', err);
return true; // 에러 발생 시 자동 재연결
},
maxRetriesPerRequest: null,
enableReadyCheck: false,
};
// Redis 클라이언트 생성 (Publisher & Subscriber)
const pubClient = new Redis(options);
const subClient = new Redis(options);
// 이벤트 리스너 등록 (연결 성공 및 실패 감지)
pubClient.on('connect', () => console.log('✅ Redis Publisher connected!'));
pubClient.on('error', (err) => console.error('❌ Redis Publisher Error:', err));
pubClient.on('end', () => {
console.warn('⚠️ Redis Publisher disconnected. Reconnecting in 3s...');
setTimeout(() => pubClient.connect(), 3000);
});
subClient.on('connect', () => console.log('✅ Redis Subscriber connected!'));
subClient.on('error', (err) => console.error('❌ Redis Subscriber Error:', err));
subClient.on('end', () => {
console.warn('⚠️ Redis Subscriber disconnected. Reconnecting in 3s...');
setTimeout(() => subClient.connect(), 3000);
});
// Redis 연결 테스트
try {
await pubClient.ping();
console.log('✅ Successfully connected to Redis!');
} catch (error) {
console.error('❌ Redis 초기 연결 실패:', error);
process.exit(1);
}
return new RedisPubSub({
publisher: pubClient,
subscriber: subClient,
});
};
✅ 적용 후 효과
Redis Pub/Sub을 적용한 후, QA 서버에서도 웹소켓 데이터 유실 없이 100% 전달되는 것을 확인했다.
✅ 이전 문제점
• 로컬 개발 환경에서는 웹소켓이 정상 동작
• 하지만 QA/배포 환경에서 다중 인스턴스 간 메시지 손실 발생
• PM2 다중 프로세스 환경에서 서버 간 클라이언트 연결 정보가 공유되지 않음
• 웹소켓 연결이 각 개별 서버에서만 유지되었기 때문에 서버가 다르면 데이터가 유실됨
• 엄밀히 말하자면, 데이터가 유실되는것은 아니다.! 서로 다른 인스턴스간 데이터를 동기화하지 못했고, 그렇게 본인과 연결되지 않은 클라이언트에게 데이터를 전달하지 못한거 일뿐
✅ Redis Pub/Sub 적용 후
• 모든 웹소켓 서버가 동일한 메시지를 받아 처리할 수 있게 됨
• 서로 다른 서버에 연결된 클라이언트들도 동일한 메시지를 받을 수 있음
• 웹소켓 메시지가 서버 인스턴스 간 자동으로 공유되면서 데이터 유실 문제 해결
🔥 결론
“로컬에서는 잘 되는데 배포하면 웹소켓 데이터가 유실되는 이유?”
👉 PM2 다중 인스턴스 환경에서 웹소켓 연결 정보가 공유되지 않기 때문!
“어떻게 해결할 수 있을까?”
👉 Redis Pub/Sub을 활용하여 웹소켓 메시지를 모든 인스턴스에서 공유하도록 구성!
📌 Redis Pub/Sub을 적용한 결과:
• 웹소켓 데이터 유실 0% -> 사실 따지고보면 유실은 아니지만
• 다중 인스턴스 환경에서도 모든 클라이언트가 동일한 메시지 수신
• 안정적인 실시간 카카오톡 채널 상담 서비스 구축 가능 🚀
'Dev > etc' 카테고리의 다른 글
매직마우스 감도 조절하는 방법 (0) | 2023.12.25 |
---|---|
비스트의 12시30분은 틀렸다. (0) | 2023.11.15 |
[MAC] 특정 포트 열기 (0) | 2023.10.07 |
[MAC] 사용중인 포트 찾아서 죽이기 (0) | 2023.07.26 |
Maven Central에 배포 정리. (0) | 2023.07.11 |