Search
📒

9-4. WebSocket

WebSocket은 AMQP, HTTP와 마찬가지로 Application Layer 상에 정의된 통신 규약의 일종입니다. 한번 연결을 통해 채널을 형성하고, 해당 채널에서 양방향으로 메시지를 전달하는 통신규약입니다. HTTP와는 달리 양방향에서 원하는 시점에 메시지를 보낼 수 있으며, 한 Enpoint에 여러 Client가 연결이 가능하다는 점에서 채팅과 같은 기능을 구현하는데 많이 사용됩니다.
WebSocket 체널을 형성하기 위해서, Client는 Server의 HTTP요청을 우선 보내게 됩니다. 이 HTTP 요청에는 Upgrade: websocket 이라는 Header를 포함하고 있으며, 이 요청을 받은 서버 측에서는 101 응답과 함께 WebSocket 메시지를 주고받을 준비를 하게 됩니다. 이후로는 어느 한쪽이 연결을 종료하기 전까지, 양방향(full-duplex)으로 메시지를 주고받을 수 있습니다. 이 과정을 WebSocket Handshake 요청이라고 합니다.
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
Plain Text
복사
Client Handshake 요청
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
Plain Text
복사
Server Handshake 응답
이후 연결된 체널을 통해서 요청과 응답을 주고 받게 됩니다. 이 과정에서 연결이 형성될 때, 메시지가 도달할 때, 연결이 종료될 때, 오류가 발생할 때를 기준으로 작동 방식을 정의하게 됩니다. 이렇게 Event에 대한 행동을 기준으로 행동양식을 정의하는 것을 Event Driven Programming 이라고 부릅니다.

STOMP

일반적인 WebSocket을 이용한 통신은 Socket API를 이용한 통신과 유사합니다. 우선 연결이 형성된다면, 어떤 형태의 데이터든 형식없이 전송하게 되며, 이는 개발자가 데이터를 직접 해석해야 함을 의미합니다. 그래서 WebSocket 요청을 주고받을때, HTTP 처럼 특정 형식에 맞는 데이터를 주고받자고 합의할 수 있습니다. 그중 Spring에서 지원하는 한가지는 STOMP(Simple/Streaming Text Oriented Message Protocol) 입니다.
COMMAND header1:value1 header2:value2 Body^@
Plain Text
복사
STOMP Frame 형식
STOMP에서는 WebSocket 상에서 주고받는 메시지를 Frame이라는 단위로 작성하는데, HTTP와 유사하게 작성하게 됩니다. COMMAND 는 HTTP 입장에서 보면 Request Method, 그 외에 headerBody 가 존재하는 점에서 유사성을 찾을 수 있습니다.
STOMP 규약을 사용함으로서, 저희가 Spring에서 @RequestBody , @ResponseBody 등의 Annotation을 통해 HTTP 요청 / 응답을 쉽게 조작하듯이, WebSocket 통신상의 메시지를 쉽게 해석할 수 있게 됩니다.

WebSocket Endpoint

다양하고 많은 URI로 정의되는 HTTP 서버와 달리, WebSocket은 한 URI 상에서 Handshake를 진행한 뒤, 해당 URI 위에서 지속적으로 메시지를 주고받게 됩니다. 여기에 STOMP에서는 Server에서 정의한 특정 경로에 대하여, 접속한 클라이언트 중 일부에 대해서만 메시지를 전달하기 위해 destination을 정의하게 됩니다.
SEND destination:/queue/test Hello from TCP! ^@
Plain Text
복사
WebSocket 연결을 이뤄낸 Client는 이후 Destination에 대하여 구독하여 특정 메시지만 받도록 할 수 있습니다.

Spring Boot WebSocket

Spring Boot에서 WebSocket을 지원하고자 한다면, 아래의 의존성을 추가하여야 합니다.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
Groovy
복사
이를 Spring Web과 함께 준비하여 프로젝트를 생성합시다.
또한 채팅 어플리케이션을 구성하기 위해 필요한 stomp.js 와 HTML 문서들을 첨부합니다. 해당 파일들을 프로젝트의 resource/static 경로에 배치하도록 합시다.
static.zip
7.5KB

WebSocketConfig

@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws/chat"); registry.addEndpoint("/ws/chat").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/receive-endpoint"); registry.setApplicationDestinationPrefixes("/send-endpoint"); }}
Java
복사
@EnableWebSocketMessageBroker 를 통해 Spring Boot에서 STOMP를 활용한 WebSocket 통신을 가능하게 합니다. WebSocketMessageBrokerConfigurer 를 구현함으로서, Spring Security에서 진행한것 처럼 미리 정의된 함수들을 통해 구성을 하게 됩니다.
regsiterStompEndpoints() 함수를 통해, 최초로 WebSocket Handshake를 진행할 Endpoint를 설정합니다. 또한 WebSocket을 지원하지 않는 브라우저를 위해서 withSockJS() 함수를 통해 SockJS 를 지원할 수 있습니다. SockJS 는 WebSocket을 지원하지 않는 브라우저에서, 일반 HTTP 규약을 가지고, WebSocket의 행동규칙을 흉내내는 객체를 제공해주는 라이브러리 입니다. WebSocket을 지원한다면 WebSocket을 사용합니다.
configureMessageBroker() 함수를 사용하면 destination 을 정의할 수 있습니다. 이중 setApplicationDestinationPrefixes() 의 경우 서버가 메시지를 받기 위한 Destination, enableSimpleBroker() 의 경우 클라이언트가 메시지를 받기 위해 구독할 Destination이라고 볼 수 있습니다. 단, Destination 자체에 구독하는 것이 아닌 Prefix(접두사) 형태로 구독할 수 있는 Destination이 적용됩니다.

WebSocketMapping

@Controller public class WebSocketMapping { private static final Logger logger = LoggerFactory.getLogger(WebSocketMapping.class); private final SimpMessagingTemplate simpMessagingTemplate; public WebSocketMapping(SimpMessagingTemplate simpMessagingTemplate) { this.simpMessagingTemplate = simpMessagingTemplate; } @MessageMapping("/ws/chat") public void sendRoom(ChatMessage chatMessage) { logger.info(chatMessage.toString()); final String time = new SimpleDateFormat("HH:mm").format(new Date()); simpMessagingTemplate.convertAndSend( String.format("/receive-endpoint/%s", chatMessage.getRoomId()), new ChatMessage(chatMessage.getRoomId(), chatMessage.getSender(), chatMessage.getMessage(), time) ); } }
Java
복사
WebSocket의 Endpoint도 @Controller 객체를 사용합니다. 다만 @RequestMapping 이 아닌 @MessageMapping 을 이용합니다. 여기서 기입하는 것은 WebSocketConfig 에서 구성했던 endpoint 의 값입니다. 이 endpoint와 WebSocketConfig 에서 구성한 enableSimpleBroker() 함수에서 정의했던 destination을 합쳐, 클라이언트가 메시지를 보내는 경로가 완성됩니다.
Client의 메시지를 받고, 필요한 작업을 처리한 뒤에는 SimpMessagingTemplate 객체를 이용하여 다시 Destination에 구독한 Client들에게 메시지를 전송할 수 있습니다.

테스트 해보기

실행 후 http://localhost:8080/ 으로 접근하면
와 같은 화면이 나오게 됩니다. 닉네임을 입력후 입장 버튼을 누르면 입장 가능한 방들이 나열되며, 이후 채팅을 진행할 수 있습니다. 두개의 탭을 이용해 채팅이 됨을 확인해 봅시다.