Spring 채팅 구현하기 - 1. WebSocket

Kevin·2023년 8월 4일
1

Spring

목록 보기
6/11
post-thumbnail

HTTP와 Web Socket

일반적으로 클라이언트와 서버가 통신하는 방식은 HTTP 방식이다.

만약 HTTP 방식을 통해서 실시간 통신을 구현(흉내) 내기 위해서는 어떻게 할 수 있을까?


바로 생각이 나는 것은 HTTP 요청을 실시간 처럼 짧은 시간 동안 지속적으로 주고 받는 것이다.

즉 클라이언트에서 주기적으로 백엔드 서버에 요청을 보내는 것이다.

위의 방식을 풀링이라고 칭한다.

이것보다 조금 더 경제적으로 하려면, 백엔드에서 변경 사항이 생길 때마다 프론트엔드에게 응답을 보내주는 것이다.

이런 방법은 Server-sent Event라고 칭하며, SSE 역시 HTTP 프로토콜로 동작한다.


기억하자. HTTP는 단방향 통신이고, 실시간에 필요한건 양방향 통신이다.

그렇기에 위 기술들은 실시간 채팅에 불필요 하다.


그렇다면 양방향 통신을 위한 대안은 어떤 기술이 있을까?
오늘 중점적으로 알아볼 것은 웹소켓을 통한 양방향 통신이다.

웹 소켓은 클라이언트들이 중앙 서버를 거쳐(통해) 실시간 양방향 통신을 하도록 도와준다.

웹 소켓은 일반적으로 TCP 연결을 통해서, 양방향 통신 채널을 제공하는 기술이다.

정확히는 서버와 클라이언트 사이에서 소켓 커넥션을 유지하면서, 양방향 통신을 가능케 하는 기술이다.

웹소켓에서는 HTTP로 HandShake를 통해서 초기 통신을 시작한 후, 웹 소켓 프로토콜로 변환하여서 데이터를 전송한다.

먼저 클라이언트에서 HandShake를 요청하면, 서버에서는 성공 응답으로 101을 반환한다.

웹 소켓을 위해서 별도의 포트를 열 필요는 없다. 웹 소켓은 HTTP가 사용하던 80 포트 및 HTTPS의 포트인 443 위에서 동작하도록 설계가 되어있다.

또한 호환성을 위해 HandShake는 HTTP upgrade 헤더를 사용하며, HTTP 프로토콜에서 웹 소켓 프로토콜로 변경된다.

웹 소켓은 일반적인 HTTP 통신과 다르게, 양방향 데이터를 실시간으로 전송할 수 있다. 그러기 위해서는 클라이언트는 서버에 웹 소켓 연결을 한 상태로 유지하며, 언제든지 서버로부터 데이터를 받을 준비를 하고 있다.


스프링에서의 웹 소켓 방식

  1. 먼저 WebSocketConfig를 통해서 Socket에 대한 설정들을 한다.
@Configuration
@EnableWebSocket // 웹 소켓을 사용하도록 정의
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
                .addHandler(signalingSocketHandler(), "/room") // 직접 구현한 웹소켓 핸들러 (signalingSocketHandler)를 웹소켓이 연결될 때, Handshake할 주소 (/room)
                .setAllowedOriginPatterns("*"); // 클라이언트에서 웹 소켓 서버에 요청하는 모든 요청을 수락, CORS 방지
                // 웹소켓을 지원하지 않는 브라우저 환경에서도 비슷한 경험을 할 수 있는 기능을 제공
                // todo: 실제 서비스에서는 "*"으로 하면 안된다. 스프링에서 웹소켓을 사용할 때, same-origin만 허용하는 것이 기본정책이다.
    }

    @Bean
    public ChatSocketHandler signalingSocketHandler() { // WebSocketHandler을 웹 소켓 핸들러로 정의
        return new ChatSocketHandler();
    }
}
  1. 1번에서 등록했던 커스텀 웹 소켓 핸들러를 override하여서 구현한다.

@Slf4j
public class WebSocketHandler extends TextWebSocketHandler {

    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

    @Override // 웹 소켓 연결시
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
    }

    @Override // 데이터 통신시
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
    }

    @Override // 웹소켓 통신 에러시
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        super.handleTransportError(session, exception);
    }

    @Override // 웹 소켓 연결 종료시
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
    }
}

기본적인 코드는 위의 코드가 전부이다.
그러면 본격적으로 예시를 하나씩 들면서 채팅을 구현해보도록 하자.


만약 유저가 채팅방에 처음 들어오게 되었다면, 채팅방에 있던 기존 유저들에게 새로운 유저가 접근했음을 알려주는 로직을 구현하고자 할 때 위 메서드 중 어떤 메서드를 통해 해당 기능을 구현해야하는가? 바로 afterConnectionEstablished 메서드를 통해 구현하면 될 것이다. 한번 구현해보자.

@Override // 웹 소켓 연결시
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);

        Message message = Message.builder().sender(session.getId()).receiver("all").build();
        message.newConnect();

        for (WebSocketSession s : sessions) {
            if(!(s.getId().equals(session.getId()))) {
                s.sendMessage(new TextMessage("Hi " + new JSONObject(message) + "!"));
            }
        }
    }

위 방식대로 구현하면, 방에 새로 들어온 유저를 제외하고, 기존에 채팅방에 있던 유저들에게 새로운 유저가 접근했음을 알려줄 수 있다.

다른 예시로 만약 유저가 채팅을 쳤을 때 채팅방에 있는 본인을 포함한 모든 유저들에게 채팅을 보낼 때 어떤 메서드를 활용해야 하겠는가? handleTextMessage 메서드를 활용하면 된다.

@Override // 메시지 전달
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        JSONObject jsonObject = new JSONObject(payload);
        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage(jsonObject.getString("data")));
        }
    }

그러면 마지막 예시로 만약 유저가 채팅방을 나갔을 때(브라우져를 종료) 채팅방에 남아있는 유저들에게 말해주기 위해서 어떻게 해야겠는가? afterConnectionClosed 메서드를 활용하면 된다는 것을 모두 알게 될 것이다.
@Override // 웹 소켓 연결 종료시
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        var sessionId = session.getId();

        sessions.remove(sessionId);

        final Message message = new Message();
        message.closeConnect();
        message.setSender(sessionId);

        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage("BYE " + new JSONObject(message) + "!"));
        }
    }

시연 영상


참고 레퍼런스

// 주요 개념들 및 세션을 통한 채팅 구현

Spring Websocket & STOMP

// JWT를 이용한 채팅 구현

Spring WebSocket STOMP 채팅 서버 구현하기 (with. JWT, Exception Handling)

WebSocket & Spring

profile
Hello, World! \n

0개의 댓글