이미지 출처 : https://asfirstalways.tistory.com/359
WebSocket은 웹 앱과 서버 간의 지속적인 연결(persistent connection) 을 유지하여 양방향 통신을 가능하게 하는 프로토콜입니다. 기존 HTTP 통신의 단점을 보완하여, 클라이언트와 서버가 서로 데이터를 실시간으로 주고받을 수 있는 구조를 제공합니다.
초기 웹 통신은 Request → Response 구조인 HTTP 프로토콜 기반의 단방향 통신이었습니다. 실시간 알림, 채팅 등에서는 다음과 같은 문제가 있었습니다:
이러한 문제를 해결하기 위해 WebSocket이 등장했습니다.
양방향 통신 (Full-Duplex)
→ 서버와 클라이언트가 동시에 데이터를 주고받을 수 있습니다.
지속적인 연결 유지
→ HTTP와 달리, 요청-응답 후 연결이 끊기지 않고 계속 유지됩니다.
헤더 오버헤드 없음
→ 한번 연결되면 그 후에는 가벼운 패킷만 주고받아 효율적입니다.
실시간 서비스에 최적화
→ 채팅, 게임, 주식 시세, 알림 등 실시간성이 중요한 서비스에 적합합니다.
서버 부하 증가
WebSocket은 연결을 계속 유지하기 때문에, 수천~수만 개의 커넥션을 동시에 처리할 경우 서버 리소스를 많이 차지합니다. (polling을 이용했을 때의 부하랑 다름)
연결 상태 관리 필요
클라이언트 연결이 끊기거나 에러가 발생할 경우 재연결 로직을 프론트에서 구현해야 하며, 서버에서도 상태를 적절히 관리해야 합니다.
WebSocket은 초기에는 HTTP로 연결 요청을 보내고, Upgrade 헤더를 통해 WebSocket으로 프로토콜 전환을 요청합니다.
요청 예시:
GET /chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
응답 예시:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
→ 이 응답이 오면 클라이언트와 서버는 WebSocket 연결을 맺고 실시간으로 통신할 수 있습니다.
WebSocket 연결 이후에는 클라이언트와 서버가 자유롭게 데이터를 주고받습니다. 이때 전송되는 데이터는 메시지(message) 단위로 나뉘며, 내부적으로는 여러 프레임(frame) 으로 구성됩니다.
어느 한 쪽에서 종료 요청을 보내면 WebSocket 연결은 닫히며 양측 모두 close 이벤트를 받습니다.
WebSocket은 실시간 통신에 뛰어난 성능을 자랑하지만, 채팅방 구현처럼 메시지를 주제별로 구독/발행하거나, 사용자별 라우팅, 접속/해제 상태 관리까지 직접 처리해야 하는 경우에는 한계가 있습니다.
이를 해결하기 위한 프로토콜이 STOMP (Simple Text Oriented Messaging Protocol) 입니다.
STOMP는 WebSocket 위에서 동작하는 메시징 프로토콜로, HTTP처럼 명확한 명령과 구조를 제공합니다. 메시지는 텍스트 기반이며 헤더와 바디로 구성되어, 서버와 클라이언트 간 통신을 더 구조화할 수 있습니다.
CONNECT: 클라이언트가 서버에 연결할 때 사용SEND: 특정 목적지로 메시지를 보냄SUBSCRIBE: 특정 주제(채널)를 구독하여 메시지를 받을 수 있음UNSUBSCRIBE: 구독 해제DISCONNECT: 연결 종료즉, WebSocket이 단순히 데이터를 보내는 통신선을 만든다면, STOMP는 그 위에서 "메시지를 어떻게 전달하고 받아야 하는지에 대한 규칙"을 만드는 역할입니다.
Spring에서는 STOMP를 이용해 다음과 같은 고급 메시징 기능을 구현할 수 있습니다:
@MessageMapping으로 서버 내 라우팅 구성/pub/... 주소로 메시지를 보내고,/sub/... 주소를 구독한 클라이언트에게 실시간 메시지를 전달SimpMessagingTemplate으로 처리STOMP 덕분에 서버 코드는 단순해지고, 프론트는 주제만 잘 구독하면 실시간 데이터를 받을 수 있게 됩니다.
실제로 제가 구현한 실시간 채팅 기능은 다음과 같은 구성입니다:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/chat")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
@Controller
@RequiredArgsConstructor
public class ChatSocketController {
private final ChatMessageService chatMessageService;
private final UserService userService;
private final SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat/send")
public void send(@Payload ChatMessageSocketDto dto) {
User sender = userService.getUserById(dto.getSenderId());
ChatMessageResponseDto savedMessage = chatMessageService.sendMessage(dto.getRoomId(), sender, dto.getContent());
messagingTemplate.convertAndSend("/sub/chat/room/" + dto.getRoomId(), savedMessage);
}
}
@MessageMapping, SimpMessagingTemplate, 그리고 STOMP 조합으로 구독 기반 채팅 시스템을 깔끔하게 구현할 수 있음/sub/...)을 구독하고 메시지를 /pub/...으로 보내는 방식으로 쉽게 연동 가능