웹소켓(Websocket)은 TCP를 이용한 통신 프로토콜로, HTTP의 Stateless한 점을 개선하기 위해 만들어졌다. 기존의 HTTP는 사용자가 요청을 보내야 서버에서 응답을 받을 수 있지만, 웹소켓을 사용하면 사용자의 요청 없이도 서버에서 데이터를 받아 사용할 수 있게 된다.
예를 들어 채팅 앱을 만들 때, 유저 A가 유저 B에게 메세지를 보냈다고 하자. HTTP만을 사용하여 이 기능을 구현한 경우 유저 B가 서버에 주기적으로 요청을 보내야 메세지를 받았다는 것을 알 수 있지만, 웹소켓을 사용한 경우 유저 A가 메세지를 보냈을 때 서버에서 이 요청을 처리하면서 유저 B에게 메세지를 받았다는 데이터를 전송할 수 있는 것이다.
1. Spring에 WebSocket dependency 추가
Spring은 기본적으로 SockJS와 STOMP라는 텍스트 기반 프로토콜을 이용해 웹소켓 기능을 구현한다.
- SockJS는 웹소켓 통신 구축을 담당하는데, 일부 브라우저가 웹소켓을 지원하지 않는 경우 HTTP 통신을 주기적으로 보내는 polling 방식 등으로 웹소켓을 대체한다.
- STOMP는 Simple Text Oriented Messaging Protocol, 즉 간단한 텍스트를 주고받는 프로토콜이다. 말은 어렵지만 결국 REST API를 구현할 때처럼 DTO를 이용해 데이터를 전송하게 돕는다고 보면 된다.
Spring에 웹소켓 dependency를 설치하기 위해선 build.gradle에 아래 코드를 추가하거나, Spring initializr에서 Websocket을 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
2. WebSocket 설정 클래스 생성
Spring 프로젝트에 WebSocket dependecncy를 추가했다면, 다음으로는 WebSocket 설정 클래스를 만들어야 한다.
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry
// 주어진 prefix로 시작하는 경로로 메세지가 들어오면
// @Controller의 @MessageMapping으로 전송
.setApplicationDestinationPrefixes("/ws")
// 주어진 prefix로 시작하는 경로를 subscribe하는
// 클라이언트에게 메세지를 전송
// 해당 경로는 내장된 STOMP broker가 관리
.enableSimpleBroker("/topic");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// SockJS를 이용해 연결을 유지할 경로
registry
.addEndpoint("/websocket")
.withSockJS();
}
}
웹소켓 설정 클래스는 기본적으로 설정 클래스임을 표시하는 @Configuration
어노테이션과 웹소켓 브로커 설정을 활성화하는 @EnableWebSocketMessageBroker
어노테이션을 갖고, 웹소켓 브로커 설정을 바꿀 수 있는 메소드를 포함한 WebSocketMessageBrokerConfigurer
인터페이스를 상속받는다.
설정 클래스에선 크게 두 메소드를 Override해야 하는데, 하나는 STOMP에서 DTO를 주고받는 기능인 Broker에 관한 메소드인 configureMessageBroker
이고, 다른 하나는 SockJS를 이용해 웹소켓 통신을 구축할 경로를 설정하는 메소드인 registerStompEndpoints
이다.
configureMessageBroker
메소드는 특정 경로로 요청이 들어온 경우, 웹소켓 처리를 담당하는 Controller를 거쳐 해당 요청을 클라이언트로 전송하는 STOMP Broker로 넘기도록 설정한다. 예를 들어 위의 예제같은 경우, /ws로 시작하는 경로(예를 들어 /ws, /ws/{userId} 등)로 요청이 들어오면 해당 요청을 Controller로 넘긴 다음, Controller에선 요청을 처리한 뒤 /topic으로 시작하는 경로(예를 들어 /topic, /topic/private 등)를 사용하는 STOMP Broker로 넘기게 된다.registerStompEndpoints
는 웹소켓 통신을 구축할 경로를 설정하는 메소드이다. 예를 들어 위의 예제같은 경우 클라이언트에서 SockJS를 이용해 /websocket으로 연결 요청을 보내면 웹소켓 통신이 생성된다.
3. WebSocket 요청 처리 Controller 생성
WebSocket 설정 클래스까지 만들어 주었다면, 마지막으로는 실제로 WebSocket 요청을 처리하는 Controller 클래스를 만들어야 한다. Controller 클래스에 요청을 보내는 방식은 두 가지가 있는데, 하나는 STOMP의 클라이언트 라이브러리를 이용해 직접 백엔드의 STOMP로 요청을 보내는 방식이고, 다른 하나는 REST API를 이용해 간접적으로 요청을 보내는 방식이다.
이하는 content라는 클라이언트에서 STOMP 라이브러리를 통해 필드를 가진 DTO 클래스를 받아 content의 HTML 태그를 제거해 STOMP Broker로 반환하는 예제이다.
import java.security.Principal;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;
@Controller
public class MessageController {
@MessageMapping("/public")
@SendTo("/topic/public")
public DTO getMessage(final DTO dto) {
dto.setContent(HtmlUtils.htmlEscape(dto.getContent()));
return dto;
}
// 개인에게 보내는 메세지는 송신자에게 바로 돌아옴 -> 무의미
}
이하는 위의 예제 기능을 REST API를 통해 구현하되, 전체 사용자 혹은 특정 사용자에게 전달하는 예제이다.
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import lombok.AllArgsConstructor;
@RestController
@RequestMapping("message")
@AllArgsConstructor
public class WebSocketController {
private final SimpMessagingTemplate messagingTemplate;
@RequestMapping(method = RequestMethod.POST, value="")
public void sendPublicMessage(@RequestBody final DTO dto) {
dto.setContent(HtmlUtils.htmlEscape(dto.getContent()));
messagingTemplate.convertAndSend("/topic/public", dto);
}
@RequestMapping(method = RequestMethod.POST, value="{user}")
public void sendPrivateMessage(
@PathVariable final String user,
@RequestBody final DTO dto
) {
dto.setContent(HtmlUtils.htmlEscape(dto.getContent()));
messagingTemplate.convertAndSendToUser(user, "/topic/private", dto);
}
}
위 두 예제를 보면 STOMP로 요청을 받은 경우에는 DTO를 반환하고, REST API로 요청을 받은 경우에는 SimpMessagingTemplate 객체를 이용해 DTO를 넘겨주는 것을 알 수 있다.
'웹 > Spring' 카테고리의 다른 글
Spring Security를 이용한 세션 확인과 커스텀 인가 (0) | 2022.04.25 |
---|---|
Spring Security에서 JWT를 이용해 인증 토큰 발행하기 (0) | 2022.04.13 |
Spring Security에서 사용자 인가하기 (0) | 2022.04.12 |
Spring Security에서 DB에 존재하는 사용자 인증하기 (0) | 2022.04.12 |
Spring 프로젝트에서 Spring Security 사용하기 (0) | 2022.04.12 |