본문 바로가기

학습 기록 (Learning Logs)/Today I Learned

채팅 서버

최종 구조

+-----------+             +--------------+
|  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 구조

흐름

  1. 서버 A는 메시지를 Kafka에 produce
  2. 모든 서버들(B 포함)은 Kafka consumer로 메시지를 consume
  3. 각 서버는 자기가 관리하는 유저에게만 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으로 메시지 전달

 

핵심 흐름

  1. 유저 A가 WebSocket을 통해 메시지를 보냄 → 서버 A
  2. 서버 A는 메시지를 Kafka에 produce
  3. 모든 WebSocket 서버가 Kafka를 consume
  4. 메시지를 받으면 자기 서버에 연결된 대상 유저가 있는지 확인 후 WebSocket으로 전달
  5. 동시에 메시지를 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 등을 사용

WebSocketHttpSession에 자동으로 접근하지 않음

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