[DevBid - 실시간통신] 멀티 서버 환경에서 Redis Pub/Sub이 필요한 이유
📚 실시간 경매 시스템 구축기 시리즈
- WebSocket으로 실시간 경매 구현하기
- WebSocket 메시지 동기화 문제와 Redis 선택 ← 현재 글
- Redis 분산락과 Pub/Sub으로 멀티서버 동시성 제어
- Redis 도입 후 마주한 문제와 해결
- 멀티서버 환경에서 Pub/Sub와 분산락 검증
현재 구현의 한계
현재는 단일 서버 환경에서 Spring 의 @EventListener 와 WebSocket 을 조합해서 실시간 입찰과 즉시구매 동기화를 구현했습니다. 지금은 완벽하게 동작하지만..
만약 서비스가 성장하면서 트래픽이 증가하면 필연적으로 멀티서버 환경을 구축해야 합니다.
왜 서버를 나눠야 할까?
Devbid 같은 경매 서비스는 특정 시간대에 트래픽이 몰리게 됩니다.
- 인기 경매 마감 직전: 동시 접속자 급증
- 새로운 경매 오픈: 대량의 조회 요청
- 실시간 입찰 경쟁: 빈번한
WebSocket메시지
단일 서버 환경으로는 이런 순간적인 부하를 감당하기 어렵다 생각
- CPU - 입찰 검증, 비즈니스 로직 처리
- 메모리:
WebSocket연결 유지 (연결당 리소스 점유) - 네트워크: 실시간 메시지 전송
일반적인 해결책은 서버를 여러 대로 늘리고 로드밸런서로 트래픽을 분산시키는 것
즉, 수평적 확장(Scale-out)
그런데 문제가 있다.
서버를 여러 대로 늘리는 순간, 현재의 WebSocket 구현은 작동하지 않는다…
Spring Event의 작동 방식
1
2
3
4
5
6
7
8
9
10
11
12
13
public class AuctionWebSocketEventListener {
private final SimpMessagingTemplate messagingTemplate;
@EventListener //인메모리 이벤트
public void handleBidPlacedEvent(BidPlacedEvent event) {
log.info("입찰 이벤트 수신: {}", event);
//DTO 생성
...
//같은 JVM 프로세스 내에서만 전달됨
messagingTemplate.convertAndSend("/topic/auctions/" + event.getAuctionId(), dto);
log.info("WebSocket 전송 이벤트 종료..");
}
}
Spring의@EventListener는 같은 애플리케이션 컨텍스트(JVM 프로세스) 내에서만 이벤트를 전파- 서버1 에서 발생한 이벤트는 서버1의 메모리에만 존재
- 서버2는 서버1의 메모리에 접근할 수 없음
- 각 서버는 완전히 독립적인
Java프로세스
각 서버는 독립적인 JVM 사용
실제로 벌어질 상황
1
2
3
4
[서버 A - 8080포트] [서버 B - 8081포트]
├─ 유저1 (WebSocket 연결) ├─ 유저2 (WebSocket 연결)
├─ 유저3 (WebSocket 연결) ├─ 유저4 (WebSocket 연결)
└─ 유저5 (WebSocket 연결) └─ 유저6 (WebSocket 연결)
시나리오: 유저1이 “맥북” 경매에 100만원 입찰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. 유저1 → 서버A: "100만원 입찰!"
↓
2. 서버A: 입찰 처리 완료
↓
3. 서버A: BidPlacedEvent 발행 (서버A의 메모리에만 존재)
↓
4. 서버A: @EventListener가 이벤트 수신
↓
5. 서버A: WebSocket으로 전송
├─ 유저1 "100만원 입찰됨!" 받음
├─ 유저3 "100만원 입찰됨!" 받음
└─ 유저5 "100만원 입찰됨!" 받음
서버B는
├─ 유저2 아무것도 모름 (여전히 95만원으로 보임)
├─ 유저4 아무것도 모름
└─ 유저6 아무것도 모름
이게 왜 심각한 문제인가?
1
2
3
4
5
6
7
14:00:01 - 유저1이 100만원 입찰 (서버A에서 처리)
- 서버A 유저들: 100만원으로 갱신
- 서버B 유저들: 여전히 95만원으로 보임
14:00:05 - 유저2 (서버B): "95만원이네? 100만원 입찰!"
→ DB: "현재가가 이미 100만원인데?"
→ 입찰 실패
- 경매 서비스에서 실시간 가격 동기화가 안 되면, 유저는 계속 실패만 경험해야 됩니다.
서버 증설의 의미 퇴색
서버를 아무리 늘려도, 각 서버가 독립적으로 동작하면서 서버 간에 알림이나 이벤트 정보를 공유하지 못하면 문제가 발생합니다.
이 때문에 한 서버에서 처리된 알림이 다른 서버에 전달되지 않아 일부 유저는 실시간 알림을 전혀 받지 못하게 됩니다.
메시지 브로커
여러 서버가 공통으로 메시지를 주고받을 수 있는 중앙 메시지 브로커가 필요합니다.
이를 구현하는 방식은 여러 가지가 있습니다.
Message Queue (RabbitMQ, Kafka)
1
서버A → RabbitMQ → 서버B, C, D
장점
- 메시지 영속성 보장 (메시지 유실 방지)
- 복잡한 라우팅 지원
- 메시지 재처리 가능
단점
- 설정과 운영이 복잡
- 추가 인프라 관리 부담
- 실시간 알림에는 오버스펙
언제 사용?
- 메시지가 절대 유실되면 안 되는 경우 (주문, 결제)
- 메시지 처리 순서가 중요한 경우
- 메시지를 나중에 다시 처리해야 하는 경우
Database Polling
1
모든 서버가 주기적으로 DB 조회 -> 새로운 입찰 있으면 WebSocket 전송
장점
- 추가 인프라 불필요
- 구현이 단순
단점
- DB에 지속적 부하
- 실시간성 떨어짐 (폴링 주기에 의존)
- 경매처럼 빠른 갱신이 필요한 서비스에는 부적합
Redis Pub/Sub
1
서버A -> Redis -> 서버B, C, D
장점
- 빠르다 - 메모리 기반, 밀리초 단위 메시지 전파
- 가볍다 - 설정이 간단하고 러닝커브 낮음
- 실시간 최적화 - Pub/Sub은 실시간 메세징 용도로 설계
- 범용성 - 대부분의 서비스가 캐싱 용도로 사용 중
단점
- 메시지 영속성 없음 - 발행 시점에 구독자가 없으면 메시지 소실
- 메시지 재처리 불가능 - 과거 메시지 조회 및 재전송 불가
DevBid는 Redis Pub/Sub을 선택했습니다.
경매 입찰, 즉시구매 등 실시간 알림의 특성을 고려하면
- 실시간성 제일 중요 - 100ms라도 빠른 전달이 사용자 경험에 직결
- 메시지 유실되어도 치명적이지 않음 - 다음 입찰 알림이 곧 도착
- 메시지 재처리 필요 없음 - 과거 입찰 알림을 다시 볼 이유가 없음
- 단순 브로드캐스팅만 필요 - 복잡한 메시지 라우팅이나 필터링 불필요
실시간 알림이라는 핵심 요구사항과 멀티 서버 확장성을 고려했을 때, Redis Pub/Sub이 가장 적합한 선택이라고 판단했습니다.
향후 확장 가능성
Redis Pub/Sub 인프라를 구축해두면 다른 실시간 기능도 쉽게 추가할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
실시간 채팅 (경매 Q&A)
→ Channel: "chat:room:{roomId}"
→ 채팅방별로 메시지 격리 전달
사용자별 알림 (입찰 성공/실패, 낙찰)
→ Channel: "notifications:user:{userId}"
→ 개인화된 알림 푸시
경매 상태 변경 (종료, 연장)
→ Channel: "auction:status:{auctionId}"
메시지 영속성이 필요하면?
서비스가 성장하며 채팅 이력 보관, 알림 재전송 등의 요구사항이 생길 수 있습니다. 그때는
- Redis Streams로 전환 (Pub/Sub + 영속성 제공)
- 하이브리드: 입찰은 Redis Pub/Sub, 채팅은 Kafka 사용
현재는 Redis Pub/Sub만으로 충분하지만, 아키텍쳐 설계 시 항상 확장 가능성을 염두해 두는 것이 중요하다 생각됩니다.
다음 단계
이제 실제로 Redis Pub/Sub를 DevBid에 어떻게 적용했는지 구현 과정과 발생한 문제들을 다음 포스팅에서 확인할 수 있습니다.