1. 개요
- 실시간으로 사용자에게 알림을 전달하는 SSE(Server-Sent Events) 기반의 알림 시스템을 구축함.
- 로드밸런서(ELB)를 사용하는 다중 인스턴스(Multi-instance) 환경에서 문제에 직면함.
- 본 문서는 해당 문제의 원인을 분석하고, 이를 해결하기 위해 Redis Pub/Sub과 Kafka를 도입하여 아키텍처를 개선한 과정을 기록함.
2. 문제 상황
최초 아키텍처
- 사용자 ID(
Long)를 Key로, SseEmitter 객체를 Value로 갖는 ConcurrentHashMap을 사용하여 실시간 연결을 관리했음.
- 알림을 보내야 할 때,
SseEmitterService의 send() 메소드가 Map에서 사용자의 Emitter를 찾아 직접 메시지를 전송하는 단순한 구조였음.
문제점
- 로드밸런서 뒤에서 여러 인스턴스가 실행되자, 사용자가 최초에 인스턴스 A와 SSE 연결을 맺었더라도 알림 전송 요청은 인스턴스 B로 라우팅될 수 있었음.
- 인스턴스 B의
ConcurrentHashMap에는 해당 사용자의 Emitter 정보가 없으므로, emitters.get(userId) 결과는 null이 되어 알림이 전송되지 않고 유실됨.
- 결과적으로 사용자는 알림을 받기도 하고 못 받기도 하는, 예측 불가능한 상황에 놓임.
3. 해결 과정
1단계: 인스턴스 간 상태 공유 문제 해결 (Redis Pub/Sub 도입)
- 원인 분석: 각 인스턴스의 메모리에 Emitter를 저장하는 것이 문제의 근본 원인임을 파악. Emitter 객체 자체는 직렬화하여 공유할 수 없으므로, 서버 간에 알림 이벤트를 전달할 '신호'가 필요했음.
- 해결책: 인스턴스 간의 실시간 메시지 전달을 위해 Redis Pub/Sub을 도입함.
- 동작 흐름 변경:
- 알림 전송 요청이 들어오면, 특정 Emitter를 찾는 대신
NotificationDto를 Redis의 특정 채널(예: alarm-channel)에 발행(Publish) 하도록 변경함.
- 모든 인스턴스는
RedisMessageListenerContainer를 통해 해당 채널을 구독(Subscribe) 하고 있도록 설정함.
- 메시지를 수신한 모든 인스턴스의
RedisNotificationSubscriber는 자신의 메모리에 해당 사용자의 Emitter가 있는지 확인하고, 가진 인스턴스만이 최종적으로 클라이언트에게 알림을 전송함.
- 결과: 이로써 어떤 인스턴스가 알림 요청을 받더라도, 실제 연결을 가진 인스턴스가 응답할 수 있게 되어 알림 유실 문제가 해결됨.