요약
다중 WAS 환경에서의 안정적인 실시간 알림 전송을 위한 Kafka 기반 SSE 아키텍처 구축기
본 포스팅은 Server-Sent Events(SSE)를 기반으로 하는 알림 시스템을 다중 WAS 환경에서도 안정적으로 운영하기 위해 Kafka를 도입한 전체 설계 및 구현 과정을 기술한 엔지니어링 아카이브이다.
특히 다음과 같은 내용을 중심으로 구성되어 있다.
- 단일 인스턴스에만 유효한 SseEmitter의 한계 → Redis 공유 불가
- Redis Pub/Sub 접근의 단점: 트랜잭션 불일치, 알림 유실 등
- Kafka 기반 브로커 아키텍처 도입: 알림 전송의 일관성, 확장성 확보
- 단일 파티션 + 고유 consumer group 방식을 통한 브로드캐스트 패턴 구현
- 멱등성, 순서 보장, DLQ 기반 재처리 구조 등 실전 운영 환경 최적화
- 알림 유실 없는 안전한 처리를 위한 DefaultErrorHandler, DeadLetterPublishingRecoverer 적용
단순한 메시지 전송을 넘어서 트랜잭션 정합성, 재전송 보장, 멀티 인스턴스 환경에서의 동시성 고려까지 포함하며, Kafka를 활용한 고신뢰 실시간 알림 시스템의 전체 구현 전략을 체계적으로 담고 있는 글이다.
목표
다중 WAS 환경에서도 안정적으로 SSE(Server-Sent Events) 알림을 전달할 수 있도록, 알림 전송 로직의 일관성과 확장성을 보장하는 이벤트 기반 아키텍처를 구축하고자 한다.
문제 정의
1. 단일 WAS 환경의 한계
SSE는 서버가 클라이언트로 실시간 알림을 push할 수 있는 방식으로, 서버 내 특정 객체(SseEmitter)를 통해 응답을 전송한다.
일반적으로 아래와 같은 방식으로 메모리에 저장된다.
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
emitters.put(userId, emitter);
- 단일 WAS에서는 문제 없이 운영
- 하지만 다중 WAS 환경에서는 클라이언트 A가 WAS1에 연결되어 있고, 알림은 WAS2에서 발생한다면?
- WAS2는 A의 SseEmitter를 가지고 있지 않기 때문에 알림을 전달할 수 없다.
❌ 실패했던 시도들
접근 1. Redis에 Emitter 공유 저장
- SseEmitter는 JVM 객체로, Redis에 직렬화하여 저장하거나 공유 불가능
- HTTP 연결 스트림은 특정 인스턴스에서만 유효
접근 2. Redis Pub/Sub 브로드캐스트
- 각 WAS 인스턴스가 Redis 채널을 구독하고, 알림 발생 시 메시지를 브로드캐스트
- → 메시지를 수신한 WAS가 Emitter가 존재할 경우 클라이언트에 전송
문제점
- DB 트랜잭션 롤백 후에도 Redis 메시지가 발행될 수 있어 트랜잭션 일관성 문제가 발생
- 알림 전송 실패 시 재전송 보장이 되지 않음
✅ 최적의 해결책: Kafka 기반 알림 브로커
설계 핵심
[API 서버] --(produce)--> [Kafka] --(consume)--> [SSE 서버] --(SSE)--> [Client]
- API 서버: 비즈니스 로직 처리 후 Kafka에 알림 발행
- SSE 서버: Kafka 토픽 구독 후 Emitter를 통해 실시간 전송
기대 효과
- 확장성: 느슨한 결합으로 WAS를 자유롭게 수평 확장 가능
- 신뢰성: Kafka의 내구성과 재처리 기능으로 실패 복원 가능
- 정확성: 순서 보장 + 중복 전송 방지
- 유실 방지: DLQ를 통한 오류 추적 및 재처리 가능
Kafka 설정 및 설계 전략
1. Topic 구성
| 항목 | 값 | 설명 |
| 이름 | sse-event | 알림 이벤트용 Kafka 토픽 |
| 파티션 | 1 | 순서 보장을 위한 단일 파티션 |
| 복제 | 3 | 장애 대응 및 고가용성 확보 |
2. Consumer 설정
| 항목 | 값 | 설명 |
| Group ID | WAS 인스턴스별 고유값 | 브로드캐스트 구조 |
| Offset reset | latest | 새 인스턴스는 최신 메시지만 수신 |
→ 각 WAS가 고유한 Group ID를 사용함으로써, 모든 인스턴스가 동일 메시지를 수신할 수 있음.
3. Producer 설정
| 항목 | 값 | 설명 |
| acks | all | ISR 모두 응답 시 전송 성공 처리 |
| retry | 3 | 일시적 오류 시 재시도 |
| idempotence | true | 중복 없는 전송 보장 |
| max.in.flight.requests | 3 | 순서 역전 방지 |
Kafka 토픽 설정 코드
@Bean
public NewTopic sseTopic() {
return TopicBuilder.name("sse-event")
.partitions(1)
.replicas(3)
.config("retention.ms", "3600000")
.config("min.insync.replicas", "2")
.build();
}
- partitions=1: 순서 보장을 위해 단일 파티션
- retention.ms=1시간: 알림은 단기 소비 이벤트
- min.insync.replicas=2: 브로커 1개 장애 시에도 정상 처리
Kafka Consumer 구성
@Bean
public ConsumerFactory<String, SseEventDto> sseConsumerFactory() {
return new DefaultKafkaConsumerFactory<>(commonConsumerProps(sseGroupId),
new StringDeserializer(),
new JsonDeserializer<>(SseEventDto.class));
}
@Bean(name = "sseKafkaListener")
public ConcurrentKafkaListenerContainerFactory<String, SseEventDto> sseFactory(
DefaultErrorHandler errorHandler) {
ConcurrentKafkaListenerContainerFactory<String, SseEventDto> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(sseConsumerFactory());
factory.setCommonErrorHandler(errorHandler);
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL); // 수동 커밋
return factory;
}
- AckMode.MANUAL: SSE 전송 성공 시에만 Kafka 오프셋 커밋
- 중복 알림 방지 + 장애 복원을 모두 고려한 설계
에러 핸들링과 DLQ 구성
실시간 전송 도중 오류가 발생해도 메시지 유실 없이 처리할 수 있도록 DefaultErrorHandler + DLQ 전략 구성
public DefaultErrorHandler sseEventErrorHandler(KafkaTemplate<String, SseEventDto> kafkaTemplate) {
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(
kafkaTemplate,
(record, ex) -> new TopicPartition("sse-dlq", record.partition())
);
FixedBackOff backOff = new FixedBackOff(1000L, 3); // 1초 간격, 3회 재시도
return new DefaultErrorHandler(recoverer, backOff);
}
동작 플로우
- Kafka 메시지 수신 → sseService.sendToClient() 수행
- 예외 발생 시 → DefaultErrorHandler 호출
- 최대 3회 재시도 실패 시 → sse-dlq 토픽으로 메시지 이관
- 운영자는 DLQ 로그를 기반으로 재처리 및 모니터링 가능
마무리
이번 Kafka 기반 SSE 알림 시스템 설계를 통해 다음과 같은 기술적 가치를 달성했다.
- 확장성: 다중 인스턴스 환경에서도 안정적인 실시간 알림 전달
- 신뢰성: 트랜잭션 일관성 + Kafka의 내구성으로 알림 유실 방지
- 운영 편의성: DLQ 기반의 장애 복원 및 모니터링 체계 마련
이 구조는 실시간 알림, 채팅, 메시징 등 다양한 서비스에 적용 가능하며, 대규모 트래픽에도 유연하게 대응할 수 있는 확장 가능한 기반이 될 수 있다.
끝.
'카카오테크 부트캠프' 카테고리의 다른 글
| DB 부하 98% 감소, 캐시 적중률 87%: 트래픽 집중형 시스템의 Redis 도입기 (3) | 2025.07.18 |
|---|---|
| 동적인 데이터 캐시 동시성 제어 전략 비교 및 분산 락 선택 (0) | 2025.07.15 |
| Redis 캐시, Cache Stampede 방지 전략 보고서 (TTL Jitter, Mutex Locking) (0) | 2025.07.13 |
| Redis 캐시 키 설계 관련 보고서: 사용자 domain 정보 처리 전략 (1) | 2025.07.11 |
| SSE 도입 후 궁금증 완전 해소 – Polling 한계 극복부터 Nginx 설정, Tomcat 병목까지 총정리 (1) | 2025.06.29 |