Spring Framework 웹소켓 (22) - Day 69

jaehoon ahn·2025년 12월 29일

Spring boot

목록 보기
20/20
post-thumbnail

웹소켓

HTTP 초기 목적

⇒ 문서 전달

HTTP의 한계

⇒ HTTP는 클라이언트에게 요청이 와야만 응답을 할 수 있다.

⇒ 이 한계를 극복하기 위해서는 실시간 양방향 통신이 필요

웹소켓 라이프사이클

  1. HTTP 요청을 클라이언트에서 서버쪽으로 먼저 보냄
  2. 그 후 서버가 웹소켓으로 통신을 하자고 응답을 보냄
    1. 이때부터 HTTP 통신이 아닌 웹소켓 통신이 됌

⇒ 해당 과정을 Handshake라고 함

⇒ 클라이언트와 서버가 웹소켓 통신을 하기 위한 일련의 과정을 Handshake라고 부름

⇒ 이때부터 양방향 통신이 이뤄짐 ⇒ 클라이언트 ↔ 서버

⇒ 각각 먼저 보낼 수 있음

  1. close
    1. 웹소켓 통신을 종료

⇒ close 또한 양쪽에서 진행할 수 있음

설치(Sock.js)

<!---- sockJS 를 이용한 WebSocket 구현을 위해 라이브러리 추가 ------->
<!-- https://github.com/sockjs/sockjs-client -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>

build.gradle → dependencies

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

⇒ 해당 모듈이 존재해야함

테스트

WebSocketConfig.java

package edu.kh.project.websocket.config;

import edu.kh.project.websocket.handler.TestWebSocketHandler;
import lombok.RequiredArgsConstructor;

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 org.springframework.web.socket.server.HandshakeInterceptor;

@Configuration // 서버 실행 시 작성된 메서드를 모두 수행 + Bean 등록
@EnableWebSocket // 웹소켓 활성화 설정
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {

	private final TestWebSocketHandler testWebSocketHandler;
	private final HandshakeInterceptor handshakeInterceptor;
	// 의존성 주입 : 같은 타입 or 상속관계의 타입,
	// 즉 Bean으로 등록된 SessionHandshakeInterceptor가 의존성 주입됌

	// WebSocketConfigurer :
	// Spring에서 웹소켓 통신을 어디에서, 어떤 방식으로 할 것인지
	// 규칙을 정의하는 설정용 인터페이스
	// 1. 핸들러 등록
	// 2. 접속 주소 매핑
	// 3. 부가 기능 설정

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		// addHandler(웹소켓핸들러, "웹소켓 요청 주소")
		registry.addHandler(testWebSocketHandler, "/testSock")
				// 클라이언트가 http://localhost/testSock으로 클라이언트 요청을 보내면
				// Websocket 통신으로 변환 후 testWebsocketHandler가 처리하도록 등록
				.addInterceptors(handshakeInterceptor)
				// 클라이언트 연결 시 session을 가로채 핸들러에게 전달하는 handshakeInterceptor 등록
				.setAllowedOriginPatterns("http://localhost/", "http://127.0.0.1/", "http://192.168.132.20")
				// 웹소켓 요청이 허용되는 ip/도메인 지정
				.withSockJS();
	}
}

SessionHandshakeInterceptor.java

package edu.kh.project.websocket.interceptor;

import java.util.Map;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import jakarta.servlet.http.HttpSession;

// SessionHandshakeInterceptor 클래스

// - WebSocketHandler가 동작하기 전/후에
// 연결된 클라이언트의 세션을 가로채는 동작을 작성할 클래스

// Handshake : 클라이언트와 서버가 Websocket 연결을 수립하기 위해 HTTP 프로토콜을 통해
// 수행하는 초기 단계
// -> 즉, 기존 HTTP 연결을 WebSocket 연결로 변경

// 클라이언트마다 고유한 세션을 가짐
// 클라이언트의 세션을 가져와서 해당 클라이언트에게 메세지를 보낸다는 것을 해주기 위해
// 클라이언트의 세션을 가로채는 동작이 필요

// 핸드쉐이크 코드는 js에서 진행
@Component // 일반적인 Bean으로 등록할 때 사용하는 어노테이션
public class SessionHandshakeInterceptor implements HandshakeInterceptor {

	// 핸들러 동작 전에 수행되는 메서드
	@Override
	public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Map<String, Object> attributes) throws Exception {
		// ServerHttpRequest : HttpServletRequest의 부모 인터페이스
		// ServerHttpResponse : HttpServletResponse의 부모 인터페이스

		// attributes : 해당 맵에 세팅된 속성(데이터)은
		// 다음에 동작할 Handler 객체에게 전달됨
		// HandshakeInterceptor -> Handler 데이터 전달하는 역할

		// request가 참조하는 객체가
		// SetvletServerHttpRequest로 다운캐스팅이 가능한가?
		if (request instanceof ServletServerHttpRequest) {
			// instanceof : 부모 자식관계인지 확인하는 키워드
			// 다운캐스팅
			ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
			// ServerHttpRequest(부모) -> ServletServerHttpRequest(자식)

			// 웹소켓 동작을 요청한 클라이언트의 세션을 얻어옴
			HttpSession session = servletRequest.getServletRequest().getSession();
			// servletRequest.getServletRequest() = 여기까지가 request
			// request.getSession() 가져오기

			// 가로챈 클라이언트의 세션을 Handler에 전달할 수 있게 세팅
			attributes.put("session", session);

		}
		// true or false에 따라 인터셉터를 진행할지 말지를 결정
		// 즉, 가로채기 진행 여부 : true로 작성해야 세션을 가로채서 핸들러에게 전달하는게 가능해짐

		// 현재까지는 클라이언트에서 서버에게 요청을 보내는 (session전달) 역할을 하는 코드를 작성한 것
		return true;
	}

	// 핸들러 동작 후에 수행되는 메서드
	@Override
	public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Exception exception) {

	}

}

TestWebSocketHandler.java

package edu.kh.project.websocket.handler;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.springframework.stereotype.Component;
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;

import lombok.extern.slf4j.Slf4j;

// OOOWebSocketHandler 클래스
// - 웹소켓 동작 시 수행할 구문을 작성하는 클래스
// 즉, 어떤 동작을 하고싶은 것인지를 의미

@Component
@Slf4j
public class TestWebSocketHandler extends TextWebSocketHandler {

	// 동기화된 Set 생성
	private Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<>());
	// -> 여러 스레드가 동작하는 환경에서 하나의 컬렉션에 여러 스레드 접근하여 의도치 않은 문제가
	// 발생되지 않게 하기 위해 동기화를 진행하여 스레드가 순서대로 동작할 수 있도록 함.

	// WebSocketSession :
	// 클라이언트 - 서버 간 양방향 통신을 담당하는 객체
	// SessionHandshakeInterceptor가 가로챈
	// 연결된 클라이언트의 HttpSession을 가지고 있음

	// 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행하는 메서드
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		// 연결된 클라이언트의 WebSocketSession 정보를 Set에 추가
		// -> 웹소켓에 연결된 클라이언트 정보를 모아두기 위함
		sessions.add(session);
	}

	// 클라이언트와 연결이 종료되면 실행
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		// 웹소켓 연결이 끊긴 클라이언트 정보를 Set에서 제거
		sessions.remove(session);
	}

	// 클라이언트로부터 텍스트 메시지를 받았을 때 실행
	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		// TextMessage : 웹소켓으로 연결된 클라이언트가 전달한
		// 텍스트(내용)가 담겨있는 객체
		// message.getPayload() : 통신 시 탑재된 데이터 자체

		log.info("전달받은 메시지 : {}", message.getPayload());

		// 전달 받은 메시지를
		// 현재 해당 웹소켓에 연결된 모든 클라이언트에게 보내기
		for (WebSocketSession s : sessions) {
			s.sendMessage(message);
		}
	}
}

/*
 * WebSocketHandler 인터페이스 : 웹소켓을 위한 메소드를 지원하는 인터페이스 -> WebSocketHandler 인터페이스를
 * 상속받은 클래스를 이용해 웹소켓 기능을 구현 WebSocketHandler 주요 메소드
 * 
 * void handlerMessage(WebSocketSession session, WebSocketMessage message) -
 * 클라이언트로부터 메세지가 도착하면 실행
 * 
 * void afterConnectionEstablished(WebSocketSession session) - 클라이언트와 연결이 완료되고,
 * 통신할 준비가 되면 실행 void afterConnectionClosed(WebSocketSession session,
 * CloseStatus closeStatus) - 클라이언트와 연결이 종료되면 실행 void
 * handleTransportError(WebSocketSession session, Throwable exception) - 메세지 전송중
 * 에러가 발생하면 실행 ----------------------------------------------------------------
 * TextWebSocketHandler : WebSocketHandler 인터페이스를 상속받아 구현한 텍스트 메세지 전용 웹소켓 핸들러
 * 클래스 handlerTextMessage(WebSocketSession session, TextMessage message) -
 * 클라이언트로부터 텍스트 메세지를 받았을때 실행
 * 
 * BinaryWebSocketHandler: WebSocketHandler 인터페이스를 상속받아 구현한 이진 데이터 메시지를 처리하는 데
 * 사용. 주로 바이너리 데이터(예: 이미지, 파일)를 주고받을 때 사용.
 */

websocket_test.js

// websocket_test.js
// 웹소켓 테스트

// 1. SockJS 라이브러리 추가
// -> common.html 추가해둠

// 2. SockJS 객체 생성
const testSock = new SockJS("/testSock");
// - WebSocket 통신을 할 수 있게끔 해주는
// 객체를 생성함과 동시에 자동으로
// (localhost)/test으로 연결 요청을 보냄
// => 서버에게 보내는 요청 -> http://localhost/testSock으로 연결 요청을 보냄
// => 추후 http://는 ws://로 바뀜

// 3. 생성된 SockJS 객체를 이용해서 서버에 메시지 전달
const sendMessageFn = (name, str) => {
  // JSON을 이용해서 데이터를 TEXT 형태로 전달
  const obj = {
    name: name,
    str: str,
  };
  testSock.send(JSON.stringify(obj));
};

// sendMessageFn("홍길동", "안녕하세요~");

// 4. 서버로부터 클라이언트에게
// 웹소켓을 이용한 메시지가 전달된 경우
testSock.addEventListener("message", (e) => {
  // e.data : 서버로부터 전달받은 message
  const msg = JSON.parse(e.data); // 서버로부터 JSON 형태로 받아온 것을 JS 객체 형태로 변환
  console.log(`${msg.name}의 메세지 : ${msg.str}`);
});

0개의 댓글