elevne's Study Note

Spring WebSocket 본문

Backend/Spring

Spring WebSocket

elevne 2023. 6. 28. 16:01

WebSocket 은 두 프로그램 간 데이터를 주고받기 위해 사용되는 통신 방법 중 하나이다. 양방향 통신 (통상적인 HTTP 는 단방향), 실시간 네트워킹을 지원한다.

 

 

웹소켓 이전에 사용된 비슷한 기술들이 있다. 첫 번째로 Polling 이라는 것이 있다. 이는 서버에 일정 주기로 요청을 송신, 응답을 받는 방법이다. 실시간 통신에서는 언제 통신이 발생할지 모르기 때문에 불필요한 Request, Connection 을 계속 생성한다. 그 다음으로는 Long Polling 이라는 것이 있다. 이는 서버에 요청을 보내고 응답을 받을 때까지 연결을 종료시키지 않는다. 응답을 받으면 끊고, 재용청한다. 어느정도 Polling 의 단점을 개선하기는 하였으나, 많은 양의 메시지가 흐를 경우 Polling 과 같은 단점이 생긴다. 또 Streamin 이라는 것도 있다. 이는 서버에 요청을 보내고 끊기지 않은 연결상태에서 끊임없이 데이터를 수신한다. 이는 클라이언트에서 서버로 데이터를 송신하기 어렵다는 단점이 있다. 이 세 방법들은 모두 HTTP 를 통해 통신하기 때문에 Request, Response 둘의 Header 가 불필요하게 큰 문제가 있다.

 

 

그렇다면 WebSocket 은 어떻게 동작하는 것일까? 이 또한 Handshaking 이 필요하다. Handshaking 과정은 WebSocket 이 아닌 HTTP 프로토콜로 이루어진다. GET 요청으로 보내고, Header 내에 Upgrade: websocket, connection: Upgrade 가 들어간다. Upgrade 란 현재 클라이언트, 서버, 전송 프로토콜 연결에서 다른 프로토콜 연결로 전환하기 위한 규칙이다. 

 

 

Handshake

 

 

응답이 101 switching protocol 로 오면 요청이 성공한 것이다.

 

 

Response

 

 

Handshake 가 완료되면 프로토콜이 ws 로 변경된다. (HTTPS 처럼 WSS 도 가능) WebSocket 통신을 위해 별도의 포트를 열어줄 필요는 없다. 이는 HTTP 또는 HTTPS 통신을 위해 오픈한 포트를 사용한다. 웹소켓은 HTTP 포트 80, HTTPS 443 위에서 동작하도록 설계되어있다.

 

 

WebSocket메시지라는 것을 주고받는다. 메시지는 여러 개의 frame 이 모여 구성하는 하나의 논리적 메시지 단위다. 여기서 frame 은 communication 에서 가장 작은 단위의 데이터로 작은 헤더와 페이로드로 구성된다.

 

 

 

WebSocket 은 HTML5 이후에 등장하였기 때문에, 그 이전의 기술들로 구성된 서비스에서는 이를 그대로 사용할 수 없다. 이 때는 Socket io, SockJS 라는 것들을 사용할 수 있다. 이는 JS 를 이용하여 브라우저의 종류에 상관없이 실시간 웹을 구현한다. (WebSocket, FlashSocket, JSONP Polling ... 등을 하나의 API 로 추상화) 브라우저와 웹 서버의 종류, 버전을 확인하여 가장 적합한 기술을 적용해주는 것이다. 

 

 

또, WebSocket 은 문자열들을 주고받을 뿐 그 이상의 일은 처리하지 않는다. 주고받은 문자열의 해독은 온전히 애플리케이션에 맡긴다. 형식이 정해져있어서 쉽게 해석할 수 있는 HTTP 와 달리, WebSocket 은 형식이 정해져있지 않기 때문에 애플리케이션에서 쉽게 해석할 수 없다. 때문에 WebSocket 은 Sub-protocol 을 사용해서 주고받는 메시지의 형태를 약속하는 경우가 많다. 그 중 하나가 STOMP 이다. Simple Text Oriented Messaging Protocol 의 약자로, 메시징 전송을 효율적으로 하기 위한 프로토콜로, pub/sub 기반으로 동작한다.

 

 

 

프로젝트를 진행하며 채팅 기능을 하나의 핵심 기능으로 넣을 예정이었다. Spring WebSocket 을 사용하여 실시가 채팅을 구현해보고자 하였다. 코드는 아래 블로그 링크를 대부분 참고하여 작성하였다.

 

링크: https://learnote-dev.com/java/Spring-%EA%B2%8C%EC%8B%9C%ED%8C%90-API-%EB%A7%8C%EB%93%A4%EA%B8%B0-webSocket%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85%EC%84%9C%EB%B2%84-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0/

 

 

SpringBoot 에서 WebSocket 을 사용하기 위해서는 WebSocketConfig 클래스를 만들고, 그 안에 클라이언트가 웹 소켓을 통해 보내는 데이터를 받아 처리할 Handler 와 소켓 연결 주소를 등록해줘야 한다.

 

 

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

    private final MsgHandler msgHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(msgHandler, "/ws/chat").setAllowedOriginPatterns("*")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }

    @Bean
    public WebSocketContainerFactoryBean createWebSocketContainer() {
        WebSocketContainerFactoryBean container = new WebSocketContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }

}

 

 

먼저 @EnableWebSocket 애노테이션을 @Configuration 에 붙여줌으로써 WebSocket 요청 처리를 구성한다. 위 Configuration 클래스는 WebSocketConfigurer 인터페이스를 구현한다. 이는 void registerWebSocketHandlers(WebSocketHandlerRegistry registry) 메소드 한 개만 포함하고있는 인터페이스로, @EnableWebsocket 이 달린 클래스에서 WebSocket request 를 핸들링할 콜백을 설정하기 위해 사용된다. WebSocketHandlerRegistry 인터페이스는 WebSocketHandlerRegistration addHandler(WebSocketHandler webSocketHandler, String... paths) 메소드를 갖는다. 여기에 위와같이 Handler, URL 을 등록해주면 되는 것이다.

 

 

Handler 은 아래와 같이 작성하였다.

 

 

@Component
@RequiredArgsConstructor
public class MsgHandler extends TextWebSocketHandler {

    private final ObjectMapper objectMapper;
    private final WebSocketService service;

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws JsonProcessingException, IOException {
        String payload = message.getPayload();
        Message msg = objectMapper.readValue(payload, Message.class);
        Room room = service.findRoomById(msg.getRoomId());
        room.handlerActions(session, msg, service);
    }

}

 

 

Spring 은 Text, Binary 타입의 핸들러를 지원한다. (채팅서비스이기에 TextWebSocketHandler 사용) handleTextMessages 내에서는, 메시지를 JSON 형식으로 받아 ObjectMapper 을 사용하여 직접 작성한 Message 타입으로 변환해준다. 그 다음 해당 메시지의 목적지 (채팅방) 을 메모리에서 검색하여 목적지로 메시지를 보내준다. 만약 메시지 요청이 방 참여라면, 새로운 세션을 메모리에 추가한다.

 

 

Message, Room 클래스는 아래와 같이 작성됐다.

 

 

@Getter
@Setter
@ToString
public class Message {
    private String roomId;
    private String from;
    private MessageType messageType;
    private String content;
    public enum MessageType {
        ENTER, TEXT
    }
}

 

 

@Getter
public class Room {

    private String roomId;
    private String name;
    private Set<WebSocketSession> sessions = new HashSet<>();

    @Builder
    public Room(String roomId, String name) {
        this.roomId = roomId;
        this.name = name;
    }

    public void handlerActions(WebSocketSession session, Message msg, WebSocketService service) {
        if (msg.getMessageType().equals(Message.MessageType.ENTER)) {
            sessions.add(session);
        }
        sendMessage(msg, service);
    }

    public <T> void sendMessage(T msg, WebSocketService service) {
        sessions.parallelStream().forEach(session -> service.sendMessage(session, msg));
    }

}

 

 

Room 의 handlerActions, sendMessage 메소드는 메시지에 대한 처리를 진행한다. 활용되는 WebSocketService 코드는 아래와 같다.

 

 

@Service
@RequiredArgsConstructor
public class WebSocketService {

    private final ObjectMapper objectMapper;
    private Map<String, Room> chatRooms;

    @PostConstruct
    private void init() {
        chatRooms = new LinkedHashMap<>();
    }

    public List<Room> findAllRoom() { return new ArrayList<>(chatRooms.values()); }

    public Room findRoomById(String roomId) { return chatRooms.get(roomId); }

    public Room creatRoom(String name) {
        String randomId = UUID.randomUUID().toString();
        Room room = Room.builder()
                .roomId(randomId)
                .name(name)
                .build();
        chatRooms.put(randomId, room);
        return room;
    }

    public <T> void sendMessage(WebSocketSession session, T msg) {
        try {
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(msg)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

 

 

 

Postman 에서 연결을 테스트해볼 수 있다.

 

 

Result

 

 

STOMP, SockJS 를 사용할 수 있는 코드를 활용하고 싶었으나 React Native 내에서 이 둘을 사용했을 때 자꾸 Connection 이 끊기는 문제가 발생했다... 그래서 우선은 가장 기본적인 WebSocket 활용방안으로 구현해두었다. 나중에 꼭 다시 업그레이드 시켜야 할 기능이다!!! (메시지 저장 등의 기능도 추가해야함)

 

 

 

 

 

 

 

 

 

Reference:

https://www.youtube.com/watch?v=MPQHvwPxDUw 

https://brunch.co.kr/@springboot/695

'Backend > Spring' 카테고리의 다른 글

Spring WebSocket (STOMP)  (0) 2023.06.30
Spring Security : JWT 적용해보기 (3)  (0) 2023.06.29
Spring Security : JWT 적용해보기 (2)  (0) 2023.06.11
Spring Security : JWT 적용해보기 (1)  (0) 2023.06.10
Spring Boot Project (1)  (0) 2023.06.06