웹소켓(WebSocket)은 클라이언트와 서버 간의 양방향 통신을 가능하게 하는 프로토콜입니다. 이를 통해 웹 애플리케이션에서 실시간으로 데이터를 주고받을 수 있습니다.
웹 소켓의 특징 :
- Stateful :
- 웹소켓은 상태를 유지(stateful)하는 연결 방식입니다. 일반 HTTP 요청과 달리, 클라이언트와 서버 간의 연결이 유지되며 지속적인 통신이 가능합니다. 이는 실시간 애플리케이션에서 매우 유용합니다.
- 클라이언트와 서버 둘 다 리스닝을 유지 :
- 웹소켓은 클라이언트와 서버가 지속적으로 연결을 유지하며 서로 데이터를 주고받을 수 있는 양방향 통신 프로토콜입니다. 일반적으로 클라이언트와 서버는 이벤트 기반의 비동기 방식으로 통신을 처리하며, 이는
while
루프와 같은 지속적인 리스닝을 포함할 수 있습니다. 하지만 실제 구현에서는 이벤트 핸들러를 통해 효율적으로 데이터를 주고받습니다.
웹 소켓 간단 예제
config(설정파일) :
// SRP : 마트 점원 (메시지 브로커) 세팅
@EnableWebSocketMessageBroker
@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// 웹 소켓 엔드포인트 설정
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/connect").setAllowedOriginPatterns("*");
}
// 구독, 발행 엔드포인트 설정
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub"); // sub로 시작하는 모든 주소
registry.setApplicationDestinationPrefixes("/pub"); // pub로 시작하는 모든 주소
}
}
클래스 및 어노테이션:
@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
- @Configuration: 이 클래스가 Spring의 설정 클래스임을 나타냅니다.
- WebSocketMessageBrokerConfigurer 인터페이스: WebSocket 메시지 브로커를 설정하기 위해 필요한 메서드를 제공합니다.
웹 소켓 엔드포인트 설정:
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/connect").setAllowedOriginPatterns("*");
}
- registerStompEndpoints: STOMP (Simple Text Oriented Messaging Protocol) 엔드포인트를 등록합니다. 클라이언트는 이 엔드포인트를 통해 WebSocket 서버에 연결할 수 있습니다.
- addEndpoint("/connect"): 클라이언트가
/connect
엔드포인트로 연결할 수 있게 합니다.
- setAllowedOriginPatterns("")*: 모든 도메인에서의 접속을 허용합니다. 이는 Cross-Origin Resource Sharing (CORS) 문제를 해결합니다. 실제 운영 환경에서는 특정 도메인만 허용하는 것이 보안에 좋습니다.
메시지 브로커 설정:
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub"); // sub로 시작하는 모든 주소
registry.setApplicationDestinationPrefixes("/pub"); // pub로 시작하는 모든 주소
}
- configureMessageBroker: 메시지 브로커를 구성합니다.
- enableSimpleBroker("/sub"): 간단한 메시지 브로커를 활성화하고,
sub
로 시작하는 주소를 대상으로 메시지를 브로킹합니다. 예를 들어, 클라이언트는/sub/topic/messages
와 같은 주소를 구독할 수 있습니다.
- setApplicationDestinationPrefixes("/pub"): 발행(publish) 주소의 접두사를
pub
로 설정합니다. 클라이언트는/pub/message
와 같은 주소로 메시지를 전송할 수 있습니다.
v1
index 파일 내용 :
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
</head>
<body>
<div>
<ul>
<li><a href="/">채팅목록</a></li>
<li><a href="/chat-form">채팅 메시지쓰기</a></li>
</ul>
</div>
<h1>채팅 목록</h1>
<button onclick="send1()">메시지전송(pub)1</button>
<hr>
<ul id="chat-box">
{{#models}}
<li>{{msg}}</li>
{{/models}}
</ul>
<script>
// 1. 웹 소켓 연결 세팅 및 연결 완료
let socket = new WebSocket('ws://localhost:8080/connect');
let stompClient = Stomp.over(socket);
stompClient.connect({}, (frame) => {
console.log("1. Connected")
// 2. 구독하기 /sub -> /pub/room 이런 url로 요청해야 발동
stompClient.subscribe("/sub/1", (response) => {
console.log("2. Sub/1")
console.log(response);
});
});
function send1() {
stompClient.send("/pub/room", {}, "1");
}
</script>
</body>
</html>
웹 소켓 연결 설정:
let socket = new WebSocket('ws://localhost:8080/connect');
let stompClient = Stomp.over(socket);
stompClient.connect({}, (frame) => {
console.log("1. Connected")
// 2. 구독하기 /sub -> /pub/room 이런 url로 요청해야 발동
stompClient.subscribe("/sub/1", (response) => {
console.log("2. Sub/1")
console.log(response);
});
});
- 웹 소켓 생성:
let socket = new WebSocket('ws://localhost:8080/connect');
WebSocket
객체를 생성하여 서버에 연결합니다. 여기서는 ws://localhost:8080/connect
엔드포인트로 연결합니다.- STOMP 클라이언트 생성:
let stompClient = Stomp.over(socket);
Stomp.over(socket)
를 사용하여 STOMP 클라이언트를 생성합니다.- 웹 소켓 연결 및 구독 설정:
stompClient.connect({}, (frame) => {
console.log("1. Connected")
// 구독 설정
stompClient.subscribe("/sub/1", (response) => {
console.log("2. Sub/1")
console.log(response);
});
});
connect
메서드: 서버와 STOMP 연결을 설정합니다. 연결이 완료되면 콜백 함수가 실행됩니다.subscribe
메서드: 특정 경로(/sub/1
)를 구독하여 해당 경로로 오는 메시지를 수신합니다. 메시지가 도착하면 콜백 함수가 실행되어 콘솔에 출력됩니다.메시지 전송 함수:
function send1() {
stompClient.send("/pub/room", {}, "1");
}
send1
함수:/pub/room
경로로 "1"이라는 메시지를 전송합니다.
stompClient.send
메서드: 특정 경로로 메시지를 발행합니다. 여기서는/pub/room
경로로 빈 헤더와 함께 "1" 메시지를 전송합니다.
전체 흐름:
- 웹 소켓 연결: 클라이언트는
ws://localhost:8080/connect
엔드포인트로 WebSocket 연결을 설정합니다.
- STOMP 클라이언트 설정: STOMP 프로토콜을 사용하여 WebSocket을 통해 메시지를 주고받기 위한 클라이언트를 설정합니다.
- 연결 완료 후 구독: 연결이 완료되면
/sub/1
경로를 구독하여 해당 경로로 전송된 메시지를 수신합니다.
- 메시지 전송:
send1
함수가 호출되면/pub/room
경로로 메시지 "1"을 전송합니다.
controller :
// /pub/room 에서 /pub가 생략되어있음
@MessageMapping("/room")
public void pubTest(String number) {
System.out.println("확인 : " + number);
sms.convertAndSend("/sub/" + number, "hello world " + number);
}
- @MessageMapping("/room"):
- 이 어노테이션은
/room
경로로 들어오는 메시지를 이 메서드(pubTest
)로 매핑합니다. - 클라이언트는
/pub/room
경로로 메시지를 보낼 때, 이 메서드가 실행됩니다. 여기서/pub
는 생략된 상태입니다.
- public void pubTest(String number):
pubTest
메서드는String
타입의number
를 매개변수로 받습니다. 이는 클라이언트가 보낸 메시지의 본문입니다.
- System.out.println("확인 : " + number);:
- 전달된
number
값을 콘솔에 출력합니다. 주로 디버깅 목적입니다.
- sms.convertAndSend("/sub/" + number, "hello world " + number);:
sms
객체를 사용하여/sub/{number}
경로로 메시지를 전송합니다.- 메시지 내용은
"hello world " + number
입니다. - 여기서
number
값은 클라이언트가 구독한 경로의 일부가 됩니다.
v2 (글을 썼을 때 전달)
index :
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
</head>
<body>
<div>
<ul>
<li><a href="/">채팅목록</a></li>
<li><a href="/save-form">채팅 메시지쓰기</a></li>
</ul>
</div>
<h1>채팅 목록</h1>
<hr>
<ul id="chat-box">
{{#models}}
<li>{{msg}}</li>
{{/models}}
</ul>
<script>
// 1. 웹소켓 연결 세팅 및 연결 완료
let socket = new WebSocket('ws://192.168.0.88:8080/connect'); // 현재 컴퓨터의 ip 주소
let stompClient = Stomp.over(socket);
stompClient.connect({}, (frame)=>{
console.log("1", "Connected");
stompClient.subscribe("/sub/chat", (response)=>{
console.log("2", response);
let body = JSON.parse(response.body);
console.log("3", body);
let site = document.querySelector("#chat-box");
let dom = document.createElement("li");
dom.innerHTML = body.msg;
site.prepend(dom);
});
});
</script>
</body>
</html>
v1 과의 변경점 :
- 경로가 /sub/chat 으로 변경되었습니다.
- 응답을 받으면 JSON으로 파싱한 뒤 HTML 요소로 변환하여
prepend
메서드를 사용해 요소를 리스트의 제일 앞에 추가합니다.
controller :
@RequiredArgsConstructor
@Controller
public class ChatController {
private final ChatService chatService;
private final SimpMessageSendingOperations sms;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("models", chatService.findAll());
return "index";
}
@GetMapping("/save-form")
public String saveForm(){
return "save-form";
}
@PostMapping("/chat")
public String save(String msg) {
Chat chat = chatService.save(msg);
sms.convertAndSend("/sub/chat", chat);
return "redirect:/";
}
}
@PostMapping("/chat")
메서드는 클라이언트로부터 전달받은 문자열msg
를 저장합니다.
- 저장된
msg
는Chat
객체로 변환되어 데이터베이스에 삽입됩니다.
- 그런 다음,
sms.convertAndSend("/sub/chat", chat)
를 호출하여/sub/chat
경로를 구독하고 있는 모든 클라이언트에게Chat
객체를 전송합니다.
- 이 과정이 완료되면 메서드는 "redirect:/"를 반환하여 클라이언트를 루트 경로(
/
)로 리디렉션합니다.
Share article