이번에 채팅을 구현하면서 겪었던 경험과 생각해보았던 방법을 정리 및 개념 이해를 위해 작성하였습니다.
채팅은 뭐가 기본이 되어야 해? 라고 물어보면 대부분 실시간으로 메시지를 주고 받을 수 있어야지!
라고 할 것 같아요.
저도 당연히 그렇게 생각했습니다. 그래서 이러한 방법이 뭐가 있을까? 하고 찾아보니 websocket을 이용하면 된다고 하더라구요.
어차피 같은 통신인데 http와 다른게 뭐야? 라고 생각하고 찾아보니 http는 유저가 요청을 해야 서버가 응답해주는 것이고 websokcet은 서버로 데이터가 들어오면 그냥 응답해주는 거죠
그러니깐 내가 "야!!" 라고 해야 "왜!!!"라고 하는 친구는 http, 내가 먼저 젤 호출해야되죠?
그런데 그냥 "왜!!!!" 라고 먼저 물어봐주는 친구가 websocket이라고 생각하면 이해가 쉽더라구요!
그래서 채팅을 위해서는
라는 이유로 websocket을 선택하였습니다.
그러면 websocket은 어떤식으로 접속으을 하는가? 라고 보면
이런식으로 구현됩니다
통신할때 TCP/IP를 사용하면 뭐가 좋은가?
TCP/IP 는 신뢰성 있는 연결을 위해 사용됩니다. 그리고 웹소켓은 3handshake를 통해 신뢰성 있는 통신을 할 수가 있는거죠
그러면 어차피 3handshake를 사용하는데 두 프로토콜의 차이가 뭐냐? 라고 생각하실 수도 있는데 저도 궁금해서 찾아봤습니다.
답변을 보면 웹소켓은 어플리케이션 프로토콜에 메시지 지향적이고, TCP를 전송 계층으로서 사용한다는 것입니다.
즉 HTTP handshake를 통해 서버와 클라이언트가 연결이된다면 그제서야 websocket을 통해 메시지를 주고 받는 다는 것이죠
그리고 이때는 http1.1 프로토콜을 이용한다고 합니다.
왜 HTTP 1.1이냐?
HTTP 1.1 은 기본적으로 하나의 요청당 하나의 응답을 처리 하기 때문에 순차적으로 처리합니다.
3 handshake 는 syn -> syn/ack -> ack의 과정을 통해 하나씩 인증과정을 거처야 하기 때문이죠
OAuth 에서 과정에서 유저정보 값을 요청하기 위한 프로토콜 방식에서도 HTTP1.1로 요청하죠
Stomp는 Simple Text Oriented Messaging Protocol이라는 이름 처럼 메시지 전송에 특화되어있는 프로토콜이라는 것을 알 수 있습니다.
장점은
1. 메세지의 헤더에 값을 줄 수 있다.
2. pub/sub 구분하여 메시지 공급자, 소비자로 나누어 구현하기에 용이하다가 있습니다.
이러한 장점으로 저는 websocket위에서 동작하는 stomp를 통해 구현할 예정입니다.
그런데, 이러한 통신 프로토콜은 우리가 잘 알고있는 rabbitmq나 kafka처럼 하나의 토픽을 기준으로 그 토픽에서 메시지를 갖고 오는 식으로 구현할 수 있을 것 같지 않나요?
그래서 그 방법들을 시도해보면서 겪었던 경험에 대해 작성해볼까 합니다.
가장 먼저 Stomp구현을 위한 Config 파일을 작성해야 합니다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
//
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/pub"); // 메시지 보낼 Url : /app/message
registry.enableSimpleBroker("/sub"); //sub용 sub topic/public
}
//채팅 클라이언트가 서버와 연결하웹소켓 셋팅 부분 ->웹소켓 연결 주소 -> /url/chatting
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat/chatting").setAllowedOriginPatterns("*").withSockJS();
}
}
전 프론트뷰로 리엑트를 이용했는데요 프론트에서는
let socketJs = new SockJS("http://localhost:8080/chat/chatting");
let stompcli = Stomp.over(socketJs);
이런식으로 socket을 연결해주어야 합니다.
스프링의 경우
implementation 'org.springframework.boot:spring-boot-starter-websocket'
참고로 리엑트의 경우
import SockJS from 'sockjs-client'
import Stomp from 'stompjs';
각각 dependency를 추가해주었습니다.
위 코드를 보시면 쉽게 이해 되시겠지만
chat/chatting
엔드포인트를 통해 프론트와 백엔드가 서로 통신 환경을 만들죠
Controller 부분입니다.
@RestController
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
private final SimpMessagingTemplate template;
// //채팅 컨트롤러 (stomp)
@MessageMapping(value = "/chat/chatting") //클라이언트에서 수신되는 곳
public void chatController(MessageDto messageDto) {
//로그를 저장
chatService.saveLog(messageDto);
template.convertAndSend("/sub/chatting/room/" + messageDto.getRoomId(), messageDto); // 클이언트로 전송
}
간단하죠? 끝입니다
리엑트는 다음과 같습니다
stompcli.send(`/pub/chat/chatting`, {}, JSON.stringify(msg)) //-> 보낼때
stompcli.connect({},() => {
stompcli.subscribe(`/sub/chatting/room/11`, (data) => // -> 받을때
전 이부분이 이해가 제일 안되었습니다 왜 저런식으로 연동이 되는지요
이유는 controller의 @Messagemapping으로 값이 들어오게되면 자동으로 config에 설정해놓은 값
/pub
이 붙어서
/pub/chat/chatting
식으로 만들어진다고 합니다. 그래서 리엑트에서는 저 주소로 보내줘야 합니다.
반대로 메세지를 보낼대는
config에서 설정해 놓은
/sub
이 붙어서
/sub/chatting/room + roomId
이런식으로 나가기 때문에 리엑트는 저 주소로 받아야 합니다
그리고 convertAndSend 메소드로 통해 들어온 메시지(pub) 을 내보내줍니다 (sub).
추가로 socketJS를 같이 사용하였습니다. 이유는 SocketJS는 websocket이 지원하지 않는 브라우저에서도 사용 할 수 있도록 해줍니다
또한 서로다른 Origin을 사용하는 경우 (스프링은 8080 리엑트는 3000)
CORS 에러가 생길 수 있습니다.
그래서 이러한 에러 예외처리를 하고자
.setAllowedOriginPatterns("*")
로 지정해 주었습니다. 보안을 위해서는 당연히 이렇게 하는 것보단 특정 domain 주소만 열어 놓는게 좋겠죠
kafka와 rabbitmq를 사용하여 구현하고자 했던 방법은 다음 글에 적어보겠습니다