김태오

Spring Boot 에서 간단한 WebSocket 구현 본문

Socket

Spring Boot 에서 간단한 WebSocket 구현

ystc1247 2023. 12. 21. 00:51

우선 WebSocket 을 정의하면 persistent TCP connection 으로 이루어지는 Bi-Directional, full-duplex communication protocol 이다. 

 

잘 와닿지 않으니 정의를 분해하여 이해해보자.

 

우선 persistent 라 하면, 계속 연결을 유지한다는 뜻이다. REST 와 gRPC 등에서 single-request 이후 connection 이 종료되는 것과 달리 connection 이후 일정 기간 동안 client 와 server 간의 연결이 종료되지 않고 오픈되어있다.

 

Bi-Directional 은 양쪽의, 즉 client 와 server side 에서 각각의 traffic 을 모두 핸들링할 수 있다는 뜻이다. 일반 HTTP 에서 보통 client 가 요청을 보내고 server 가 응답을 보내는 것과 달리, bi-directional connection 에서는 server 가 요청받지 않은 (response 형식이 아닌) 메시지를 client 로 전송할 수 있다. 이는 notification, live feed 등에서 유용하게 쓰일 수 있다.

 

full-duplex 는 하나의 party 가 동시간에 메시지를 전송하고 전송받을 수 있다는 뜻이다. server 는 어떤 시간대에서는 메시지를 전송하고 전송받을 수 있고, client 또한 마찬가지이다. 이는 two-way connection 이 동시적이고 dynamic 한 data exchange 를 할 수 있도록 한다. HTTP 에서는 client 에서 server 로 요청을 보내면, server 가 프로세싱 후 response 를 전달할 때까지 기다려야 하며, server 또한 request 가 오기 전에 response 를 보낼 수 없으므로, half-duplex 라 할 수 있다.

 

즉 WebSocket 은 연결 후 message exchange 에 어떠한 조건도 작용하지 않는다.

 

우선 WebSocket 은 TCP 연결이기 때문에 handshake 로 시작을 한다. client 는 server 로 우선 일반적인 HTTP request 를 보내는데, 이 때 header 에 upgrade : Websocket 이 담겨있어야 한다. 이는 protocol 을 HTTP 에서 WebSocket 으로 변환해준다. 

GET /websocket HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

 

Websocket connection 은 http:// scheme 을 사용하지 않기 때문에 ws:// 혹은 wss:// 로 시작하는 endpoint 를 사용한다. wss 는 https 와 마찬가지로 secure Websocket connection 에 사용된다.

 

connection 을 종료할 때는 client 나 server 에서 "close frame" 을 전송한다. 이것에는 status code 가 담겨있다.

1000: Normal closure.
1001: Going away (like a server shutting down or a browser tab closing).
1002: Protocol error.
1006: Abnormal closure (no close frame received).

responding party 에서는 자신만의 close frame 을 전송한다. 양쪽의 프레임이 전송되면, TCP connection 이 종료되며, 양쪽의 party 는 연결과 연관된 모든 resource 가 제대로 release 되었다는 것을 보장해야 한다.

 

간단한 WebSocket config 이다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler(), "/websocket")
                .setAllowedOrigins("*");

    }
    @Bean
    org.springframework.web.socket.WebSocketHandler webSocketHandler() {
        return new WebSocketHandler();
    }
}

 

우선 registerWebSocketHandlers method 는 WebSocketConfigurer 에서 Override 되는데, 특정 URL 로 Websocket handler 를 매핑하기 위해 사용된다. 이 경우에는 /websocket 으로 mapping 하고 있으며, setAllowedOrigins("*") 로 모든 Origin 에서 연결을 가능케 하는 간단한 CORS configuration 을 추가했다. 

 

다음으로 Bean 으로 등록된 WebSocketHandler 이다.

 

@Slf4j
public class WebSocketHandler implements org.springframework.web.socket.WebSocketHandler {

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("Connection established on session: {}", session.getId());

    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        String tutorial = (String) message.getPayload();
        log.info("Message: {}", tutorial);
        session.sendMessage(new TextMessage("Started processing tutorial: " + session + " - " + tutorial));
        Thread.sleep(1000);
        session.sendMessage(new TextMessage("Completed processing tutorial: " + tutorial));

    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        log.info("Exception occured: {} on session: {}", exception.getMessage(), session.getId());

    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        log.info("Connection closed on session: {} with status: {}", session.getId(), closeStatus.getCode());

    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

}

 

client side 에서 메시지를 받아보지 않을 것이기 때문에 logging 을 위해 Slf4j annotation 을 추가했다. 메시지 핸들링, 연결 시작과 종료 등 WebSocketHandler 에서 몇 개의 method 를 Override 하였다. handleMessage method 에서는 payload 가 들어오면, 즉시 client 로 하나의 메시지를 전송하며, 1초 후 (여기에 어떠한 서버에서의 프로세스가 들어가는 것이다.) 메시지를 하나 더 전송한다.

 

Postman 에서 간단히 테스트해본다.

 

연결이 문제없이 완료되고, "test message" 라는 메시지를 전송하면 즉시 message 가 하나 날라오고, 1초 뒤 (thread.sleep을 설정해놓았기 때문에) message 가 하나 더 날라온다.

 

 

정상적인 로깅도 확인할 수 있다.