* 저는 Spring Boot 2.7.9 버전과 Java 11 버전을 사용하고 있습니다.
함수를 생성하기 위해 코드를 작성할 때 파악한 정보를 바탕으로 작성했기 때문에 잘못된 정보가 포함되어 있을 수 있습니다. 뇌에 기록된 정보는 확인을 피하는 방식으로 작성되었으며, 잘못된 정보가 있으면 피드백 부탁드립니다.
1. 구성
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
config.enableSimpleBroker("/sub");
config.setApplicationDestinationPrefixes("/pub");
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
이 코드는 websocket 기능을 사용하는 데 필요한 설정을 구성합니다.
websocket 기능을 처음 찾았을 때 @EnableWebsocket과 @EnableWebsocketMessageBroker라는 두 가지 방법을 찾았습니다. 사실 두 가지가 독립적으로 사용되는지 몰라서 두 가지 모두 적용해서 개발했습니다. 코드가 사용되지 않는다는 사실을 깨닫고 두 함수가 분리되어 있음을 확인한 후 코드를 정리했습니다.

나중에 Bing AI에게 물어보니 위와 같은 대답을 하더군요.
우리는 Stomp 프로토콜을 통해 통신 기능을 구현했기 때문에 MessageBroker 기능만 작동했습니다.
메시지브로커 좀 더 발전된 기능이라고 하길래 선택하길 잘한 것 같아요^^
그런 다음 @EnableWebsocketMessageBroker 주석을 살펴봐야 합니까?

WebSocketMessageBrokerConfigurer를 상속하고 둘 메서드를 재정의하여 구성하는 예도 있습니다.
여기에서 우리는 HTTP 프로토콜을 사용하여 registerStompEndpoints 메서드로 지정된 끝점에 연결할 때 Stomp 프로토콜을 사용하여 통신을 활성화하기 위해 websocket이 연결된다는 것을 이해합니다.
function connect() {
var socket = new SockJS('/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
Spring Official에서 제공하는 WebSocket 가이드에서 전면 JavaScript 코드를 보면 새로운 SockJS(Spring WebSocket은 SockJS 기능을 지원함)와 적당한 endpoint(외부 서버에 연결하기 위해서는 해당 도메인 주소도 입력해야 함)를 WebSocket으로 저장할 수 있습니다. 변수를 만들고 사용하십시오.
우리의 경우 위의 코드에서 볼 수 있는데 endpoint가 “/ws”로 표시되어 있으므로 “/gs-guide-websocket”을 “/ws” 또는 “http://localhost:8080/ws”로 변경한다. ” 이렇게 하면 websocket 연결이 활성화됩니다.
일단 연결되면 STOMP라는 프로토콜을 사용하여 통신이 이루어진다고 합니다.
재정의된 두 번째 메서드인 configureMessageBroker 를 살펴보겠습니다.
@Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
config.enableSimpleBroker("/sub");
config.setApplicationDestinationPrefixes("/pub");
}
위의 코드를 같이 보면 실용성이 없을 것 같아서 관련 부분만 가져왔습니다.
이 코드에서 이상한 점을 발견하셨나요?
@EnableWebsocketMessageBroker 애노테이션의 설명에는 enableStompBrokerRelay 메소드에 주소를 넣어 사용하라고 되어 있는데 제가 작성한 코드(다른 블로그에 보고된)는 enableSimpleBroker 메소드를 호출합니다.
두 가지가 어느 정도 같은 기능을 하고 있다는 것을 느낄 수 있었지만 정확히 어떤 차이가 있는지 짚어내기 어려웠다.
이 경우 bingAI에게 도움을 요청해야 합니다!

기본 사용에는 SimpleBroker를 사용하고 사용자 지정 기능 및 설정에는 StompBrokerRelay를 사용할 수 있는 것 같습니다.
웹 소켓과 실시간 채팅 기능에 대해서는 입문 수준에서 배우게 될 것이고, 우리가 작업하고 있는 웹 응용 프로그램의 범위가 크지 않기 때문에 SimpleBroker를 그대로 사용하고 기회가 되었으면 합니다. 나중에 StompBrokerRelay에 대해 알아보십시오.
둘의 차이점을 요약하면 공통적으로 어떤 기능을 가지고 있습니까? 만약에,
내가 아는 한 역할은 프런트 엔드 메시지 출력을 위해 “구독”이라는 함수를 작성하는 데 필요한 STOMP 주소를 제공하는 것입니다.
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
나는 또한이 코드를 약간 가져 왔습니다.
위에서 작성한 프런트코드 아래에 주소 “/toipc/greetings”와 익명 함수가 stompClient.subscribe라는 함수에 매개변수로 전달되는 것을 볼 수 있습니다.
프런트 코드의 기능을 관찰하고 평가한 결과,
“/toipc/greetings”라는 주소를 구독하고 그 주소에서 STOMP 메시지를 받아 내용 값을 가져와서 화면에 표시하는 기능처럼 보였습니다.
코드에서 SimpleBroker에 대해 “/sub”라는 경로를 지정했습니다.
“/sub” 경로에 추가된 값에 따라 메시지는 해당 주소의 구독자에게만 배포됩니다.
예) “/sub/room/1” 주소를 구독하면 “sub/room/2″의 대화는 출력되지 않습니다.
이 부분은 설명하는 것보다 이해하려고 노력하는 것이 더 빠를 것 같습니다.
(아니면 제가 설명을 잘 못하는건지. 도와주세요ㅜㅜ)
그리고 주소를 쓰는 config.setApplicationDestinationPrefixes라는 또 다른 메서드가 있습니다.
STOMP 프로토콜을 통해 앞에서 메소드 주소로 본문 값을 보내면 @MessageMapping 주석이 달린 해당 주소에 해당하는 메소드가 백엔드 서버의 컨트롤러에서 실행된다.
function sendName() {
stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}
해당 코드는 위에 링크된 공식 스프링 가이드에서 사용되는 전면 코드의 일부이기도 합니다.
이를 해석하려면 json 형식의 name: 변수를 연결된 웹소켓에 “/app/hello” 주소로 직렬화하고 STOMP 프로토콜로 보내는 것이 중요해 보입니다.
코드에서 우리는 총 4개의 변수를 얻습니다: memberId, memberName, messagem, “/pub” 주소의 chatroomId(DTO가 표시되지 않을 수 있음),
function sendName() {
stompClient.send("/pub/chat/join", {}, JSON.stringify({'memberId' : $("#memberId").val() , 'memberName': $("#memberName").val() , 'message': $("#message").val() , 'chatroomId': $("#chatroomId").val()}));
}
같은 마음으로 쓰겠습니다.
컨트롤러 코드는 아직 보지 못해서 감이 잘 안잡히는 것 같아요. (설명하기 어렵다.)
전반적인 코드 리뷰 이후에 다시 흐름을 설명하겠습니다.
2. 컨트롤러
방을 생성하는 컨트롤러도 있지만 이 글에서는 websocket을 사용하여 채팅 흐름을 처리하고 STOMP 프로토콜을 사용하여 통신하는 적절한 컨트롤러만 언급하겠습니다.
전체 코드가 궁금하시다면 Github 링크를 참고하시면 됩니다.
(Github 코드는 현재 개발 중이므로 내용이 변경될 수 있습니다.)
@Slf4j
@RestController
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
@MessageMapping("/chat/join")
public void enterUser(@Payload ChatRequestDto dto,
SimpMessageHeaderAccessor headerAccessor) {
chatService.enterUser(dto);
headerAccessor.getSessionAttributes().put("MemberName", dto.getMemberName());
headerAccessor.getSessionAttributes().put("roomId", dto.getChatroomId());
log.info("session: {}", headerAccessor.getSessionAttributes());
}
@MessageMapping("/chat/message")
public void sendMessage(@Payload ChatRequestDto dto) {
log.info("message: {}", dto.getMessage());
log.info("chatroomId: {}", dto.getChatroomId());
chatService.sendMessage(dto);
}
@EventListener
public void webSocketDisconnectListener(SessionDisconnectEvent event) {
log.info("Disconnect event: {}", event);
chatService.leaveUser(event);
}
}
여기서 주의해야 할 키워드는 3가지입니다.
1. @메시지매핑
2. SimplMessageHeaderAccessor
3. 세션 연결 해제 이벤트
먼저 @MessageMapping을 살펴보자.
내가 아는 한 HTTP 메서드에는 9가지 종류(GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE)가 있고 여기에는 MESSAGE 메서드가 없었습니다.
그런 다음 주석이 할당되고 호출되면
전면에서 보낸 STOMP 프로토콜 주소 중에는 config.setApplicationDestinationPrefixes(“/pub”); 위의 구성 코드에서 언급했습니다. .NET에서 “/pub”가 설정된 후 경로에 매핑하여 호출됩니다.
예를 들어 STOMP 로그를 /pub/chat/join으로 보내면 @MessageMapping(“/chat/join”) 메서드가 호출됩니다.
서비스 코드가 호출된 후 SimpMessageHeaderAccessor에서 SessionAttribute가 키 값으로 설정된 것을 확인할 수 있습니다.
SimpMessageHeaderAccessor에 대한 정보는 STOMP와 같은 간단한 프로토콜의 헤더와 작업하기 위해 필요한 클래스로 이해됩니다. 이 부분에 대해 더 배울 것이 있습니다.
마지막으로 SessionDisconnectEvent 아직 깊이 이해하지는 못하지만 websocket이 연결 해제될 때 호출되는 이벤트 리스너로 알고 있습니다.
또한 위에서 언급한 설정된 헤더 값은 이벤트를 통해 가져올 수 있습니다.
이를 통해 연결이 끊긴 사용자를 식별하고 이벤트를 처리할 수 있었습니다.
3. 서비스
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class ChatService {
private final ChatroomService chatroomService;
private final SimpMessagingTemplate template;
private final ChatRepository chatRepository;
public void enterUser(ChatRequestDto dto) {
Chatroom chatroom = chatroomService.findVerifiedRoomId(dto.getChatroomId());
if (chatroom.getMembers().size() < 4) {
Integer memberNumber = chatroom
.enterMember(dto.getMemberName())
.getMemberNumber(dto.getMemberName());
dto.setMessage("< " + dto.getMemberName() + " > 님이 입장하셨습니다.");
template.convertAndSend("/sub/chat/room/" + dto.getChatroomId(),
dto.toResponseDto(memberNumber).isEnterType());
} else {
template.convertAndSend("/sub/chat/room/" + dto.getChatroomId(),
dto.toResponseDto(null).isErrorType("방의 정원이 초과했습니다."));
}
}
public void sendMessage(ChatRequestDto dto) {
Chatroom chatroom = chatroomService.findVerifiedRoomId(dto.getChatroomId());
Chat chat = Chat.builder()
.memberId(dto.getMemberId())
.chatroom(chatroom)
.content(dto.getMessage())
.build();
template.convertAndSend("/sub/chat/room/" + dto.getChatroomId(),
dto.toResponseDto(chatroom.getMemberNumber(dto.getMemberName())));
chatRepository.save(chat);
}
public void leaveUser(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String memberName = (String)headerAccessor.getSessionAttributes().get("MemberName");
Long chatroomId = (Long)headerAccessor.getSessionAttributes().get("roomId");
log.info("{}", memberName);
log.info("{}", chatroomId.toString());
Chatroom chatroom = chatroomService.findVerifiedRoomId(chatroomId);
Integer memberNumber = chatroom.getMemberNumber(memberName);
chatroom.leaveMember(memberName);
log.info("headAccessor: {}", headerAccessor);
String message = "< " + memberName + " > 님이 퇴장하셨습니다.";
ChatResponseDto dto = ChatResponseDto.builder()
.memberName(memberName)
.chatroomId(chatroomId)
.message(message)
.memberNumber(memberNumber)
.build()
.isLeaveType();
template.convertAndSend("/sub/chat/room/" + chatroomId, dto);
}
}
코드 내 리포지토리나 로직에 대한 부분과 신중히 고려해야 할 부분은 건너뛰기
1.SimpMessagingTemplate
2. StompHeaderAccessor
보시면 될 것 같습니다.
이 코드에서 SimpMessagingTemplate은 DI를 통해 참조되며 convertAndSend라는 메서드를 호출합니다.
메서드는 주소를 첫 번째 매개변수로, 개체(DTO)를 두 번째 매개변수로 받아 해당 주소로 개체를 직렬화하여 보냅니다.
이 외에도 기능이 다른 메소드가 있으며 자세한 사용법은 공식 문서를 참조할 수 있습니다.
StompHeaderAccessor는 SessionDisconnectEvent의 값으로 Join에 설정된 헤더 값에 액세스하는 데 사용되는 것으로 보입니다.
아직 이 부분에 대한 자세한 내용은 파악하지 못했고, 더 알아봐야 할 것 같습니다.
언급할 중요한 사항을 찾으면 나중에 추가 기사를 작성하겠습니다.
4. 흐름 요약
전체 프로세스를 간략하게 요약하면 다음과 같습니다.
1. registerStompEndpoints 메서드에 설정된 addEndpoint(“/ws”)의 끝점 경로를 사용하여 HTTP 프로토콜을 통한 통신 및 websocket 통신을 구성합니다.
2. enableSimpleBroker(“/sub”)로 설정된 “/sub” 경로 뒤의 특정 주소를 구독합니다.
3. setApplicationDestinationPrefixes(“/pub”) 및 해당 본문 값으로 설정된 “/pub” 경로 뒤에 특정 주소를 전달하여 컨트롤러 메서드를 호출합니다.
4. 해당 메소드가 작성되는 로직을 동작시킨 후, SimpMessagingTemplate 변환 및 전송 dto는 메소드에 의해 특정 주소로 전송됩니다.
5. 특정 주소를 구독하는 클라이언트만 해당 dto를 가져오고 작성된 논리에 의해 화면에 표시됩니다.
어느 정도 이해합니다.