[spring boot] websocket을 이용해 chatting 구현하기(1)

유승한·2024년 2월 11일
0

chatting

목록 보기
1/1
post-thumbnail

개요

현재 진행중인 프로젝트에서 websocket을 이용한 채팅 시스템을 개발해야하는데 해당 부분에 대해서 공부하며 정리해보고자한다.

해당 유튜브 영상을 많이 참고했다.
https://www.youtube.com/watch?v=rvss-_t6gzg

websocket

  • 웹소켓은 실시간성을 보장하는 서비스에 사용
  • http에서도 polling, log plling, streaming을 통해 가능

HTTP vs websocket

HTTP

  • 비연결성
  • 매번 연결 맺고 끊는 과정의 비용
  • (요청-응답) 구조
  • 보내야하는 데이터 양이 많음(헤더)

웹소켓

  • 연결지향
  • 한번 연결 맺은 뒤 유지
  • 양방향 통신
  • 보내야하는 데이터의 양의 적음(처음 핸드쉐이크 제외)

STOMP

  • simple text oriented messaging protocol
  • 메시지 브로커를 활용하여 쉽게 메시지를 주고 받을 수 있는 프로토콜
    - pub - sub(발행-구독) : 발신자가 메시지를 발행하면 수신자가 그것을 수신하는 메시징 패러다임
    - 메시지 브로커 : 발신자의 메시지를 받아와서 수신자들에게 메시지를 전달하는 어떤 것
  • 웹소켓 위에 얹어 함께 사용할 수 있는 하위(서브) 프로토콜
    즉, 클라이언트가 서버로 메시지를 보내는 것은 pub, 클라이언트가 서버로부터 메시즈를 받는 것을 메시지를 구독한다는 개념으로 sub이 사용된다.

STOMP 프레임 구조(위 유튜브 영상에서 발췌)

웹소켓은 페이로드(메시지) 길이, 오퍼레이션 코드(프레임 유형), 마스킹 키, 페이로드 데이터 등의 기본 프레임을 사용하여 데이터를 교환하지만 메시지 형식이나 데이터 구조에 대한 규정은 제공하지 않는다.

반면에 STOMP를 이용 시 데이터가 이해하기 쉽게 구조화 되어 나온다는 장점이 있다.

STOMP 통신 흐름

발신자는 "/app"을 통해 메시지를 송신 시에 서버에서 메시지를 가공/처리를 한 뒤 브로커를 통해 "/topic"을 구독하고 있는 구독자에게 전달된다.
"/topic"을 통해 메시지를 송신 시에는 메시지를 가공/처리를 하지 않고 "/topic"을 구독하는 구독자에게 전달된다.

STOMP를 이용한 방명록 서비스 만들기

위 개념을 통해 스프링으로 간단한 방명록 서비스를 실습하였다.
해당 실습은 아래의 블로그를 발췌하였다.
https://growth-coder.tistory.com/157

<build.gradle>

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.10'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.websocket'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '11'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-websocket'
	implementation 'org.webjars:webjars-locator-core'
	implementation 'org.webjars:sockjs-client:1.0.2'
	implementation 'org.webjars:stomp-websocket:2.3.3'
	implementation 'org.webjars:bootstrap:3.3.7'
	implementation 'org.webjars:jquery:3.1.1-1'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

html, js 파일은 서버의 동작을 확인하기 위함으로 src/main/resources/static 파일 아래에 코드를 입력하였다.
<index.html>

<!DOCTYPE html>
<html>
<head>
    <title>Hello WebSocket</title>
    <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <link href="/main.css" rel="stylesheet">
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script src="/app.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="connect">WebSocket connection:</label>
                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                    </button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="name">What is your name?</label>
                    <input type="text" id="name" class="form-control" placeholder="Your name here...">
                </div>
                <button id="send" class="btn btn-default" type="submit">Send</button>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>Greetings</th>
                </tr>
                </thead>
                <tbody id="greetings">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

<app.js>

var stompClient = null;

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
}

function connect() {
    var socket = new SockJS('/ws');
    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);
        });
    });
}

function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}

function showGreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}

$(function () {
    $("form").on('submit', function (e) {
        e.preventDefault();
    });
    $( "#connect" ).click(function() { connect(); });
    $( "#disconnect" ).click(function() { disconnect(); });
    $( "#send" ).click(function() { sendName(); });
});

<HelloMessage.java>

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class HelloMessage {
    private String name;
}

클라이언트로부터 받은 메시지를 나타내는 모델 클래스이다.
name은 사용자의 이름을 저장하는데 사용된다.

<Greeting.java>

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Greeting {
    private String content;
}

메시지의 내용을 저장하는 모델이다.
메시지의 내용(content)를 저장하는 역할을 하며 클라이언트와 서버 간에 데이터 교환 시에 사용된다.

<GreetingController.java>

@Controller
public class GreetController {
    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws  Exception{
        Thread.sleep(1000);
        return new Greeting("Hello, "+
                HtmlUtils.htmlEscape(message.getName())+"!");
        //HtmlUtils.htmlEscape(String input)
    }
}

메시지를 받고 처리하는 컨트롤러이다.
@MessageMapping("/hello")는 클라이언트가 '/app/hello' 경로로 메시지를 보낼 때 해당 메소드(greeting)를 호출하도록 설정하였다.
이 경로는 'WebSocketConfig'에서 설정한 애플리케이션 destination prefix '/app'과 결합되어 사용된다.

@SendTo("/topic/greetings")는 'greeting' 메소드가 반환하는 객체('Greeting')를 '/topic/greetings' 경로를 구독하는 모든 클라이언트에게 전송하도록 지정한다.

이를 통해 STOMP 통신 흐름 처리에서 설명한 서버에서의 가공/처리 이후의 구독자에게 메시지를 송신이 가능하다.

<WebSocketConfig.java>

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config){
        //발행자가 토픽 경로로 메시지를 주면 구독자에게 전달
        config.enableSimpleBroker("/topic");
        //발행자가 앱 경로로 메시지를 주면 가공을 해서 구독자에게 전달
        config.setApplicationDestinationPrefixes("/app");
    }
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry){
        //커넥션을 맺는 경로 설정
        //만약 websocket을 사용할 수 없는 브라우저라면 다른 방식 사용하도록 설정
        registry.addEndpoint("/ws").withSockJS();
    }
}

'WebSocketConfig' 클래스는 WebSocket과 STOMP 메시징을 위한 설정을 담당한다. @Configuration과 @EnableWebSocketMessageBrocker 애노테이션을 통해 스프링의 웹소켓 메시지 브로커를 활성화한다.

'configureMessageBroker'메소드에서는 메시지 브로커가 사용할 경로를 설정한다. '/topic'으로 시작하는 경로는 메시지 브로커가 처리하여 구독자에게 메시지를 전달하며 '/app'으로 시작하는 경로는 애플리케이션 내에서 처리된 후 특정 브로커에게 전달된다.

'registerStompEndpoints' 메소드에서는 STOMP 프로토콜을 사용할 수 있는 WebSocket 엔드포인트를 등록한다. 'withSockJS()'를 사용하여 WebSocket을 지원하지 않는 브라우저에서도 대체 옵션을 사용할 수 있도록 한다.

실행화면

0개의 댓글

관련 채용 정보