SSE VS Socket
SSE
Socket.I/O
WebSocket은 양방향 실시간 통신이 가능하지만, 서버 확장(Scale-Out) 시 문제 발생.
Redis의 Pub/Sub(발행-구독) 기능을 사용하면 서버 간 메시지 공유 가능 → 여러 WebSocket 서버를 확장해도 모든 클라이언트가 메시지를 받을 수 있음
Client A -> WebSocket -> Server -> Redis[Publish] -> Redis Channel "chat"
⏬
Client B -> WebSocket -> Server -> Redis Subcribe -> Redis Channel "chat"
@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);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session.getId());
log.info("🚪 WebSocket 연결 종료됨: 세션 ID: {}", session.getId());
}
@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);
}
}
}
}
@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);
}
}
@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