elevne's Study Note
Spring WebSocket 본문
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 란 현재 클라이언트, 서버, 전송 프로토콜 연결에서 다른 프로토콜 연결로 전환하기 위한 규칙이다.
응답이 101 switching protocol 로 오면 요청이 성공한 것이다.
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 을 사용하여 실시가 채팅을 구현해보고자 하였다. 코드는 아래 블로그 링크를 대부분 참고하여 작성하였다.
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 에서 연결을 테스트해볼 수 있다.
STOMP, SockJS 를 사용할 수 있는 코드를 활용하고 싶었으나 React Native 내에서 이 둘을 사용했을 때 자꾸 Connection 이 끊기는 문제가 발생했다... 그래서 우선은 가장 기본적인 WebSocket 활용방안으로 구현해두었다. 나중에 꼭 다시 업그레이드 시켜야 할 기능이다!!! (메시지 저장 등의 기능도 추가해야함)
Reference:
'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 |