Q. WebSocket이 아닌 Stomp를 사용하는 이유는❓
A. WebSocket은 Text 또는 Binary 두 가지 유형의 메세지 타입은 정의하지만 메세지의 내용에 대해서는 정의하지 않습니다. 즉, WebSocket만 이용하면 해당 메세지가 어떤 요청인지? 어떤 포맷으로 오는지? 메세지 통신 과정을 어떻게 처리해야 하는지? 정해져 있지 않아서 일일이 구현해야 합니다. 따라서, Stomp라는 프로토콜을 서브 프로토콜로 사용합니다.
Stomp는 클라이언트와 서버가 서로 통신하는 데 있어 메세지의 형식, 유형, 내용 등을 정의해주는 프로토콜로 단순한 binary, text가 아닌 규격을 갖춘 메세지 입니다.
: simple/Stream Text Oriented Message Protocol의 약자로, 메세지 브로커의 역할을 한다.
COMMAND
header1:value1
header2:value2
철수, 민희, 유리, 수호 라는 사용자가 2번 방에 입장합니다.
철수가 2번 방에서 “안녕? 내가 방을 만들었음” 이라고 채팅을 전송합니다.
2번 방 메세지 브로커(중재자)가 메세지를 받습니다.
2번 방 메세지 브로커가 2번 방 구독자들(철수, 민희, 유리, 수호)에게 메세지를 전송합니다.
⇒ 또, 다른 사용자가 방에 메세지를 보내도 메세지 브로커가 인식하고 구독하는 다른 사용자들에게 해당 메세지를 모두 전송합니다.
사용자들은 채팅방에 입장하면서 동시에 2번 채팅방에 대해 구독을 하게 되고, 메세지 브로커는 클라이언트의 구독 정보를 자체적으로 메모리에 유지합니다.
SUBSCRIBE
destination:/subscribe/chat/room/2
아래와 같이 어떤 유저가 메세지를 보내면, 메세지 브로커는 SUBSCRIBE 중인 다른 유저들에게 메세지를 전달합니다.
SEND
content-type:application/json
destination:/publish/chat
{"chatRoomId":2, "type": "MESSAGE", "writer":"Cheolsoo"}
package com.yehah.draw.global.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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;
@Slf4j
@EnableWebSocketMessageBroker
@Configuration
public class StompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry){
registry.addEndpoint("/ws/draws/comm-similarity")
.setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry){
registry.setApplicationDestinationPrefixes("/pub");
registry.enableSimpleBroker("/sub");
}
}
⇒ http://domain이름/ws/draws/comm-similarity 으로 요청을 보내서 http통신에서 ws통신으로 변경한다.package com.yehah.draw.global.stomp;
import com.yehah.draw.global.common.AnimalType;
import com.yehah.draw.global.stomp.dto.MessageResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Slf4j
@RequiredArgsConstructor
@Controller
public class StompController {
private final SimpMessagingTemplate messagingTemplate;
// roomId는 FE에서 랜덤으로 생성해서 알려주기
@MessageMapping("/first-enter/{animalType}/{roomId}")
public void firstEnter(@DestinationVariable("animalType") AnimalType animalType, @DestinationVariable("roomId") String roomId){
log.info("{} 방의 연결을 시작합니다", roomId);
messagingTemplate.convertAndSend("/sub/room/"+roomId,
MessageResponse.builder()
.roomId(roomId)
.animalType(animalType)
.responseState(ResponseState.SUCCESS)
.message(roomId + "방의 연결을 시작합니다.").build());
}
@MessageMapping("/similarCheck/{animalType}/{roomId}")
public void similarcheck(@DestinationVariable("animalType")AnimalType animalType, @DestinationVariable("roomId") String roomId){
log.info("{} 방의 유사도 검사를 시작합니다.", roomId);
messagingTemplate.convertAndSend("/sub/room/"+roomId,
MessageResponse.builder()
.roomId(roomId)
.animalType(animalType)
.responseState(ResponseState.SUCCESS)
.message(roomId + "방의 유사도 검사를 시작합니다.").build());
}
}
@Operation(summary = "친구의 유사도를 확인한다.", description = "ALL")
@PostMapping("/similarcheck")
public ResponseEntity<Void> animalSimilarCheck(@ModelAttribute AnimalSimilarReqDto animalSimilarReqDto) throws IOException {
similarCheckProcessor.similarCheck(animalSimilarReqDto.getRoomId(), animalSimilarReqDto.getOriginalFile(), animalSimilarReqDto.getNewFile()
,animalSimilarReqDto.getComparisonValue(), AnimalType.animal);
return ResponseEntity.ok().build();
}
package com.yehah.draw.global.Processor;
import com.yehah.draw.domain.animal.entity.SimilarState;
import com.yehah.draw.global.common.AnimalType;
import com.yehah.draw.global.stomp.ResponseState;
import com.yehah.draw.global.stomp.StompService;
import com.yehah.draw.global.stomp.dto.MessageResponse;
import com.yehah.draw.global.stomp.dto.SimilarMessageResponse;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class SimilarCheckProcessor {
@Value("${micro.path.similarityCheck}")
private String similarityPath;
private final CommunicationProcessor communicationProcessor;
private final StompService stompService;
public void similarCheck(String roomId, MultipartFile originalFile, MultipartFile newFile
,double comparisonValue, AnimalType animalType){
MultiValueMap<String, Object> bodyData = new LinkedMultiValueMap<>();
bodyData.add("roomId", roomId);
bodyData.add("originalFile", originalFile.getResource());
bodyData.add("newFile", newFile.getResource());
String stompUrl = "/sub/room/"+roomId;
try{
double value = Double.parseDouble(String.valueOf(communicationProcessor.postMultipartMethod(bodyData, similarityPath+"/similarcheck", String.class)));
// NOTE : STOMP 연결하기
SimilarMessageResponse similarMessageResponse = SimilarMessageResponse.builder()
.roomId(roomId)
.animalType(animalType)
.similarValue(value)
.responseState(ResponseState.SUCCESS)
.message("유사도 연결에 성공하셨습니다.")
.build();
if(value >= comparisonValue){
// NOTE : STOMP 응답 전송하기
stompService.stompSuccessRes(stompUrl, SimilarState.END, similarMessageResponse);
}else{
// NOTE : STOMP 응답 전송하기
stompService.stompSuccessRes(stompUrl, SimilarState.CONTINUE, similarMessageResponse);
}
}catch(Exception e){
e.printStackTrace();
// NOTE : STOMP 응답 전송하기
stompService.stompFailRes(stompUrl, MessageResponse.builder()
.roomId(roomId)
.animalType(animalType)
.message("유사도 측정에 실패했습니다.").responseState(ResponseState.FAIL).build());
}
}
}
⇒ flask 서버를 호출해서 받은 유사도를 StompService라는 이름의 자바 파일에서 ws으로 응답한다.package com.yehah.draw.global.stomp;
import com.yehah.draw.domain.animal.entity.SimilarState;
import com.yehah.draw.global.stomp.dto.MessageResponse;
import com.yehah.draw.global.stomp.dto.SimilarMessageResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class StompService {
private final SimpMessagingTemplate messagingTemplate;
// NOTE : STOMP 성공 응답 전송하기
public void stompSuccessRes(String responseUrl, SimilarState similarState, SimilarMessageResponse similarMessageResponse){
log.info("similarState : {}", similarState);
similarMessageResponse.setSimilarState(similarState);
messagingTemplate.convertAndSend(responseUrl, similarMessageResponse);
}
// NOTE : STOMP 실패 응답 전송하기
public void stompFailRes(String responseUrl, MessageResponse messageResponse){
messagingTemplate.convertAndSend(responseUrl, messageResponse);
}
}
⇒ Stomp 유사도 검사 결과를 client 에게 반환한다.💡 플랫폼의 목표는 저희가 제공하는 원본 그림의 테두리만 있는 그림과 아이가 해당 그림에 그린 테두리의 유사도를 실시간으로 체크하여 일정 유사도를 넘기면 다음 색칠하기 페이지로 자동으로 넘어갈 수 있도록 개발하는 것이었습니다. 이때, 메세지를 응답하여 소통하는 창구는 webSocket 기반의 메세지 프로토콜인 stomp 를 사용했고 실제로 아이가 그린 그림을 보내는 창구로는 http 통신을 사용했습니다. 이렇게 구현한 이유는 http 통신으로 불필요한 데이터를 전송하거나 받지 않고 최대한의 실시간성을 보장하기 위함이었습니다.
: webSocket을 지원하지 않는 최신 브라우저에서도 해당 라이브러리의 api가 잘 작동하도록 지원하는 라이브러리이다.
⇒ 실시간으로 보이기 위해서는 잦은 요청이 필요하다.
1. client가 http 요청을 보낸다.
2. server에서 특정 이벤트나 타임아웃이 발생하면 client에게 응답을 보낸다.
3. client측에서 응답을 받으면 바로 http request를 보낸다.