채팅 구현 프로그램

황상익·2025년 2월 19일

Redis + Socket.I/O

SSE VS Socket

  • SSE

    • 단방향 (서버 -> 클라이언트) & HTTP 기반
    • HTTP 기반 (Keep alive)
    • 단순한 텍스트
    • 각 클라이언트마다 하나의 HTTP 연결 필요
    • 서버에서 과도한 데이터 전송시, 클라이언트 쉽게 과부하 발생
    • 단일 서버 부하 증가 => HTTP Keep-Alive 연결을 유지해야 함 → 서버당 최대 연결 개수 제한됨
    • 추후 ALB 사용시 부하 분산이 어려움. (특정 서버로 계속 요청이 전달이 됨)
    • 서버 다운시 연결이 끊기고 재연결
  • Socket.I/O

    • 양방향 (서버 <-> 클라이언트)
    • 독립적인 WebSocket 프로토콜
    • WebSocket Connection 유지
    • JSON 텍스트 지원
    • 재연결이 필요할때 직접 핸들링 해야함
    • 서버 부하 클 수 있음
    • Stateful 연결로 부하 분산 어려움
    • LB를 거친다 해도 세션이 유지되지 않으면 메시지가 특정 서버에만 전달. => Sticky Session(고정 세션) 필요
    • Scale-Out 시 클라이언트가 다른 서버로 라우팅되면 세션이 끊어짐
    • WebSocket은 특정 서버와 연결된 클라이언트만 메시지를 받을 수 있음
      → 다른 서버에 연결된 클라이언트와 동기화하려면 Redis, Kafka 같은 Pub/Sub 시스템 필요

🔥 WebSocket에 Redis를 사용한 이유 (Scale-Out 대응)🔥

WebSocket은 양방향 실시간 통신이 가능하지만, 서버 확장(Scale-Out) 시 문제 발생.

  • 서버간 클라이언트 연결을 공유할 방법 필요
  • Redis를 Pub/Sub 방식으로 사용, WebSocket 확장성 해결

Redis를 사용한 WebSocket 확장

Redis의 Pub/Sub(발행-구독) 기능을 사용하면 서버 간 메시지 공유 가능 → 여러 WebSocket 서버를 확장해도 모든 클라이언트가 메시지를 받을 수 있음

  1. WebSocket 서버에서 메시지를 수신하면 Redis에 발행
  2. 모든 WebSocket 서버가 Redis의 채널을 구독 (Subscribe)
  3. 서버가 많아도 모든 클라이언트가 메시지를 받을 수 있음

WebSocket + Redis 구조 (흐름도)

Client A -> WebSocket -> Server -> Redis[Publish] -> Redis Channel "chat" 
														   ⏬
Client B -> WebSocket -> Server -> Redis Subcribe -> Redis Channel "chat"

ChatWebSocketHandler

  • Socket 연결되면 해당 세션 저장
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        String sessionId = session.getId();
        sessions.put(sessionId, session);
        log.info("✅ WebSocket 연결됨: 세션 ID: {}", sessionId);
    }
  • 클라이언트가 메시지를 보내면 로그를 남김
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
        log.info("📩 메시지 수신: {}", message.getPayload());

        String payload = message.getPayload();
        if ("ping".equalsIgnoreCase(payload)) {
            session.sendMessage(new TextMessage("pong"));
            log.info("✅ Ping 요청에 대한 Pong 응답 전송");
            return;
        }

        chatMessagePublisher.publish("chat", payload);
    }
  • 해당 WebSocket 종료시 해당 세션을 삭제
  @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        sessions.remove(session.getId());
        log.info("🚪 WebSocket 연결 종료됨: 세션 ID: {}", session.getId());
    }

ChatMessageListener

  • MessageListener 인터페이스를 통해 Redis PUB/SUB 메시지 수신
  • Redis에서 메시지 수신 하면 onMessage 호출
@Slf4j
@Component
@RequiredArgsConstructor
public class ChatMessageListener implements MessageListener {

    private static final Map<<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

    /**
     * MessageListener 인터페이스를 통해 Redis PUB/SUB 메시지 수신
     * Redis에서 메시지 수신하면 onMessage 메서드 호출
     * @param message
     * @param pattern
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String receivedMessage = new String(message.getBody());
        log.info("📩 Redis 메시지 수신: {}", receivedMessage);

        // 연결된 모든 WebSocket 클라이언트에 메시지 전송
        for (WebSocketSession session : sessions.values()) {
            try {
                session.sendMessage(new TextMessage(receivedMessage));
                log.info("WebSocket으로 메시지 전송: {}", receivedMessage);
            } catch (IOException e) {
                log.error("WebSocket 메시지 전송 실패", e);
            }
        }
    }
}

ChatMessagePublisher

  • WebSocket에서 받은 메시지를 Redis pub/sub을 통해 발행
@Slf4j
@Component
@RequiredArgsConstructor
public class ChatMessagePublisher {

    private final RedisTemplate<String, String> redisTemplate;

    /**
     * WebSocket에서 받은 메시지를 Redis Pub/Sub을 이용해 발행하는 역할
     * @param channel
     * @param message
     */
    public void publish(String channel, String message) {
        redisTemplate.convertAndSend(channel, message);
        log.info("📤 Redis에 메시지 발행: {} → {}", channel, message);
    }
}

WebSocketConfig

@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {

    private final ChatWebSocketHandler chatWebSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatWebSocketHandler, "/ws/chat")
                .setAllowedOrigins("*");  // 모든 출처 허용 (개발용)
    }
}

흐름 정리

클라이언트 -> WebSocket -> ChatWebSocketHandler.handleTextMessage() 
-> chat 채널로 메시지 발행 -> ChatMessagePublisher -> 
ChatMessageListener가 Redis 메시지 구독 -> WebSocket 클라이언트에게 메시지 전송 : onMessage
profile
개발자를 향해 가는 중입니다~! 항상 겸손

0개의 댓글