spring이 제공하는 websocket에는 stomp라는 프레임워크가 또 있다.
STOMP : Simple Text Oriented Messaging Protocol
stomp는 메세지 전송을 효율적으로 하기 위한 프로토콜로, 기본적으로 PUB/SUB 구조로 되어있다. 따라서 메세지를 전송하고 / 받아서 처리하는 부분이 확실하게 구조로 정해져있기 때문에 명확하게 인지하고 개발할 수 있다.
- STOMP 프로토콜은 클라이언트/서버 간 전송할 메세지의 유형, 형식, 내용들을 정의한 규칙이다.
- TCP 또는 WebSocket과 같은 "양방향 네트워크 프로토콜 기반"으로 동작한다.
- 헤더에 값을 세팅할 수 있어서 헤더 값을 기반으로 통신 시 인증처리를 구현할 수 있다.
pub/sub이란 메세지를 공급하는 주체와 소비하는 주체를 분리해 제공하는 메세징 방법이다. 대표적인 예를 들어 설명을 해보겠다.
우체통(Topic)이 있다면 집배원(Publisher)이 신문을 우체통에 배달하는 행위가 있고, 우체통에 신문이 배달되는 것을 기다렸다가 빼서 보는 구독자(Subscriber)의 행위가 있다. 이때 구독자는 다수가 될 수 있다.
즉, 채팅방을 생성하는 것은 우체통 Topic을 만드는 것이고 채팅방에 들어가는 것은 구독자로서 Subscriber가 되는 것이다. 채팅방에 글을 써서 보내는 행위는 우체통에 신문을 넣는 Publisher가 된다.
이때 Message Brocker란 개념이 있는데, 이것은 Publisher로부터 전달받은 메세지를 Subsciber에게 메세지를 주고 받게 해주는 중간 역활을 하는 것을 말한다.
클라이언트는 SEND, SUBSCRIBE 명령을 통해서 메세지의 내용과 수신 대상을 설명하는 "destination" 헤더와 함께 메세지에 대한 전송이나 구독을 할 수 있다. 이것이 브로커를 통해 연결된 다른 클라이언트로 메세지를 보내거나, 서버로 메세지로 보내 일부 작업을 요청할 수 있는 PUB/SUB 메커니즘을 가능하게 한다.
스프링이 지원하는 STOMP에서는 스프링 웹 소켓 애플리케이션이 클라이언트에게 STOMP 브로커의 역활을 한다. 이때 메세지는 @Controller 메세지 처리방법이나, Subscriber를 추적해서 구독중인 사용자에게 메세지를 전파(Broadcast)하는 Simple In Memory 브로커에게 라우팅 된다.
이렇게 spring 환경에서 추가적인 설정없이 STOMP 프로토콜을 사용하면 메세지 브로커는 자동으로 In Memory Broker를 사용하게 된다.
stomp의 프레임 구조
일단 제가 우리 프로젝트에서 제가 만든 실시간 초대 알림에 대해서 써보겠다.
먼저, websocket의 sockjs와 stomp를 사용하려면 spring-boot gradle 기준으로 dependencies를 작성해야한다.
그리고 session을 이용한 handshake를 구현해야되는 websocket과 달리 stomp는 broker를 통해서 handshake를 알아서 해주기 때문에 정말로 코드작성이 간단하다 .. 물론 알아서 다 해주기 때문에 처음 접하는 사람이라면 그만큼 이해하기가 힘들다...
stomp의 broker를 구현을 해보자면 이렇다.
우선 EnableWebSocketMessageBroker annotation을 통해 broker를 사용한다는 주석처리를 해주고 WebSocketMessageBrokerConfigurer을 상속받아야한다.
그럼 이 interface가 가지고 있는 메소드 2개만 사용하면 설정은 끝이난다. 우선 연결하기 위해서는 프론트가 stomp가 제공하는 시작점(endpoint)를 연결해야하기 때문에 endpoint를 설정하는 메소드가 필요하다.
--> .addEndpoint("/알아서정하면됨")
그리고 그 밑에 있는 .setHandshakeHandler는 프론트에서 websocket 연결이 있을 때마다 서버 로그에 찍어주는 역할을 한다. 물론 new UserHandShakeHandler()는 알아서 기능에 맞게 만들어줘야 한다.
그리고 그 밑에 있는 .setAllowedOriginPatterns("*")는 프론트 주소나 로컬 주소가 들어가면 된다. 물론 아시겠지만 별 표시는 모두 포함이라는 뜻이다. 노파심이지만 똑같은 의미의 코드로 .setAllowedOrigins("")도 있다. 잘 골라서 쓰면 된다.
그리고 그 밑에 있는 .withSockjs는 좀 허무하겠지만 Sockjs를 사용하고 싶으면 이 한줄이면 끝이난다.
자!! 여기까지가 Endpoint 메소드의 대한 내용이였다.
그럼 이제부터 그 밑에있는 집배원과 우체통과 구독자에 대해서 설명하겠다.
일단 코드에 pub이라고 쓰여있는 곳이 집배원의 url이고 sub라고 쓰여있는 곳이 우체통의 url가 된다. 이것을 이해하려면 우선 그림이 필요하겠다.
일단 어딜가나 이런 그림이 있다. 이 그림은 spring 공식 홈페이지에 있는 그림이다. 물론 나는 이 그림을 보면서 더 머리가 어지러웠다. 내가 이해한 그림은 내가 그려보겠다.
내 그림도 많이 이상할 수도 있지만 이해하기를 바란다. 그만큼 이해하면 쉬울 수 있지만 이해하기까지가 어려운건 다 똑같은거 같다. 이렇게 집배원과 우체통을 정해놨으면 이제 MassageMapping을 하면 된다.
@RequiredArgsConstructor
@Controller
public class AlarmController {
private final ProjectMemberRepository projectMemberRepository;
private final MemberRepository memberRepository;
private final SimpMessageSendingOperations messagingTemplates;
@MessageMapping("/invite")
public InviteResponseDto inviteProject(@Payload InviteRequestDto requestDto) {
InviteResponseDto inviteResponseDto = new InviteResponseDto();
// 초대하려는 이메일이 존재하지 않을 때 !!
System.out.println(requestDto.getReceiverEmail());
if (!memberRepository.existsByEmail(requestDto.getReceiverEmail())) {
inviteResponseDto.setMsg("초대하려는 이메일이 존재하지 않습니다.");
System.out.println(memberRepository.existsByEmail(requestDto.getReceiverEmail()));
return inviteResponseDto;
}
if (Objects.equals(requestDto.getSenderEmail(), requestDto.getReceiverEmail())) {
inviteResponseDto.setMsg("자기자신은 초대할 수 없습니다.");
return inviteResponseDto;
}
// 초대하려는 이메일이 존재할 때 !!
Member receiver = memberRepository.findByEmail(requestDto.getReceiverEmail());
if (projectMemberRepository.existsByMember_IdAndProject_Id(receiver.getId(), requestDto.getProjectId())) {
inviteResponseDto.setMsg("이미 초대되어 있는 회원입니다.");
return inviteResponseDto;
}
// 프론트에게 전해줄 보내는 자의 Id와 보내고 싶은 사용자 Id를 담아서 전해준다.
inviteResponseDto.setMsg("초대완료");
inviteResponseDto.setSenderEmail(requestDto.getSenderEmail());
inviteResponseDto.setReceiverEmail(requestDto.getReceiverEmail());
System.out.println(receiver.getId());
messagingTemplates.convertAndSend("/sub/invite/" + receiver.getId(), inviteResponseDto);
return inviteResponseDto;
}
}
처음 @MassageMapping 할 때 쓰는 url은 내가 정해놓은 /pub이 숨겨져 있다고보면 된다. 따라서 "/invite"만 쓴 것이다.
그리고 마지막 SimpMessageSendingOperations에 들어있는 convertAndSend로 내가 보내고 싶은 우체통과 보내는 메세지를 적어주면 끝이난다. 이것을 응용하면 된다. 그리고 당연히 프론트는 이 우체통을 구독해야만 메세지를 받을 수 있다.