Spring/스프링 기본

Spring Web Socket 적용하기

공대키메라 2022. 2. 4. 14:14

웹 소켓이 무엇인가? 

 

docs.spring.io에서 Spring web socket을 어떻게 사용하는지 찾고 있는데 이에 대한 설명도 함께 수록되어 있었다. 

 

출처 : https://docs.spring.io/spring-framework/docs/5.0.4.RELEASE/spring-framework-reference/web.html#websocket

 

소개에는 WebSocket의 응답 및 요청 protocol에 대해서 설명해준다. 

 

간략하게 요청 형식을 보자면 다음과 같다. 

A WebSocket interaction begins with an HTTP request that uses the HTTP "Upgrade" header to upgrade, or in this case to switch, to the WebSocket protocol:

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13 Origin: http://localhost:8080

 

응답은 다음과 같다. 

Instead of the usual 200 status code, a server with WebSocket support returns:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

 

위에 대해 더 자세하게 알고 싶다면 RFC 6455 를 읽어보라고도 하는데 너무 길어서 패스하고 우선 위의 주소에서만 소개하는걸 자세히 읽었다. 

 

HTTP vs WebSocket

이제부터 서로의 차이점을 들면서 설명을 진행한다. 

 

Even though WebSocket is designed to be HTTP compatible and starts with an HTTP request,
it is important to understand that the two protocols lead to very different architectures and application programming models.

In HTTP and REST, an application is modeled as many URLs. To interact with the application clients access those URLs, request-response style. Servers route requests to the appropriate handler based on the HTTP URL, method, and headers.

By contrast in WebSockets there is usually just one URL for the initial connect and subsequently all application messages flow on that same TCP connection. This points to an entirely different asynchronous, event-driven, messaging architecture.

WebSocket is also a low-level transport protocol which unlike HTTP does not prescribe any semantics to the content of messages. That means there is no way to route or process a message unless client and server agree on message semantics.

WebSocket clients and servers can negotiate the use of a higher-level, messaging protocol (e.g. STOMP), via the "Sec-WebSocket-Protocol" header on the HTTP handshake request, or in the absence of that they need to come up with their own conventions.

 

web socket이 http랑 호환되도록 디자인됐고 http 요청으로 시작해도, 두개의 프로토콜이 매우 다른 구조와 애플리케이션 프로래밍 모델에 쓰인다는걸 이해하는 것은 중요합니다. 

HTTP와 REST 에서, 어플리케이션은 많은 URL들로 구성됩니다. 애플리케이션 클라이언트와 상호작용하기 위해 그URL들(요청-응답 스타일)에 접근합니다. 서버 route는 HTTP URL, 메서드, 헤더에 근거해 적절한 핸들러를 요청합니다. 

이와 대조적으로 웹 소켓에는 초기 연결을 위해 하나의 URL만 있고, 연속적으로 모든 애플리케이션 메시지들이 같은 TCP 연결로 흐릅니다. 이것은 전체적으로 다른 비동기, 이벤트 주도, 메시징 아키텍처를 가르킨다. 

웹 소켓은 또한 HTTP와 달리 메시지의 내용에 어떤 시맨틱들을 규정하지 않는 저레벨 전송 프로토콜입니다.
That means there is no way to route or process a message unless client and server agree on message semantics.
(시멘틱을 어떻게 해석해야 할 지... ㅠㅠ- 결국 양방향 연결된것을 말고는 HTTP와 달리 접근이 불가한다는거 같음)

웹 소켓 클라이언드들과 서버들은 HTTP handshake 요청, 또는 그들 자신의 조약을 제안하길 필요로 하는 것의 부재 Sec-WebSocket-Protocol 헤더를 통해서 고레벨, 메시징 프로토콜의 사용을 협상할 수 있다. 

 

어우 무슨말인지 잘 모르겟다... 다른 정리된 내용을 찾아보았다.

 

출처 : https://choseongho93.tistory.com/266

Transport protocol의 일종으로 서버와 클라이언트 간의 효율적인 양방향 통신을 실현하기 위한 구조입니다.
웹소켓은 단순한 API로 구성되어있으며, 웹소켓을 이용하면 하나의 HTTP 접속으로 양방향 메시지를 자유롭게 주고받을 수 있습니다.
 
위 배경에서 웹소켓이 나오기 이전에는 모두 클라이언트의 요청이 없다면, 서버로부터 응답을 받을 수 없는 구조였습니다.
웹소켓은 이러한 문제를 해결하는 새로운 약속이었습니다.
웹소켓에서는 서버와 브라우저 사이에 양방향 소통이 가능합니다. 브라우저는 서버가 직접 보내는 데이터를 받아들일 수 있고, 사용자가 다른 웹사이트로 이동하지 않아도 최신 데이터가 적용된 웹을 볼 수 있게 해줍니다. 웹페이지를 ‘새로고침’하거나 다른 주소로 이동할 때 덧붙인 부가 정보를 통해서만 새로운 데이터를 제공하는 웹서비스 환경의 빗장을 본질적으로 풀어준 셈입니다.
 
웹에서도 채팅이나 게임, 실시간 주식차트와 같은 실시간이 요구되는 응용프로그램의 개발을 한층 효과적으로 구현할 수 있게 됩니다.
가상화폐의 분산화 기술의 핵심도 WebSocket으로 구현할 수 있습니다.

 

물론 이에 따른 단점도 있으니 참조한 사이트를 보길 강력 추천한다. 

 

그래서 Spring Web Socket을 이용해서 예제를 한번 구현해보았다. 

 

출처 : https://spring.io/guides/gs/messaging-stomp-websocket/

 

그전에 알고 넘어가면 좋은 정보를 찾아보았다.

 

SocketJs

WebSocket과 유사한 객체를 제공하는 브라우저 JavaScript라이브러리

  • socketJS를 이용하면 websocket을 지원하지 않는 브라우저 까지 커버가능
  • 서버개발시 스프링 설정에서 일반 webSocket으로 통신할지 SockJS호환으로 통신할지 결정 가

 

Stomp

STOMP는 HTTP에서 모델링 되는 Frame 기반 프로토콜

  • 메세징 전송을 효율적으로 하기 위해 탄생한 프로토콜(websocket위에서 동작)
  • 클라이언트와 서버가 전송할 메시지의 유형, 형식, 내용들을 정의하는 메커니즘
  • 텍스트 기반 프로토콜으로 subscriber, sender, broker를 따로 두어 처리를 한다.
  • stomp를 이용하면 채팅방을 여러개로 개설이 가능하다.
  1. 채팅방 생성: pub/sub 구현을 위한 Topic 생성
  2. 채팅방 입장: Topic 구독
  3. 채팅방에서 메세지를 송수신: 해당 Topic으로 메세지를 송신(pub), 메세지를 수신(sub)

 

출처: https://velog.io/@syi9595/Socket.js-%EC%99%80-Stomp%EB%9E%80

 

1. Spring Web Socket  예제 구현하기

의존성 추가

plugins {
	id 'org.springframework.boot' version '2.6.3'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	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'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

 

클래스 구현

 

1. HelloMessage.java

public class HelloMessage {

  private String name;

  public HelloMessage() {
  }

  public HelloMessage(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

 

2. Greeting.java

public class Greeting {

  private String content;

  public Greeting() {
  }

  public Greeting(String content) {
    this.content = content;
  }

  public String getContent() {
    return content;
  }

}

 

3. GreetingController.java

@Controller
public class GreetingController {


// The @MessageMapping annotation ensures that, 
// if a message is sent to the /hello destination, the greeting() method is called.
// After the one-second delay, the greeting() method creates a Greeting object and returns it. The return value is broadcast to 
// all subscribers of /topic/greetings, as specified in the @SendTo
  @MessageMapping("/hello")
  @SendTo("/topic/greetings")
  public Greeting greeting(HelloMessage message) throws Exception {
    Thread.sleep(1000); // simulated delay
    return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
  }

}

 

4. WebSocketConfig.java - web socket 기능을 Spring configuration 클래스에 등록

...
@Configuration
//@EnableWebSocketMessageBroker enables WebSocket message handling, backed by a message broker.
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/gs-guide-websocket").withSockJS();
  }

}

 

5. 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>

 

6. javascript 파일 작성 - 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('/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);
        });
    });
}

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(); });
});

 

코드를 보면 생각보다 간단하다. 

 

new SocketJS에 입력한 '/gs-guide-websocket'는 addEndPoint로 registerStompEndPoint에서 등록한 것이다. 

 

send 버튼을 클릭하는 순간 sendName함수가 실행되고 

 

sendName 함수에서는 "/app/hello"로 설정했는데 /app은 prefix로 설정한 것이고, /hello는 @MessageMapping에 입력한 값이다. 

 

그러면 등록된 url로 알맞게 등록된 annotation을 찾아서 해당 메소드를 실행하고 반환값을 받아오면 된다. 

 

 

서버를 띄워보면 다음과 같은 모습이 나온다. 

 

 

여기서 이름을 입력하고 connect를 누르게 되면... Hello 하는 것과 함께 내가 입력한 이름이 나오게 된다. 

 

 

근데 정말 충격적인 것은 이것이 spring docs에서 소개해주는 내용의 전부라는 사실이다. 당황 ㅋㅋ;;;

 

사실 구현한것도 아니고 그냥 그대로 따라서 쳐서 한것이라서 뭐... 더잇을 줄 알앗는데 너무 간단해서 이정도로는 어떤 기능이 있는지도 모르겠다!

 

그래서 구글링을 통해 어떻게 더 활용을 했는지 찾아보았다.

 

사실 이번 장을 1편으로 하고 2편을 새로 만들어서 어떻게 다양하게 구성하는지 찾아보려고 했는데 

 

이게 그렇기에는 이미 다른 블로그에서 정리도 잘 되어있다보니 이것을 내가 하는게 아니라 복붙만 해서 이해하는 방식이라서 참고하면 좋은 사이트를 정리하려고 한다. 

 

채팅 구현 출처 사이트 : https://daddyprogrammer.org/post/4691/spring-websocket-chatting-server-stomp-server/

처음부터 차근차근 Map으로 채팅창을 구현해서 NoSQL도 사용하고 점점 고도화 하면서 설명해주는것이 아주 학습하기 좋다. 강력 추천!

 

 

'Spring > 스프링 기본' 카테고리의 다른 글

Filters vs HandlerInterceptors - 개념  (0) 2022.02.28
Glory of Rest 란 뭘까? + HATEOS 적용기  (0) 2022.02.16
Spring VS Spring boot  (3) 2022.02.03
@Json~~~ 관련 annotation 정리  (0) 2022.02.03
Bean이란?  (2) 2022.01.28