Skip to content

채팅 기능 구현 진행 문서

Taewan Kim edited this page Sep 5, 2021 · 2 revisions

웹 소켓

채팅 기능을 구현하기 위해서는 웹 소켓이 필요하다.

웹 소켓은 HTTP 환경에서 클라이언트와 서버 사이에 하나의 TCP 연결을 통해 실시간으로 전이중 통신을 가능하게 하는 프로토콜이다. 여기서 전이중 통신이란, 일방적인 송신 또는 수신만이 가능한 단방향 통신과 달리 가정에서의 전화와 같이 양방향으로 송신과 수신이 가능한 것을 말한다. 양방향 통신이 아닌 단방향 통신의 예로는 텔레비전 방송, 라디오를 들 수 있는데, 데이터를 수신만 할 수 있고, TV나 라디오를 통해 데이터를 보낼 수 없다.

SNS, 멀티 플레이어 게임, 구글 공유 문서 등 실시간 웹 어플리케이션 구현을 위해 웹 소켓을 사용하고 있다.

웹 소켓 연결 방식

웹 소켓은 전이중 통신이므로, 연속적인 데이터 전송의 신뢰성을 보장하기 위해 Handshake 과정을 진행한다.

기존의 다른 TCP 기반의 프로토콜은 TCP layer에서의 Handshake를 통해 연결을 수립하는 반면,

웹 소켓은 HTTP 요청 기반으로 Handshake 과정을 거쳐 연결을 수립한다.

웹 소켓은 연결을 수립하기 위해 Upgrade 헤더Connection 헤더를 포함하는 HTTP 요청을 보낸다.

https://user-images.githubusercontent.com/50273712/129447412-30e32809-b1fe-4e95-9d7a-85553f3ab92b.png

연결이 수립되었을 때는 통상적인 상태 코드 200 대신, 웹 소켓 서버는 아래와 같이 101을 응답한다.

https://user-images.githubusercontent.com/50273712/129448362-0ada9130-1181-4d3b-ac4b-33bd4130b00d.png

101 Switching Protocols: Handshake 요청 내용을 기반으로 다음부터 WebSocket으로 통신할 수 있다는 뜻이다.

Handshake 과정을 통해 연결이 수립되면 응용 프로그램 계층 프로토콜이 HTTP에서 웹 소켓으로 업그레이드가 된다. 업그레이드가 되면 HTTP는 사용되지 않고, 웹 소켓 연결이 닫힐 때까지 두 끝 점에서 웹 소켓 프로토콜을 사용하여 데이터를 주고 받게 된다. 웹 소켓 연결은 주로 새로고침이나 창 닫기 등의 이벤트 발생 시 닫히게 된다.



STOMP

스프링에서 웹 소켓을 사용할 때, 알아야 하는 또다른 프로토콜이 있다. STOMP 프로토콜이다.

STOMP는 Simple Text Oriented Message Protocol의 약자로 단순 텍스트 기반 메시징 프로토콜이다.

웹 소켓 프로토콜은 Text 또는 Binary 두 가지 유형의 메시지 타입은 정의하지만 메시지의 내용에 대해서는 정의하지 않는다. 따라서 STOMP라는 프로토콜을 서브 프로토콜로 사용한다. STOMP를 사용하게 되면 단순한 Binary, Text가 아닌 규격을 갖춘(format) message를 보낼 수 있다.

스프링은 웹 소켓 모듈을 통해서 STOMP를 제공하고 있다.

STOMP의 형식은 HTTP와 닮아 있다.

COMMAND

header1:value1
header2:value2

Body^@

COMMAND로 SEND 또는 SUBSCRIBE 명령을 사용하며, header와 value로 메시지의 수신 대상과 메시지에 대한 정보를 설명할 수 있다. 기존의 WebSocket만으로는 표현이 불가능한 형식이다. 이를 통해 STOMP 프로토콜은 Publisher-Subscriber를 지정하고, 중간에서 메시지 브로커 역할을 함으로써 특정 사용자에게만 메시지를 전송하는 기능 등을 가능하게 한다.

Publisher-Subscriber 패턴의 예시는 다음과 같다.

Client A는 다음과 같이 5번 채팅방에 대해 구독(SUBSCRIBE)을 걸어놓을 수 있다.

SUBSCRIBE
destination:/subscribe/chat/room/5

그리고 나서 Client B가 아래와 같이 채팅 메시지를 보낼 수 있다.

SEND
content-type:application/json
destination:/publish/chat
{"chatRoomId":5,"type":"MESSAGE","writer":"clientB"}

프로젝트 내부 코드

config - WebSocketConfig.class

@Configuration
@EnableWebSocketMessageBroker //메시지 브로커가 지원하는 'WebSocket 메시지 처리'를 활성화한다.
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// 메세지 브로커를 설정한다.
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
// 메모리 기반의 Simple Message Broker를 활성화하면서,
// 메세지 브로커가 "/subscribe"으로 시작하는 주소의 subscriber들에게 Response를 전달할 수 있도록 한다.
        registry.enableSimpleBroker("/subscribe");

        // 클라이언트가 서버로 메시지 보낼 때 붙여야 하는 prefix
        registry.setApplicationDestinationPrefixes("/publish");
    }

// websocket 연결용 STOMP EndPoint를 등록한다.
// STOMP 프로토콜 사용: 메세지를 보낼 때 특정 url로 보내면, 해당 url을 subscribe한 사용자들을 알맞게 찾아서 전송
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws-connection")
                .setAllowedOrigins("chrome-extension://ggnhohnkfcpcanfekomdkjffnfcjnjam")//APIC이라는 프로그램으로 소켓 테스트 
                .withSockJS();
    }
}

controller - ChattingController.class

@RestController
public class ChattingController {
    private final ChattingService chattingService;
    private final SimpMessagingTemplate simpMessagingTemplate;

    public ChattingController(ChattingService chattingService, SimpMessagingTemplate simpMessagingTemplate) {
        this.chattingService = chattingService;
        this.simpMessagingTemplate = simpMessagingTemplate;
    }

    // 클라이언트에서 /publish/messages로 보내면 여기서 처리됨. 채팅 방을 구독 중인 사람들한테 다 뿌려줌
    @MessageMapping("/messages")
    public void chat(@Valid ChatRequest chatRequest) {
        chattingService.save(chatRequest);
        simpMessagingTemplate.convertAndSend("/subscribe/rooms/" + chatRequest.getRoomId(), chatRequest.getMessage());
    }

    @GetMapping("/messages/{userId}")
    public List<LatestChatResponse> findAllLatestChats(@PathVariable Long userId) {
        return chattingService.findAllLatestChats(userId);
    }

    @GetMapping("/messages")
    public List<ChatResponse> findAllChats(@RequestParam("roomId") Long roomId) {
        return chattingService.findAllChats(roomId);
    }
}