SpringBoot

[Spring] 웹소켓(WebSocket)이란 + Spring에서 동작 과정

영리제리 2025. 4. 30. 13:49

 

채팅이나 실시간 통신을 할 때, 웹소켓을 많이 사용합니다. 

웹소켓은 티키타카티키타카초가 필요한 HTTP와는 뭔가 다를 것 같죠?

소켓핑

 

 

 

웹소켓(WebSocket)이란,

서버와 클라이언트의 양방향 통신 프로토콜

  • 웹소켓은 TCP 소켓을 기반으로 작동 -> TCP를 사용하기 때문에 데이터의 순서와 신뢰성이 보장됨
  • HTTP는 클라이언트의 요청이 있어야 서버에서 응답을 보낼 수 있는 단방향 프로토콜이지만, 웹소켓은 클라이언트와 서버 모두 데이터를 송수신할 수 있는 양방향 프로토콜
  • HTTP와 동일한 어플리케이션 계층에서 동작
  • 웹소켓은 헤더의 크기가 작고 오버헤드가 적기 떄문에 HTTP보다 효율적인 통신이 가능
  • 웹소켓은 최초 연결 시, HTTP 접속 후(HandShake) 양방향 메세지를 주고받을 수 있음

 

웹소켓 접속 과정

1. TCP/IP 접속
2. WebSocket 최초 연결 HandShake
3. WebSocket 양방향 통신

 

웹소켓 최초 연결 HandShake

1) 클라이언트가 웹소켓 열기 HTTP Request를 전송

 

Http Request Header

GET ws://localhost:8080/test/chat-ws HTTP/1.1
Host: localhost:8080
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://localhost:8080
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: JSESSIONID=592EBF5FCC5877A304C2AF9
Sec-WebSocket-Key:
  • Upgrade : websocket으로 웹소켓 프로토콜로 프로토콜 변경 요청  

 

 

2) 서버가 웹소켓 열기 수락 HTTP Response

HTTP/1.1 101
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: j+tLbLO4QvX38F6+SPoIbZU3
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Date:
  • 101 Switching Protocols:  웹소켓으로 프로토콜 변경 완료

 

 

 

어머 그러면 Spring에서는 어떻게 웹소켓 연결하고 통신하는지 너무 궁금하죠~?

할수있다 아자아자핑

 

 

Spring WebSocket API 동작 원리

1. WebSocket 최초 연결 HandShake

1. 클라이언트가 WebSocket 연결 URL 요청
2. WebSocketHandlerMapping이 URL을 매핑해서 WebSocketHttpRequestHandler로 넘김
3. WebSocketHttpRequestHandler는 HandshakeHandler를 통해 HTTP를 WebSocket으로 업그레이드
4. 업그레이드 성공 후, WebSocketSession을 생성 (이후 session을 통해 sendMessage / handleMessage로 실시간 통신)
5. "작성한 웹소켓 Handler"에서 afterConnectionEstablished(session) 호출
6. 클라이언트에게 WebSocket 프로토콜 변경 완료 Response 응답 전달

 

 

  • Spring에서 제공하는 WebSocket API와 SockJS는 Spring MVC 기술에 종속되지 않는다.
    : WebSocket 서버는 WebSocketHandler 인터페이스 구현체를 통해서 각 경로에 대한 핸들러를 구현할 수 있고 이를 통해 웹소켓 통신이 가능하다.
  • 단, 웹소켓 설정 및 초기 핸들링(업그레이드 요청 수락, 핸들러 매핑)은 Spring MVC DispatcherServlet이 관리하는 ApplicationContext를 통해 처리되어야 한다.

 

2. Websocket 연결 후 양방향 통신 과정

[클라이언트가 메시지 보냄]
→ Tomcat이 수신
→ Spring의 WebSocketHandlerAdapter(TomcatWebSocketHandlerAdapter) 호출
→ WebSocketHandler.handleMessage(session, message) 호출
→ 비즈니스 로직 처리

[서버가 메시지 보냄]
→ WebSocketSession.sendMessage 호출
→ Tomcat RemoteEndpoint를 통해 프레임 전송
→ 클라이언트 수신

 

WebSocket Container 
: WebSocket 프로토콜을 처리하기 위해 서버 측에서 동작하는 핵심 컴포넌트

  • 서버에서 WebSocket 연결을 수립하고 유지하며, 클라이언트와의 WebSocket 메시지를 송수신하는 JSR-356 (Java WebSocket API)에 따라 구현된 런타임 환경
  • Tomcat의 경우는 org.apache.tomcat.websocket.server.WsServerContainer
  • WebSocketSession 객체로 연결 상태와 메타 정보 관리
  • 스프링은 WebSocketHandler만 제공하고, 실제 통신은 톰캣의 WebSocketContainer가 처리

 

 

오우오우 저도 아직까지 완벽핑은 아니지만 

그냥 흐름 정도만 보기로해요^O^

 

 

 

WebSocket 구현

1) 웹소켓 라이브러리 의존성 추가

Spring : pom.xml

	...
		
	<!-- WebSocket -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-websocket</artifactId>
		<version>${spring.maven.artifact.version}</version>
	</dependency>
		
	...

 

or

Spring Boot : build.gradle

implementation 'org.springframework.boot:spring-boot-starter-websocket'

 

 

2) 웹소켓 config 설정

WsConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import egovframework.example.test.chat.handler.ChatHandler;


@Configuration
@EnableWebSocket
public class WsConfig implements WebSocketConfigurer {
	

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(chatHandler(), "/chat-ws")
			.setAllowedOrigins("*")
			.withSockJS();
		
	}

	@Bean
	public ChatHandler chatHandler() {
		return new ChatHandler();
	}
	

}

 

 

⇒ Handler를 통해 웹소켓 활성화

  • @EnableWebSocket으로 웹소켓 활성화
  • @Configuration으로 웹소켓 설정
  • 웹소켓에 접속하기 위한 EndPoint (URL)설정 ⇒ /chat-ws
  • CORS 설정 : .setAllowedOrigins("*");
  • SockJS 설정 : 브라우저에 따라 Websocket을 지원하지 않을 수 있으므로, 웹소켓을 지원하지 않는 환경에서도 웹소켓과 비슷한 환경을 지원하기 위해 SockJS 라이브러리 설정

 

3) 웹소켓 Handler 생성

ChatHandler.java

import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.websocket.server.ServerEndpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class ChatHandler extends TextWebSocketHandler {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(EchoHandler.class);

	private Map<String, WebSocketSession> users = new ConcurrentHashMap<>();

	@Override
	public void afterConnectionEstablished(
			WebSocketSession session) throws Exception {
		log(session.getId() + " 연결 됨");
		users.put(session.getId(), session);
	}

	@Override
	public void afterConnectionClosed(
			WebSocketSession session, CloseStatus status) throws Exception {
		log(session.getId() + " 연결 종료됨");
		users.remove(session.getId());
	}

	@Override
	protected void handleTextMessage(
			WebSocketSession session, TextMessage message) throws Exception {
		log(session.getId() + "로부터 메시지 수신: " + message.getPayload());
		for (WebSocketSession s : users.values()) {
			s.sendMessage(message);
			log(s.getId() + "에 메시지 발송: " + message.getPayload());
		}
	}

	@Override
	public void handleTransportError(
			WebSocketSession session, Throwable exception) throws Exception {
		log(session.getId() + " 예외 발생: " + exception.getMessage());
	}

	private void log(String logmsg) {
		LOGGER.info(new Date() + " : " + logmsg);
	}

}
  • "/chat-ws" 로 웹소켓 연결 요청이 들어오면 웹소켓 핸들러(ChatHandler) 실행
  • afterConnectionEstablished() : 웹소켓 최초연결 시, WebSocketSession 저장
  • WebsocketSession : 웹소켓 연결될 때 생기는 연결 정보를 담는 객체
    • 세션을 컬렉션으로 관리 : 모든 클라이언트에게 메세지를 보내는 것처럼 관리할 수 있음
  • handleTextMessage()를 통해 메세지를 클라이언트로 부터 받음
  • WebSocketSession.sendMessage()를 통해 클라이언트에게 메세지 전송
  • afterConnectionClosed() : 웹소켓 종료 시, WebSocketSession 삭제
  • WebSocketSession은 동시성을 지원하지 않는다
    • 때문에 ConcurrentHashMap 컬렉션을 사용하여 동시성을 보장한다

 

4) 웹소켓 구현 화면

chat-ws.jsp

<%@ page contentType="text/html; charset=UTF-8" trimDirectiveWhitespaces="true"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Websockeet Chatting</title>
<!-- <script type="text/javascript" src="js/jquery-3.6.0.min.js"></script> -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script>
<script type="text/javascript">
	var wsocket;
	
	function connect() {
		var serverUrl = $("#serverUrl").val();
		alert("Call Websocket URL : "+serverUrl);
		
		wsocket = new WebSocket(serverUrl);
		wsocket.onopen = onOpen;
 		wsocket.onmessage = onMessage;
		wsocket.onclose = onClose; 
	}
	function disconnect() {
		wsocket.close();
	}
	function onOpen(evt) {
		appendMessage("Connected~!");
	}
	function onMessage(evt) {
		var data = evt.data;
		if (data.substring(0, 4) == "msg:") {
			appendMessage(data.substring(4));
		}
	}
	function onClose(evt) {
		appendMessage("Websocket Closed !!!");
		console.log(evt);
	}
	
	function send() {
		var nickname = $("#nickname").val();
		var msg = $("#message").val();
		wsocket.send("msg:"+nickname+":" + msg);
		$("#message").val("");
	}

	function appendMessage(msg) {
		console.log(">> mag: "+msg);
		$("#chatMessageArea").append(msg+"<br>");
		var chatAreaHeight = $("#chatArea").height();
		var maxScroll = $("#chatMessageArea").height() - chatAreaHeight;
		$("#chatArea").scrollTop(maxScroll);
	}

	$(document).ready(function() {
		$('#message').keypress(function(event){
			var keycode = (event.keyCode ? event.keyCode : event.which);
			if(keycode == '13'){
				send();	
			}
			event.stopPropagation();
		});
		$('#sendBtn').click(function() { send(); });
		$('#enterBtn').click(function() { connect(); });
		$('#exitBtn').click(function() { disconnect(); });
	});
</script>
<style>
#chatArea {
	width: 200px; height: 100px; overflow-y: auto; border: 1px solid black;
}
</style>
</head>
<body>
	<input type="hidden" id="serverUrl" value="ws://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/chat-ws" style="width:400px"><br>

	닉네임 : <input type="text" id="nickname">
	<input type="button" id="enterBtn" value="Enter">
	<input type="button" id="exitBtn" value="Exit">
    
    <h1>채팅창</h1>
    <div id="chatArea"><div id="chatMessageArea"></div></div>
    <br/>
    <input type="text" id="message">
    <input type="button" id="sendBtn" value="Send Message">
</body>
</html>

 

 

채팅 참여

 

 

채팅하기

 

 

채팅 종료

 

 

 

 

왕왕 재밌죠?? ㅎㅎㅎ

지금은 채팅방이 없는 간단한 웹소켓 연결이지만

다음에는 STOMP를 활용한 채팅방도 만들어 볼게요~~ 

굉장해 엄청나 궁금하다고요?? 아이참~~ 기둘핑

 

 

 

 

 

 

참고
- https://docs.spring.io/spring-framework/reference/web/websocket/server.html
- https://blog.naver.com/eztcpcom/220070508655
- https://dev-gorany.tistory.com/212
- ​https://velog.io/@koseungbin/WebSocket#websocket-session-%EB%8F%99%EC%8B%9C%EC%84%B1

감사합니다 땡쿠
 

WebSocket API :: Spring Framework

The Spring WebSocket API is easy to integrate into a Spring MVC application where the DispatcherServlet serves both HTTP WebSocket handshake and other HTTP requests. It is also easy to integrate into other HTTP processing scenarios by invoking WebSocketHtt

docs.spring.io