본문 바로가기

BackEnd/Java Spring

[Spring] WebSocket

728x90

머리말

오늘은 Spring 프레임워크를 공부하면서 WebSocket을 사용할 기회가 생겨서 아래와 같이 WebSocket에 관련된 내용을 정리해보았습니다.

실시간 데이터 전송을 필요로 하는 채팅서버를 구현하기위하여 WebSocket을 도입하여 사용해보았습니다. WebSocket을 적용하면서 어떤것들이 있고, 어떻게 사용해야하는지에 대하여 정리해보겠습니다.

 

Http vs WebSocket

Http

  • HTTP는 비 연결성이고 매번 연결을 맺고 끊는 과정이 필요하다. ( 요청 - 응답 ) 구조
  • 기본적으로 무상태(Stateless)이므로 연결된 상태를 저장하지 않는다.
  • 실시간으로 바뀌는 정보에 대해서는 지속적으로 요청해야한다.

WebSocket

  • TCP Layer위에서 통신하는 계층이다. TCP 처럼 핸드쉐이크를 이용하여 연결을 맺는다.
  • 웹소켓은 처음에 핸드쉐이크 요청을 한다. 이때 HTTP 요청이 upgrade 된다.
  • 즉, 최초 접속시에는 HTTP 프로토콜을 이용해 핸드셰이킹 한다.
  • 업그레이드 후에는 http://~ 가 아닌 ws://~로 요청을 보내야한다. 이후 웹소켓으로 연결되고 데이터를 주고받는다.
  • 웹소켓 커넥션을 종료시키기 위해서도 Closing 핸드쉐이크가 필요하다. 이후 웹소켓 연결이 종료된다.
  • 따라서 한번 연결을 한후, 클라이언트 간 지속적으로 연결을 유지할 수 있다.

Websocket을 사용하는 이유?

  • 데이터를 자주 전송해야할때 사용한다. HTTP는 전송할때마다 연결이 필요함으로 연결시 불필요한 행동을 하지 않는다.
  • 실시간 데이터 전송시 사용 : Http는 기본적으로 연결시에 Header 정보를 전송하게되는데 웹 소켓은 핸드쉐이크 이후에는 Header정보를 전송하지 않는다. 즉 데이터량을 줄일 수 있다.

Spring에서 WebSocket을 구현해보자

Gradle 추가

implementation 'org.springframework.boot:spring-boot-starter-websocket'

WebSocketConfig

  • 웹소켓 통신을 하기위한 엔드포인트를 설정한다.
  • WebSocketHandler를 선언한다. 이 핸들러가 웹소켓 통신을 처리해준다.
  • addHandler()안에 첫번째 인자로 들어가는데, 여기에 new WebSocketChatHandler()와 같이 사용자 정의 핸들러를 직접 넣을수도 있으나 인터페이스를 주입하였습니다.
  • WebsocketHandlerRegistry에 웹소켓 앤드포인트를 /ws/chat로 설정
  • setAllowedOrigins(”*”)는 모든 cors 요청을 허용한다는 것이다.
package com.chat.chattingserver.common.config;

import com.chat.chattingserver.controller.WebSocketChatHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
    private final WebSocketHandler webSocketHandler;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
    }
}

WebSocketChatHandler

afterConnectionEstablished : 소켓 연결 직후 실행되는 메서드

  • 메서드에 인증기능을 구현하여 url Query로 토큰을 확인하고 유효하지 않는 토큰이라면 연결을 끊는다.
  • ChattingRoomManager에게 해당 참가자를 어디에 배치할 것인지에 대한 것을 위임한다.

handleTextMessage : 클라이언트에서 서버에게 메세지를 전송할때 실행되는 메서드

  • 클라이언트로부터 메세지 타입을 구분하고 그 타입에 따라서 어떤 동작을 할지 각각의 서비스에게 위임 하려고합니다. 현재는 chattingRoomManager에게 채팅이 왔다는것을 위임 합니다.

afterConnectionClosed : 연결이 끊나면 실행되는 메서드

  • 연결이 끊나면 해당 메서드가 실행되고, chattingRoomManager에게 클라이언트가 접속종료 했다는것을 알립니다.
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketChatHandler extends TextWebSocketHandler {
    private final ObjectMapper mapper;

    @Autowired
    private final ChattingRoomManager chattingRoomManager;
    private final AuthService authService;

    /**
     * 소켓 연결시
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("{} Established", session.getId());

        URI uri = session.getUri();

        if (uri != null) {
            String query = uri.getQuery();
            Map<String, String> queryParams = QueryParserUtil.parseQueryParams(query);

            String token = queryParams.get("token");
            chattingRoomManager.onConnect(session, authService.validate(token));
            log.info("{} Connected", session.getId());
        }
        else {
            session.close(CloseStatus.NOT_ACCEPTABLE);
        }
    }

    /**
     * 소켓 연결후 메세지 전송시, 추후 이미지, 동영상 전송등을 위한 분리구현
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        ChatMessageDto<?> chatMessageDto = mapper.readValue(payload, new TypeReference<ChatMessageDto<?>>() {});

        log.info("chatType : {}", chatMessageDto.getType());

        switch (chatMessageDto.getType()) {
            case ONMESSAGE:
                ChatOnMessageDto onMessage = mapper.readValue(mapper.writeValueAsString(chatMessageDto.getPayload()), ChatOnMessageDto.class);
                log.info("onMessage ChatType : {}, Message: {}", onMessage.getType(), onMessage.getMessage());
                chattingRoomManager.onMessage(session, onMessage.getMessage(), onMessage.getRoomId());
                break;
            default:
            {
                session.sendMessage(new TextMessage("Unkown Message Received"));
                break;
            }
        }
    }

    /**
     * 연결 종료시에
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        chattingRoomManager.onDisconnect(session);
        log.info("{} Disconnected", session.getId());
    }
}
728x90

'BackEnd > Java Spring' 카테고리의 다른 글

@Bean vs @Compoent  (0) 2024.03.10
[Java Spring] @Bean vs @Compoent  (0) 2024.02.26