프로젝트의 요구사항 중 같은 프로젝트에 소속되어있는 인원들끼리 이용할 수 있는 채팅방을 구현해야하는 요구사항이 있었다. 이를 구현해보기 위해 공부한 내용을 정리한다.
rest api 를 사용한 프로젝트에서 채팅은 http를 이용해서 구현할 수 있을까? 가능은 해도 매우 비효율적일 것이다. 이는 http의 특성을 보면 알 수 있다.
1. 클라이언트 - 서버 구조 : 서버에서는 클라이언트의 요청이 와야 응답을 한다. 그 전까지 서버는 대기한다.
2. stateless : http를 이용해 클라이언트에서 서버로 한 번 요청과 응답을 주고 받았어도, 서버에서는 http 요청했던 클라이언트의 상태를 저장하지 않는다.
3. connectionless : 한 번 요청 후 응답을 받으면 클라이언트와 서버 간 연결을 끊는다.
실시간 채팅을 구현할 때 다른 api처럼 http기반으로 구현하면 지속적으로 서버에 요청을 계속 해야할 것이며, 양방향 통신을 지원해야할 것이다. (물론 polling 방식같이 http로 구현할 방법이 없는 것은 아니다.)
그래서 연결이 지속적으로 유지되며, 양방향으로 통신을 지원하는 프로토콜인 websocket을 사용하기로 했다.
WebSocket
websocket은 실시간 양방향 통신 서비스에 적합한 프로토콜이다.

websocket은 위와 같은 프로세스로 진행된다.
1. 클라이언트에서 서버로 http요청을 통해 handshake 요청을 전달한다. (opening handshake)
2. http 요청을 통해 websocket 접속을 요청받으면 서버는 websocket프로토콜로 변경한다.
3. 서버에서 handshake에 대한 응답을 한다. 연결이 성공하면 101응답코드를 클라이언트가 받는다.
3. 양방향 통신을 통해 데이터를 주고 받는다. 이 때, '메시지' 라는 단위로 주고 받는다. 메시지는 한 개 이상의 프레임으로 구성되어 있다. (프레임은 osi 7 계층 중 2계층인 데이터 링크 계층에서 서로 주고 받는 가장 작은 단위)
4. websocket 연결 종료를 위해 클라이언트, 또는 서버에서 컨트롤 프레임 전송하고 응답하는 과정을 통해 연결을 종료한다. (closing handshake)
대충 이런 과정을 담고 있다. websocket을 이제 spring boot에 적용해보면 다음과 같은 과정으로 진행된다.
Stomp란?
stomp(Simple Text Oriented Messaging Protocol)가 무엇인가?
websocket은 메시지를 중심으로 데이터를 주고 받는 프로토콜인데, 메시지의 경우 프레임으로 구성되어있다고 언급했다. 프레임의 경우 텍스트 데이터, 이진 데이터, 컨트롤 프레임으로 분류되고, 즉 이는 메시지에는 텍스트 데이터나 이진 데이터 타입의 데이터만 담길 수 있다는 것이다.
문제는 메시지를 서버에서 처리할 때, 어떤 타입의 데이터인지, 어떤 요청인 처음에는 모르기에 그에 맞춰 기능들을 다 구현해야할 필요가 있다. 이를 단순화하기 위한 프로토콜이 stomp이다.
stomp는 websocket을 기반으로 동작하는 서브 프로토콜로, 텍스트 기반 메시징 프로토콜인데, 이걸 사용하면 다음과 같은 장점들이 있다.
1. 간단한 구조를 가진다. 메시지 형식이 http 메시지 형태와 유사하며, commandline을 지정해 어떤 유형의 메시지인지 서버에게 알려줄 수 있다.
2. 메시지의 헤더 값을 지정할 수 있다. 이를 통해 인증을 적용할 수 있다.
3. pub/sub구조로, 메시지 공급자와 구독자를 구분하여 관리한다. 메시지 브로커가 메시지를 구독자들 다수에게 전달하는 구조.
4. spring 에서 stomp를 지원하기때문에, 쉽게 응용이 가능하다.
코드 작성
먼저 configuration 이다.
WebSocketConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chattings")
.setAllowedOriginPatterns("*")
.withSockJS(); //낮은 버전 브라우저도 사용할 수 있도록.
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub"); // (1) 메시지 구독 요청: 메시지 송신
registry.setApplicationDestinationPrefixes("/pub"); // (2) 메시지 발행 요청: 메시지 수신
}
}
WebSocketMessageBrokerConfigurer 인터페이스를 구현한다. 두 번째 메서드에서 enableSimpleBroker가 위에서 언급한 메시지 브로커가 되겠다. 만약 다중 인스턴스 배포 환경이라면 kafka와 같은 브로커로 변경해야 할 것이다. simplebroker는 인 메모리 방식이기 때문이다.
ws://localhost:8080/chattings를 호출하면 websocket 연결이 될 것이다. 이후 /sub를 통해 채팅방에 구독 신청을 하고, 채팅 데이터를 전송할 때 마다 /pub관련 메서드를 호출해 채팅방 구독하는 모두에게 메시지 브로커가 메시지를 전달할 것이다.
ChattingRequest.java
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChattingRequest {
private Long senderId;
private String content;
}
ChattingController.java
@RestController
@RequiredArgsConstructor
public class ChattingController {
private final ChattingService chattingService;
private final SimpMessagingTemplate simpMessagingTemplate; //service에서 controller 단으로 옮김. 어디로 보내는지 더 보기 쉽게
@MessageMapping("/{chattingRoomId}/messages")
// @SendTo("/sub/{chattingRoomId}") //convertAndSend대신 이걸 사용해도 됨
public void chat(@DestinationVariable("chattingRoomId") Long chattingRoomId, ChattingRequest chattingRequest) { //@DestinationVariable에 추가로 chattingRoomId를 괄호 쓰고 넣어줘야 파라미터 인식을 함.
chattingService.save(chattingRoomId, chattingRequest);
simpMessagingTemplate.convertAndSend("/sub/" + chattingRoomId, chattingRequest.getContent());
}
}
chattingController에서 채팅 발행 관련 엔드포인트가 관리된다.
- @MessageMapping
@MessageMapping에 작성된 경로와 이전에 configuration에서 지정한 prefix인 /pub가 합쳐져 /pub/{chattingRoomId}/messages로 메시지 발행 요청을 하는 것이다. 컨트롤러의 역할을 하는 것과 다름이 없는 것이다.
- SimpleMessagingTemplate
simpleMessagingTemplate는 이전에 @EnableWebSocketMessageBroker를 통해 bean으로 등록된 것이다. 이를 통해 메시지 브로커로 데이터를 전달한다.
위의 내용은 /sub/chattingRoomId를 구독하고 있는 모든 클라이언트에게 메시지 브로커가 메시지를 보내주는 것이다.
실행과정을 정리하면 다음과 같다.
1. ws://localhost:8080/chattings을 통해 websocket연결을 한다.
2. 채팅방에 들어가면, 채팅이 올라오는 것을 실시간으로 확인할 수 있어야 하니 구독을 요청한다. /sub/1 같은 경우 1번 채팅방을 구독한 것이다.
3. 이후 채팅을 하면 /sub/1을 구독하고 있는 (즉, 1번 채팅방을 구독하는) 모두에게 전달되어야하니 /pub/1/messages를 호출해 메시지 브로커를 통해 모두에게 채팅을 전달하고, DB에 채팅 내용을 저장한다.
chattingService같은 경우 jpaRepository를 이용해 db에 저장하는 메서드이다.
Stomp 테스트
stomp를 테스트는 postman같은 곳에서는 지원하지 않는다. 이전에는 apic이라는 크롬 확장 프로그램을 썼다고 하는데, 현재는 이도 먹통이라 다른 것을 찾아봤다.
https://jxy.me/websocket-debug-tool/
WebSocket Debug Tool
jxy.me
여길 이용해 테스트했다. 탭을 두 개로 띄운 후 실시간 통신이 되는 지 확인했다.

connect type 두 개를 다 체크한 뒤 url을 입력한다. SockJS가 지원되도록 설정했으니 , ws가 아닌 http로 설정한다.

이후 subscription과 send destination을 설정했던 대로 작성한다. /sub/1은 1번 채팅방을 구독한다는 의미이다.
/pub/1/messages는 1번 채팅방을 구독하는 모두에게 전달하겠다는 것이다.

이후 데이터를 ChattingRequest에 맞게 입력한다. 이후 다른 탭의 테스트 창에서 똑같은 과정을 통해 1번 채팅방을 구독하자.

이후 send를 누르자 다른 탭에서 hi가 정상적으로 보이는 것을 볼 수 있다.
이번 기술을 적용해보면서 실시간 양방향 서비스의 기본적인 개념을 공부할 수 있었고, websocket과 stomp에 대해 깊게 이해할 수 있었다.
'Project > 개인 프로젝트' 카테고리의 다른 글
spring boot 사이드 프로젝트 : 테스트 시나리오를 세우고 jmeter를 통해 부하 테스트 해보기 (0) | 2025.02.20 |
---|