최종 구조
+-----------+ +--------------+
| Client 가 |──WebSocket→ | WS Server A |──→ Kafka Produce
+-----------+ +--------------+ │
│ │ ▼
│ └→ Redis SET [ Kafka ]
│ │
▼ ▼
+-----------+ +--------------+ +--------------+
| Client 다 |←────────────| WS Server C |←───────| Kafka Consumer|
+-----------+ +--------------+ +--------------+
▲
Redis GET ws:user:나
[WS Server App]
├── WebSocket Server
├── Kafka Producer (메시지 발행)
└── Kafka Consumer (메시지 수신 후 WebSocket 전송)
[Client 가] ─── WebSocket ───> [WS 서버 A]
|
1. Redis 저장: "user가 → ws-server-A"
|
2. 메시지 Kafka Produce
v
[Kafka Topic]
|
3. 모든 WS 서버 Kafka Consumer
|
4. 수신 서버가 Redis 조회:
"user다 는 ws-server-C에 연결돼 있음"
|
5. gRPC or REST → [WS 서버 C]
|
6. WebSocket으로 user다 에게 메시지 전송
✅ Kafka 토픽이 같더라도, 서버마다 다른 groupId를 쓰면 브로드캐스트가 된다.
멀티 서버 환경에서의 주요 고려사항
WebSocket 연결 분산
Sticky session 필요 (같은 유저가 동일 서버에 연결되도록)
또는 모든 서버가 메시지를 받을 수 있도록 Pub/Sub
Message Ordering
Kafka partition key로 채팅방 단위 정렬 가능
Failover / 장애 복구
메시지 중복 수신 처리, 연결 재시도, 메시지 재전송 메커니즘
보안
WebSocket 인증(JWT 토큰 등), 메시지 암호화, HTTPS 및 WSS 적용
오프라인 메시지 처리
오프라인 상태의 유저는 Push 알림 / 재접속 시 메시지 배달 등
WebSocket 서버 간 브로드캐스트 필요
- 서버 A, 서버 B 두 대가 있고
- user 가 서버 A에 WebSocket 연결
- 유저 다는 서버 B에 WebSocket 연결
그런데 user 가이 유저 다에게 메시지를 보내면? 서버 A는 user 다 가 어디에 붙었는지 모름
WebSocket은 연결 기반이라, 다른 서버에 연결된 유저에게는 직접 보낼 수 없음.
그래서 모든 WebSocket 서버가 서로 메시지를 공유하는 방법이 필요함
Kafka 구조
흐름
- 서버 A는 메시지를 Kafka에 produce
- 모든 서버들(B 포함)은 Kafka consumer로 메시지를 consume
- 각 서버는 자기가 관리하는 유저에게만 WebSocket으로 메시지 전송
장점
- 메시지 내구성 (durability) 보장 → 유실 위험 없음
- 병렬 처리, 파티셔닝으로 대규모 서비스에 적합
- 메시지 순서 보장 가능 (채팅방 기준으로 partition key 설정)
단점
- Kafka는 Redis보다 **지연(latency)**이 살짝 있음
- Kafka와 WebSocket 연동 구조는 상대적으로 복잡함
Kafka: 메시지 전달의 중심축
- 모든 WebSocket 서버는 Kafka에 메시지를 보내고,
- 동시에 Kafka의 메시지를 consume
{
"senderId": "user가",
"receiverId": "user디",
"roomId": "room1",
"message": "안녕!",
"timestamp": 123456789
}
user가 -> 서버A에서 publish
kafkaTemplate.send("chat-messages", chatRoomId, messageJson);
WebSocket 서버 Kafka Consumer → 라우팅 판단
@KafkaListener(topics = "chat-messages", groupId = "chat-consumers")
public void consume(String messageJson) {
ChatMessage msg = parse(messageJson);
String targetServer = redis.get("ws:user:" + msg.getReceiverId());
if (targetServer.equals(thisServerId)) {
// 자기한테 붙어있는 유저면 직접 전송
sendViaWebSocket(msg.getReceiverId(), messageJson);
} else {
// 다른 서버에 있으면 → HTTP 또는 gRPC로 전달
forwardToServer(targetServer, messageJson);
}
}
구성 방식 | 설명 | 추천 상황 |
같은 Spring 애플리케이션에 Producer & Consumer✅ |
WebSocket 서버가 메시지를 Kafka로 보내고, 동시에 Kafka 메시지를 받아서 다른 유저에게 전달 |
소규모 시스템, 유지관리 간편✅ |
Producer와 Consumer를 서로 다른 서비스로 분리 |
WebSocket 서버는 Kafka에 메시지를 보내기만 하고, 별도의 Consumer 서비스가 메시지를 처리함 |
대규모 채팅 서비스 (확장성, 분산처리, 장애 분리) |
시나리오 | 추천 방식 |
MVP, 스타트업 초기✅ | 같은 Spring 애플리케이션에서 Producer & Consumer 함께✅ |
사용자 수 증가 / 수만 명 이상 | WebSocket 서버는 Producer 전용, 별도 Consumer 서버로 분리 |
100만 이상 MAU / 수천 TPS | WebSocket 서버 간에도 Redis Pub/Sub or Kafka로 세션 브로드캐스트 + Consumer 역할 분리까지 완전한 MSA 구조 |
Producer & Consumer✅
[Spring Boot App A]
├── WebSocket Server
├── Kafka Producer (메시지 발행)
└── Kafka Consumer (메시지 수신 후 WebSocket 전송)
- 유저 123 → 서버 A에 붙음
- 서버 A: WebSocket, Producer, Consumer 모두 있음
- 서버 A에서 메시지 보내고, 받는 것도 자기 안에서 처리 → 세션 공유 필요 없음
장점
- 구조가 단순하고 빠르게 개발 가능
- WebSocket 세션을 직접 관리하면서 바로 메시지 브로드캐스트 가능
단점
- 트래픽 증가 시 확장성에 한계
- Consumer 실패나 처리 지연이 WebSocket 처리에 영향을 줄 수 있음
분리된 구조 (Microservices 구조)
분리된 구조 (Microservices 구조)
[Spring Boot - WebSocket 서버] → Kafka ← [Spring Boot - Consumer 서버]
(Producer 역할) (Consumer 역할)
- 유저 123 → 서버 A에 붙음
- 서버 A는 Kafka에 메시지를 Produce만 함
- 서버 B (Consumer)가 메시지 Consume
- 유저 123이 서버B(Consumer)에 붙어 있지 않으면 직접 못 보냄
- → 이 메시지를 다시 서버A(Producer)로 보내야 함 → "라우팅" 필요
WebSocket 서버
- 클라이언트와 WebSocket 유지
- 메시지 전송 시 Kafka로 produce만 함
Consumer 서버
- Kafka에서 메시지 consume
- Redis or shared memory를 통해 WebSocket 서버로 전달하거나,
- WebSocket 서버도 Kafka consume 해서 브로드캐스트
장점
- 확장성 좋음: Producer와 Consumer 각각 따로 scale-out 가능
- 장애가 분리되어 안정성 높음
- Consumer 서버를 기능별로 분리 가능 (e.g., 저장용, 알림용 등)
단점
- 구현 난이도 상승 (특히 WebSocket 서버 간 세션 공유 or 라우팅 필요)
- 메시지 전달을 위한 추가 Pub/Sub이나 세션 클러스터링 필요
메시지 라우팅 방식
유저가 어느 서버에 붙었는지 알아내야 함 → Redis 기반 유저-서버 매핑 테이블✅
Redis 기반 유저-서버 맵핑 테이블
- 예: user:1→ ws-server-1
- 메시지를 받은 Consumer가 해당 유저의 WebSocket 서버를 알아냄
Consumer가 WebSocket 서버가 아닌 경우, 실제 메시지를 보낼 수 없음
해당 유저가 붙은 서버로 다시 메시지를 전달해야 함
서버 간 메시지 전파 방법
방식 | 설명 | 단점 |
Redis Pub/Sub | 서버끼리 메시지 브로드캐스트 | 단일 Redis는 SPOF, 클러스터 구성 필요 |
Kafka로 WebSocket 서버도 Consumer 구성 | 모든 WS 서버가 Kafka 메시지를 consume해서, 자기가 가진 세션이면 전달 | 확장성 좋음, but 모든 서버가 전체 consume |
gRPC / HTTP로 지정 서버에 직접 전달 | user-server map 보고 직접 전송 | 라우팅 코드 필요, 서버 간 호출 비용 |
✅ 하나의 Spring Boot 프로젝트
- WebSocket 서버 + Kafka Producer + Kafka Consumer → 모두 포함
✅ 여러 인스턴스로 띄움
- ex) ws-server-1, ws-server-2, ws-server-3 …
[Client]
|
| WebSocket 연결
v
[Spring Boot 서버 A] ← Kafka 메시지 consume <--> 유저 1
[Spring Boot 서버 B] ← Kafka 메시지 consume
[Spring Boot 서버 C] ← Kafka 메시지 consume <--> 유저 2
- 유저 1 → 서버 A에 연결
- 유저 2 → 서버 C에 연결
- 유저 1이 유저 2에게 메시지를 보냄
- 서버 A가 Kafka에 메시지 produce
- 서버 B, C도 Kafka에서 메시지 consume
- 서버 C는 유저 2가 자기한테 연결된 걸 확인하고 WebSocket으로 메시지 전달
핵심 흐름
- 유저 A가 WebSocket을 통해 메시지를 보냄 → 서버 A
- 서버 A는 메시지를 Kafka에 produce
- 모든 WebSocket 서버가 Kafka를 consume
- 메시지를 받으면 자기 서버에 연결된 대상 유저가 있는지 확인 후 WebSocket으로 전달
- 동시에 메시지를 DB에 저장, 푸시 발송, 읽음 처리 등은 별도의 Consumer Thread에서 수행
장점
- WebSocket 세션 공유 필요 없음
- Kafka가 메시지를 모든 서버에 전달하므로 별도 라우팅 필요 없음
- 유저 수 증가에 따라 WebSocket 서버 scale-out 가능
- 구현이 단순하고 안정적
장애 대응 전략
항목 | 전략 |
Kafka 다운 | KRaft 모드에서 3노드 구성, replication 필수 |
메시지 유실 | acks=all, retries 설정, idempotent producer |
순서 보장 | Partition key를 채팅방 단위로 설정 |
서버 장애 | WebSocket 연결 재시도, 오프라인 메시지 처리 |
WebSocket 세션을 어디서 관리할지
- WebSocket 서버 자체에 둘지 (1차)
- Redis 같은 외부 세션 스토어를 쓸지 (2차)
1차: WebSocket 세션은 인메모리로 관리
Map<String, WebSocketSession> sessionMap;
if (sessionMap.containsKey(userId)) {
sessionMap.get(userId).sendMessage(...);
}
세션 비교
WebSocket 세션과 Spring 세션은 같은가요?
아니요
WebSocket에서 Spring의 HttpSession 쓸 수 있나요?
거의 불가능. 대신 JWT 기반 인증이 일반적
WebSocket 연결 유저를 서버에서 어떻게 관리하나요?
WebSocketSession을 Map에 저장해서 직접 관리해야 함
항목 | WebSocket 세션 | Spring HTTP 세션 |
전송 프로토콜 | WebSocket (ws, wss) | HTTP (GET, POST 등) |
연결 유지 | 상시 연결 (실시간) | 요청마다 연결 (stateless → state 유지 위해 세션 사용) |
관리 주체 | 개발자가 직접 관리 (Map<String, WebSocketSession> 등) | 서블릿 컨테이너가 자동 관리 (Tomcat, Spring) |
세션 수명 | WebSocket 연결 종료 시까지 | 기본 30분 (요청 없을 경우 만료, 설정 가능) |
사용 목적 | 실시간 데이터 송수신 | 로그인 유지, 상태 저장 |
인증 방식 | 직접 구현 (JWT, 쿼리 파라미터 등) | 자동 지원 (Spring Security + 세션 or 쿠키 기반) |
저장 위치 | 수동 Map 관리 (서버 인메모리) | 자동 관리 (서블릿 컨테이너, Redis 등 가능) |
WebSocket 세션 (WebSocketSession)
정의 | 클라이언트와 WebSocket 연결이 성립된 후 생성되는 세션 |
클래스 | org.springframework.web.socket.WebSocketSession |
특징 | - 지속적인 연결 유지 (Full-Duplex) - WebSocket 핸드셸 성공 후 서버가 관리 - 서버가 수동으로 세션을 닫기 전까지 연결 유지 |
사용 예시 | - 실시간 채팅 - 실시간 알림 (Notification) - 게임 등 실시간 통신 |
저장 위치 | 서버 메모리 (인메모리 Map 등으로 직접 관리) |
ID | session.getId()로 접근 |
인증 방식 | WebSocket 연결 직전 HTTP 요청의 헤더나 쿼리 파라미터를 통해 JWT 등을 사용 |
WebSocket은 HttpSession에 자동으로 접근하지 않음
→ JWT를 헤더나 쿼리 파라미터로 전달해서 인증 처리
new WebSocket("ws://localhost:8080/chat?token=abc.def.ghi")
private final ConcurrentHashMap<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
@OnOpen
public void handleConnect(WebSocketSession session) {
String userId = extractUserIdFromToken(session); // JWT 파싱
sessionMap.put(userId, session);
}
Spring HTTP 세션 (HttpSession)
정의 | 브라우저가 HTTP 요청을 통해 서버와 상태를 유지하기 위해 생성하는 세션 |
클래스 | javax.servlet.http.HttpSession |
특징 | - HTTP 기반 요청 간 상태 유지를 위해 사용 - 로그인 상태, 사용자 정보 등을 저장 - JSESSIONID 쿠키로 클라이언트와 연동 |
사용 예시 | - 로그인 인증 상태 유지 - 장바구니, 임시 폼 데이터 등 |
저장 위치 | 서버 메모리, Redis 등 |
ID | session.getId() |
인증 방식 | 기본적으로 JSESSIONID 쿠키 또는 Spring Security 기반 세션 관리 |
WebSocket 세션 인메모리로 관리하면 문제 생김
- WebSocket 세션은 HTTP 세션처럼 자동 관리되지 않음 → 직접 관리 + 외부 저장소 필요
- 실서비스에서는 Redis를 세션 메타데이터 저장소로 자주 씀
- 서버 scale-out, 장애 복구, 라우팅을 위해 꼭 필요한 구조
문제점
- 서버 재시작하면 연결 정보 날아감
- 서버 여러 대면 세션이 분산돼 있음 (서버 A에 연결된 유저는 서버 B가 모름)
- failover, reconnect, load balancing 문제 발생
1. Redis 기반 세션 저장소를 사용해야 함
// WebSocket 연결 시 userId 기준으로 세션 정보를 Redis에 저장
redis.set("ws:session:{userId}", serverId or session metadata)
- 유저가 어느 서버에 연결돼 있는지 추적
- 서버 간 라우팅 or 전달 가능
- 유저 강제 로그아웃, 푸시 메시지 라우팅 등도 가능
2. Redis 역할: 유저-서버 매핑 테이블
- 유저가 WebSocket 연결하면, 토큰에서 userId 추출해서 현재 서버 ID와 함께 Redis에 저장
Redis 키 | 값 |
ws:user:1234 | ws-server-1 |
ws:user:4567 | ws-server-2 |
[Client A] ─── WebSocket ───> [WS 서버 A]
|
1. Redis 저장: "userA → ws-server-A"
|
2. 메시지 Kafka Produce
v
[Kafka Topic]
|
3. 모든 WS 서버 Kafka Consumer
|
4. 수신 서버가 Redis 조회:
"userB는 ws-server-C에 연결돼 있음"
|
5. gRPC or REST → [WS 서버 C]
|
6. WebSocket으로 userB에게 메시지 전송
3. redis로 session 사용시 보완할 점
Redis 단일 장애
Redis Sentinel, Cluster 구성
서버 ID 하드코딩
서버 부팅 시 자기 ID 등록하거나 hostname 기반 결정
'학습 기록 (Learning Logs) > Today I Learned' 카테고리의 다른 글
Done is better than perfect (0) | 2025.04.03 |
---|---|
유튜브에서 자막 퍼오기 (0) | 2025.04.03 |
gRPC (0) | 2025.04.01 |
MQTT (0) | 2025.04.01 |
7장 스케쥴링 (0) | 2025.03.31 |