Programming/Spring Boot

SpringBoot 웹소켓(WebSocket) 채팅 프로그램 파헤치기

Jan92 2021. 11. 20. 04:19

스프링부트 웹소켓 채팅 프로그램 파헤치기 (WebSocket)

 

 

HTTP 통신과 Socket 통신에 대해서 간단하게,

 

HTTP(HyperText Transfer Protocal) 통신이란, HTML 파일을 전송하는 프로토콜이라는 의미로 초기에는 HTML 파일을 전송하는 것이 목적이었으나, 현재는 JSON, Image 등의 파일들도 전송이 가능합니다.

HTTP 통신은 클라이언트에서 서버로 요청을 보내고, 서버가 요청에 응답하는 방식으로 통신이 이뤄집니다.

즉, 클라이언트의 요청이 있을 때만 서버가 응답하는 '단방향' 통신입니다.

 

반면에 Socket 통신은 두 프로그램이 서로 데이터를 주고 받을 수 있는 양쪽 프로그램 모두에 생성되는 통신 단자를 사용하는 '양방향' 통신입니다.

채팅이나 주식, 게임처럼 실시간으로 데이터를 주고 받는 경우 Connection을 자주 맺고 끊는 HTTP 통신보다 Socket 통신이 더 적합합니다. 결국 Connection의 유지 여부가 웹소켓(WebSocket) 사용을 결정하는 가장 중요한 요인이 됩니다.

(단점으로는 Connection이 유지되는 동안 항상 통신을 하는 것은 아니기 때문에 Connection의 낭비가 발생할 수 있습니다.)

 

 

* WebSocket 이전에는 HTTP polling, HTTP long polling 같은 기술들을 사용했습니다.

 

 

 


 

 

WebSocket을 사용한 간단한 채팅 프로그램의 백엔드 코드 파헤치기

 

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket
	implementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '2.3.2.RELEASE'
}

 

빌드 툴은 gradle을 사용하였고, dependency는 WebSocket을 사용하기 위해 필수인 'spring-boot-starter-websocket' 'spring-boot-starter', 'lombok' 을 추가하여 작업하였습니다.

 

 

프로젝트 구조

 

* 프로젝트의 백엔드 부분 구조이며, 전체 코드는 포스팅 맨 하단에 참고한 사이트의 링크를 첨부해놨습니다.

 

 


 

 

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }
}

 

1. WebSocket Configuration 

 

웹소켓 채팅 프로그램에서 가장 먼저 구성할 것은 WebSocketConfiguration입니다.

 

WebSocketMessageBrokerConfigurer interface

 

'WebSocketMessageBrokerConfigurer interface'를 상속받은 WebSocketConfig class를 생성합니다.

해당 인터페이스는 단순한 메세징 프로토콜(STOMP)로 메시지를 처리하는 방법들을 정의한 interface로 @EnableWebSocketMessageBroker 어노테이션을 클래스에 추가하여 WebSocket을 통한 메세지 브로커 지원 기능을 활성화합니다.

 

 

* STOMP(Simple Text Oriented Messaging Protocal)

-> 데이터 교환을 위한 형식과 규칙을 정의하는 메세징 프로토콜

 

* 메시지 브로커(Message Broker)

-> 송신자(Publisher)의 메시지 프로토콜 형식으로 부터의 메세지를 수신자(Subscriber)의 메세지 프로토콜 형식으로 변환해서 전달하는 중간 프로그램 모듈

 

 

WebSocketConfig 클래스는 registerStompEndpoints(), configureMessageBroker() 두 개의 메서드를 오버라이드 했는데요. 하나씩 살펴보겠습니다.

 

 

 

 

- registerStompEndpoints()

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }

 

StompEndpointRegistry interface의 addEndpoint 메서드를 통해 클라이언트가 웹소켓 서버에 연결하는데 사용할 엔드포인트를 등록합니다. 이렇게 등록된 URL을 통해 Stomp가 연결되고, 메시지에 처리에 따른 url로 요청을 보낼 것인데, 아래에 나올 Controller의 @MessageMapping 어노테이션으로 해당하는 url을 할당해줘 SimpMessagingTemplate를 통해 약속된 경로 또는 유저에게 메시지를 전달해줍니다.

 

 

withSockJS()

* .withSockJS() 메서드는 SockJS fallback 옵션을 활성화하여 WebSocket을 사용할 수 없는 경우 대체 메세징 옵션을 사용할 수 있도록 합니다.

 

 

 

 

- configureMessageBroker

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }

 

메시지 브로커를 구성하는 메서드로 유저가 메세지를 전송하거나 받을 수 있도록 중간에서 URL prefix(접두어)를 인식하여 올바르게 전송(publish), 전달(subscribe)을 중개해주는 중개자(Broker) 역할을 합니다.

 

 

- enableSimpleBroker("/topic")

: 메모리 내 메세지 브로커가 /topic 접두사가 붙은 대상에서 클라이언트로 메시지를 다시 전달할 수 있도록 합니다.

=> "/topic"으로 시작하는 메시지가 메세지 브로커로 라우팅 되어야 한다고 정의

 

 

- setApplicationDestinationPrefixes("/app")

=> "/app"으로 시작하는 메시지가 메세지를 처리하는 메서드(message-handling methods)로 라우팅 되어야 한다고 정의

 

 

 

 

 

* 중요한 내용

 

메서드에 대한 내용을 글로만 파악하면 이해하기 쉽지 않을 수 있는데 아래에 Controller를 본 다음 최종적으로 js 코드와 함께 동작하는 까지 내용을 다 보고 해당 부분을 한번 더 보시면 이해하기 더 좋을 것 같습니다.

 

 

"/app" 으로 시작하는 path로 보내진 메세지가 메세지를 처리하는 메서드, 즉 컨트롤러에서 @MessageMapping("/chat.addUser")

으로 라우팅 됩니다. 

=> "/app/chat.addUser"

 

위 path를 통해 처리된 메시지는 컨트롤러의 @SendTo("/topic/public") 즉, "/topic"이 붙은 메시지 브로커로 보내지고 메세지 브로커는 송신자에게 맞는 프로토콜 형식으로 변환하여 메세지를 전달하게 됩니다.

 

 


 

 

@Getter
@Setter
public class ChatMessage {
    private MessageType type;
    private String content;
    private String sender;
}

 

컨트롤러 전 클라이언트와 서버 간에 교환될 메세지 페이로드 역할을 할 ChatMessage class 입니다. 

* 페이로드(payload) : 헤더와 메타데이터를 제외한 실제 사용에 있어서 필요한 데이터

 

 

 

@Controller
public class ChatController {

    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
        return chatMessage;
    }

    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public ChatMessage addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) {
        headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
        return chatMessage;
    }
}

 

채팅방에 사용자를 추가하는 메서드와 메시지를 전송하는 메서드를 가진 ChatController 입니다.

Spring에서 STOMP 메세징 작업에 접근하는 방식은 @MessageMapping 어노테이션을 사용하여 구현된 엔드포인트에 연결하는 것인데요.

@MessageMapping에 정의된 엔드포인트로 접근된 메시징 요청은 내부 처리 후 @SendTo 어노테이션을 통해 정의된 "/topic/public" 대상의 모든 가입자들에게 전송됩니다.

 

 

이어서는 프론트에서 채팅 기능을 동작시키는 js 코드를 통해 백엔드 동작 원리 및 순서를 파악해보겠습니다.

 

* 전체 코드는 포스팅 맨 하단에 링크되어 있으니 참고하시면 됩니다.

 

 


 

function connect(event) {
    username = document.querySelector('#name').value.trim();

    if(username) {
        usernamePage.classList.add('hidden');
        chatPage.classList.remove('hidden');

        var socket = new SockJS('/ws');
        stompClient = Stomp.over(socket);

        stompClient.connect({}, onConnected, onError);
    }
    event.preventDefault();
}

* js 부분은 지식이 부족하여 백엔드와 연결되는 핵심적인 부분의 흐름만 살펴보겠습니다.

 

이름을 입력하고 채팅방에 입장했을 때 (index.html에 usernameForm form의 submit 이 동작하여 connect 함수를 실행) connect 함수가 실행되며, 이 함수는 SockJS, Stomp를 통해 '/ws' 라는 포인트로 stomp를 연결을 시도하는 듯합니다.

이때 '/ws' 가 WebSocketConfig 클래스의 registerStompEndpoints 메서드에서 등록한 path와 연결됩니다.

 

(stomp 연결이 성공되었을 때 onConnected 함수가 실행되고, 실패 시 onError 함수가 실행됩니다.)

 

 

* event.preventDefault(); 해당 이벤트를 중지시키는 코드

 

 

 

function onConnected() {
    // Subscribe to the Public Topic
    stompClient.subscribe('/topic/public', onMessageReceived);

    // Tell your username to the server
    stompClient.send("/app/chat.addUser",
        {},
        JSON.stringify({sender: username, type: 'JOIN'})
    )

    connectingElement.classList.add('hidden');
}

function sendMessage(event) {
    var messageContent = messageInput.value.trim();
    if(messageContent && stompClient) {
        var chatMessage = {
            sender: username,
            content: messageInput.value,
            type: 'CHAT'
        };
        stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
        messageInput.value = '';
    }
    event.preventDefault();
}

 

onConnected, sendMessage function입니다.

 

onConnected 함수를 먼저 보면, 백엔드에서 봤던 '/topic/public', '/app/chat.addUser', '/app/chat.sendMessage' path 들을 볼 수 있습니다. stompClient.send function을 통해 해당하는 path로 요청을 보낸다는 것인데요.

 

이렇게 보낸진 url을 위 ChatController에서 @MessageMapping 어노테이션으로 잡아서 이어지는 내용을 처리하게 됩니다.

 

 

* 이제 다시 중간쯤에 있는 중요한 내용 부분을 읽어보시면 조금 더 이해하실 수 있을 것 같습니다.

 

 

 

 

 

부족한 부분이 많습니다. 내용 중 틀린 부분이 있으면 댓글로 남겨주세요. 해당되는 부분은 더 공부하고 수정하겠습니다.

 

 

 

 

참고한 자료

 

Building a chat application with Spring Boot and WebSocket

In this tutorial, you'll learn how to use Spring Boot and STOMP over WebSocket with SockJS fall back to build a fully fledged group chat application from scratch.

www.callicoder.com

 

전체 코드 깃

 

GitHub - JianChoi-Kor/socket-chatting: socket

socket. Contribute to JianChoi-Kor/socket-chatting development by creating an account on GitHub.

github.com