-
Notifications
You must be signed in to change notification settings - Fork 8
채팅 기능 구현 진행 문서
채팅 기능을 구현하기 위해서는 웹 소켓이 필요하다.
웹 소켓
은 HTTP 환경에서 클라이언트와 서버 사이에 하나의 TCP 연결을 통해 실시간으로 전이중 통신
을 가능하게 하는 프로토콜
이다. 여기서 전이중 통신
이란, 일방적인 송신 또는 수신만이 가능한 단방향 통신과 달리 가정에서의 전화와 같이 양방향으로 송신과 수신이 가능한 것
을 말한다. 양방향 통신이 아닌 단방향 통신의 예로는 텔레비전 방송, 라디오를 들 수 있는데, 데이터를 수신만 할 수 있고, TV나 라디오를 통해 데이터를 보낼 수 없다.
SNS, 멀티 플레이어 게임, 구글 공유 문서 등 실시간 웹 어플리케이션 구현을 위해 웹 소켓을 사용하고 있다.
웹 소켓은 전이중 통신이므로, 연속적인 데이터 전송의 신뢰성을 보장하기 위해 Handshake 과정을 진행한다.
기존의 다른 TCP 기반의 프로토콜은 TCP layer에서의 Handshake를 통해 연결을 수립하는 반면,
웹 소켓은 HTTP 요청 기반으로 Handshake 과정을 거쳐 연결을 수립
한다.
웹 소켓은 연결을 수립하기 위해 Upgrade 헤더
와 Connection 헤더
를 포함하는 HTTP 요청을 보낸다.
연결이 수립되었을 때는 통상적인 상태 코드 200 대신, 웹 소켓 서버는 아래와 같이 101을 응답한다.
101 Switching Protocols
: Handshake 요청 내용을 기반으로 다음부터 WebSocket으로 통신할 수 있다는 뜻이다.
Handshake 과정을 통해 연결이 수립되면 응용 프로그램 계층 프로토콜이 HTTP에서 웹 소켓으로 업그레이드가 된다. 업그레이드가 되면 HTTP는 사용되지 않고, 웹 소켓 연결이 닫힐 때까지 두 끝 점에서 웹 소켓 프로토콜을 사용하여 데이터를 주고 받게 된다. 웹 소켓 연결은 주로 새로고침이나 창 닫기 등의 이벤트 발생 시 닫히게 된다.
스프링에서 웹 소켓을 사용할 때, 알아야 하는 또다른 프로토콜이 있다. 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"}
@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();
}
}
@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);
}
}