개요
일반적인 채팅 애플리케이션은 상대방이 나에게 메시지를 전송하면, 내 화면에 즉시 메시지가 표시됩니다. HTTP 통신은 클라이언트와 서버 간 요청과 응답이 완료되면 연결을 끊어버리기 때문에, 상대방이 메시지를 전송했는지 안 했는지 알기 위해서는 새로고침을 해야만 합니다.
하지만 나에게 신규 메시지가 전송될 때마다 새로고침 하는 것은 우리가 원하는 방법이 아닐 겁니다. 이 문제를 해결하기 위해 사용할 수 있는 프로토콜은 바로 Web Socket인데요, 왜 그런지 살펴보도록 하겠습니다.
Web Socket 특징 2가지
HTTP 통신과 비교했을 때 WebSocket 통신에서 두드러지는 특징이 2가지가 있습니다. 첫째, 클라이언트와 서버가 TCP 3-way 핸드셰이크로 연결을 체결한 후, 해당 연결을 끊지 않습니다. 둘째, 클라이언트의 요청 없이도 서버에서 클라이언트로 데이터 전송이 가능합니다. 즉, 양방향 통신(클라 -> 서버, 서버-> 클라)이 가능합니다.
웹소켓 통신으로 클라이언트는 서버와 지속적으로 연결을 유지함과 동시에 서버에서 클라이언트로 데이터 전송이 가능하게 됩니다. 만약 상대방이 채팅 서버에 메시지를 전송할 경우, 서버는 지속적으로 연결을 유지하고 있는 나에게 실시간으로 메시지를 전송할 수 있습니다.
이제 웹소켓 프로토콜을 채팅 애플리케이션 개발을 통해 직접 체험해 보도록 하겠습니다.
채팅 애플리케이션 개발
프로젝트 개발환경
- Java 17
- Springboot 3.2.5
- dependency
implementation 'org.springframework.boot:spring-boot-starter-websocket'
WebSocketConfig
WebSocketConfigurer 인터페이스 구현으로 Spring이 제공하는 웹소켓 환경설정을 할 수 있습니다. WebSocketConfigurer 인터페이스의 registerWebSocketHandlers 메서드를 오버라이드 해야 합니다.
@Configuration
@EnableWebSocket
public class SocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/ws") // 특정 url 경로에 웹소켓 핸들러를 등록한다.
.addHandler(myHandler2(), "/ws/v2") // 핸들러를 여러개 등록할 수 있다.
.setAllowedOrigins("*");
}
@Bean
public WebSocketHandler myHandler() { // 메시지를 처리할 핸들러
return new MyHandler();
}
@Bean
public WebSocketHanlder myHandler2() {
return new MyHandler2();
}
}
addHandler 메서드는 특정 url에 매핑되는 메시지 처리 핸들러를 등록합니다. 위 예제에서는 ws://localhost:8080/ws url 경로에 myHandler가 매핑됩니다. 추가적으로 다른 경로에도 핸들러를 등록할 경우 주석과 같이 작성할 수 있습니다.
WebSocketHandler
WebSocketHandler는 TextWebSocketHandler 또는 BinaryWebSocketHandler를 extends 하면 됩니다. 아래 예제 코드는 마샤와곰님의 포스팅을 참고했습니다.
public class MyHandler2 extends TextWebSocketHandler {
private static final ConcurrentHashMap<String, WebSocketSession> CLIENTS = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
CLIENTS.put(session.getId(), session);
}
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String id = session.getId();
TextMessage msg = new TextMessage("Handler 2: " + message.getPayload());
CLIENTS.entrySet().forEach( arg -> {
if(!arg.getKey().equals(id)) {
try {
arg.getValue().sendMessage(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
CLIENTS.remove(session.getId());
}
}
CLIENTS 컨커런트 해시맵은 현재 연결이 체결되어 있는 세션들을 관리합니다. afterConnectionEstablished 메서드에서 해시맵에 웹소켓 세션 id와 세션을 넣는 것을 확인할 수 있습니다.
handleTextMessage 메서드에는 message를 어떻게 처리할지 작성합니다. 해시맵에서 메시지를 보낸 당사자의 세션 id를 제외한 나머지 세션에 메시지를 send 합니다.
마지막으로 웹소켓 연결을 종료할 때 afterConnectionClosed 메서드는 해시맵에서 세션을 제거해 연결 정보를 제거합니다.
테스트
저는 웹소켓 연결 테스트로 Edge 확장 프로그램 Apic을 사용했습니다. 포스트맨에서도 ws 테스트를 진행하실 수 있습니다.
우선, 두번째 탭과 세 번째 탭에서 "ws://localhost:8080/ws/v2" url로 웹소켓 연결을 체결했습니다. CLIENTS 컨커런트 해시맵에 두 탭의 WebSocketSession이 등록되어 연결정보가 저장됩니다.
세 번째 탭에서 text를 서버로 send 하면, MyHandler2에 의해 두 번째 탭으로 text가 전송됩니다. 위 사진과 같이 잘 전달된 모습을 확인할 수 있습니다.
단일 WebSocket의 한계
현재 위 예제는 특정 url과 WebSocketHandler를 매핑하고, 각 핸들러에서 웹소켓 세션(브라우저 연결정보)을 관리합니다. 만약 채팅방이 100개가 된다면 "/chatroom/1"과 같은 url과 매핑될 WebSocketHandler를 100개 만들어 세션을 관리해야 할 것입니다. 또한 클라이언트와 서버 간 어떤 형식으로 데이터를 주고받을지 규칙을 정하지 않아 데이터 처리가 곤란합니다.
이때 메시지 브로커를 도입하면 간단히 해결할 수 있습니다. 스프링은 웹소켓 상에서 STOMP 메시징 프로토콜을 사용하는 in-memory 메시지 브로커를 지원합니다. 다음 포스팅에서는 스프링의 내장 메시지 브로커를 사용해 채널링을 구현해 보도록 하겠습니다.
Reference
'개발 > 프로젝트' 카테고리의 다른 글
Springboot로 Web Socket 상에서 STOMP 메시지 브로커 적용하기 (0) | 2024.05.11 |
---|---|
Web3j 라이브러리를 활용한 이더리움 통신 Spring boot 프로젝트 (1) | 2024.04.25 |
[프로젝트] 순수 자바로 웹 애플리케이션 서버(WAS) 구현하기 (0) | 2024.02.20 |