일반적으로 클라이언트와 서버가 통신하는 방식은 HTTP 방식이다.
만약 HTTP 방식을 통해서 실시간 통신을 구현(흉내) 내기 위해서는 어떻게 할 수 있을까?
즉 클라이언트에서 주기적으로 백엔드 서버에 요청을 보내는 것이다.
위의 방식을 풀링이라고 칭한다.
이것보다 조금 더 경제적으로 하려면, 백엔드에서 변경 사항이 생길 때마다 프론트엔드에게 응답을 보내주는 것이다.
이런 방법은 Server-sent Event라고 칭하며, SSE 역시 HTTP 프로토콜로 동작한다.
그렇기에 위 기술들은 실시간 채팅에 불필요 하다.
웹 소켓은 클라이언트들이 중앙 서버를 거쳐(통해) 실시간 양방향 통신을 하도록 도와준다.
웹 소켓은 일반적으로 TCP 연결을 통해서, 양방향 통신 채널을 제공하는 기술이다.
정확히는 서버와 클라이언트 사이에서 소켓 커넥션을 유지하면서, 양방향 통신을 가능케 하는 기술이다.
웹소켓에서는 HTTP로 HandShake를 통해서 초기 통신을 시작한 후, 웹 소켓 프로토콜로 변환하여서 데이터를 전송한다.
먼저 클라이언트에서 HandShake를 요청하면, 서버에서는 성공 응답으로 101을 반환한다.
웹 소켓을 위해서 별도의 포트를 열 필요는 없다. 웹 소켓은 HTTP가 사용하던 80 포트 및 HTTPS의 포트인 443 위에서 동작하도록 설계가 되어있다.
또한 호환성을 위해 HandShake는 HTTP upgrade 헤더를 사용하며, HTTP 프로토콜에서 웹 소켓 프로토콜로 변경된다.
웹 소켓은 일반적인 HTTP 통신과 다르게, 양방향 데이터를 실시간으로 전송할 수 있다. 그러기 위해서는 클라이언트는 서버에 웹 소켓 연결을 한 상태로 유지하며, 언제든지 서버로부터 데이터를 받을 준비를 하고 있다.
@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();
}
}
@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")));
}
}
@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) + "!"));
}
}
// 주요 개념들 및 세션을 통한 채팅 구현
// JWT를 이용한 채팅 구현
Spring WebSocket STOMP 채팅 서버 구현하기 (with. JWT, Exception Handling)