RATSENO

[Spring]Springboot + websocket 채팅[1] 본문

DEV/SPRING

[Spring]Springboot + websocket 채팅[1]

RATSENO 2020. 2. 18. 11:12

인터넷 사이트에 접속하게 되면 자주 볼 수 있는 것이 접속한 유저들끼리 채팅을 할 수 있는 채팅창입니다.

이러한 채팅 기능들을 websocket을 통하여 이루어 집니다. 이번 포스팅에서는 websocket에 대해서 알아보고

간단한 채팅 application을 구현해보겠습니다.


WebSocket

Web Browser에서 Request를 보내면 Server는 Response를 줍니다.

HTTP 통신의 기본적인 동작 방식입니다.

하지만 Server에서 Client로 특정 동작을 알려야 하는 상황도 있다.

예를 들어 Browser로 Facebook에 접속해 있다가 누군가 친구가 글을 등록하는 경우, 혹은 Web Browser로 메신저를 구현하는 경우가 있습니다.

WebSocket이란 Transport protocol의 일종으로 쉽게 이야기하면 웹버전의 TCP 또는 Socket이라고 이해하면 됩니다.

WebSocket은 서버와 클라이언트 간에 Socket Connection을 유지해서 언제든 양방향 통신 또는 데이터 전송이 가능하도록 하는 기술입니다. 주로

Real-time web application구현을 위해 널리 사용되어지고 있습니다. (SNS 애플리케이션, LoL 같은 멀티플레이어 게임, 구글 Doc, 증권거래, 화상채팅 등)


WebSocket Sevrer를 운용할 때의 유의사항

HTTP에서 동작하나, 그 방식이 HTTP와는 많이 상이하다.

 

  • REST 한 방식의 HTTP 통신에서는 많은 URI를 통해 application이 설계된다.

  • WebSocket은 하나의 URL을 통해 Connection이 맺어지고, 후에는 해당 Connection으로만 통신한다

  • WebSocket은 먼저 서버와의 정기적인 HTTP 연결을 설정 한 다음 Upgrade헤더를 전송하여 양방향 웹 소켓 연결로 업그레이드합니다.

Handshake가 완료되고 Connection을 유지한다.

  • 전통적인 HTTP 통신은 요청-응답이 완료되면 Connection을 close 한다. 때문에 이론상 하나의 Server가 Port 수의 한계(n <65535)를n<65535 넘는 client의 요청을 처리할 수 있다.

  • WebSocket은 Connection을 유지하고 있으므로, 가용 Port 수만큼의 Client와 통신할 수 있다.


WebSocket API

Spring frameworkd는 WebSocket 메시지를 처리하는 Client, Server 측 application을 작성하는데

사용할 수 있는 WebSocket API를 제공하고 있습니다.

 

예제를 통하여 알아보도록 합시다. 아래의 순서대로 초기 프로젝트 셋팅을 진행하도록 하겠습니다.

 

  1. http://start.spring.io/. 로 이동

  2. Artifact’s에 원하는 값 입력 (저는 pring-boot-websocket-demo 진행하였습니다.)

  3. dependencies 영역에 Websocket을

  4. Generate Project 을 클릭 후 프로젝트를 다운로드합니다.

  5. 압축 해제하여 해당 프로젝트를 자신이 사용하는  IDE로 import 합니다

    (저는 intellij community로 진행하였습니다.)

     

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>spring-boot-websocket-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot-websocket-demo</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

최종 프로젝트 구조

 

 

첫 번째 단계는 WebSocket end-point 및 message broker를 구성하는 것입니다.

config 패키지를 생성하여 WebSocketConfig 클래스를 생성합니다.

package com.example.springbootwebsocketdemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker//@EnableWebSocketMessageBroker is used to enable our WebSocket server
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");
    }
}

 

@Configuration - 클래스 선언 앞에 작성합니다. 이 어노테이션은 해당 클래스가 Bean의 설정을 할 것이라는 것을 나타냅니다.

 

@EnableWebSocketMessageBroker - WebSocket 서버를 활성화하는 데 사용됩니다.

 

implements WebSocketMessageBrokerConfigurer

- 웹 소켓 연결을 구성하기 위한 메서드를 구현하고 제공합니다.

 

registerStompEndpoints 메서드

- 클라이언트가 웹 소켓 서버에 연결하는 데 사용할 웹 소켓 엔드 포인트를 등록합니다.

  엔드 포인트 구성에 withSockJS ()를 사용합니다.

  SockJS는 웹 소켓을 지원하지 않는 브라우저에 폴백 옵션을 활성화하는 데 사용됩니다.

  Fallback 이란?  어떤 기능이 약해지거나 제대로 동작하지 않을 때, 이에 대처하는 기능 또는 동작
메소드 이름에 STOMP라는 단어가 있을 수 있습니다. 이러한 메서드는 Spring 프레임 워크 STOMP 구현에서 제공됩니다.
STOMP는 Simple Text Oriented Messaging Protocol의 약어입니다. 데이터 교환의 형식과 규칙을 정의하는 메시징 프로토콜입니다.
STOM를 사용하는 이유?  WebSocket은 통신 프로토콜 일뿐입니다. 특정 주제를 구독한 사용자에게만 메시지를 보내는 방법 또는 특정 사용자에게 메시지를 보내는 방법과 같은 내용은 정의하지 않습니다. 이러한 기능을 위해서는 STOMP가 필요합니다.

 

configureMessageBroker 메서드

- 한 클라이언트에서 다른 클라이언트로 메시지를 라우팅 하는 데 사용될 메시지 브로커를 구성하고 있습니다.

 

registry.setApplicationDestinationPrefixes("/app");

- "/app" 시작되는 메시지가 message-handling methods으로 라우팅 되어야 한다는 것을 명시합니다.

 

registry.enableSimpleBroker("/topic");

"/topic" 시작되는 메시지가 메시지 브로커로 라우팅 되도록 정의합니다.

  메시지 브로커는 특정 주제를 구독 한 연결된 모든 클라이언트에게 메시지를 broadcast 합니다.

브로드캐스팅은 송신 호스트가 전송한 데이터가 네트워크에 연결된 모든 호스트에 전송되는 방식을 의미한다.

위의 예에서 간단한 인 메모리 메시지 브로커를 활성화했습니다.

그러나 RabbitMQ 또는 ActiveMQ와 같은 다른 모든 기능을 갖춘 메시지 브로커를 자유롭게 사용할 수 있습니다.

 

 

 

클라이언트와 서버 간에 교환되는 메시지 페이로드로 사용될 모델 클래스를 만들겠습니다.

model 패키지를 생성하여 ChatMessage 클래스를 생성합니다.

package com.example.springbootwebsocketdemo.model;

public class ChatMessage {

    private MessageType type;
    private String content;
    private String sender;

    public MessageType getType() {
        return type;
    }

    public void setType(MessageType type) {
        this.type = type;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }
}

같은 패키지에 MessageType을 정의한 Enum을 생성합니다.

package com.example.springbootwebsocketdemo.model;

public enum MessageType {
    CHAT,
    JOIN,
    LEAVE
}

 

 

다음은 Controller를 작성하겠습니다. Controller의 메서드는 message handling methods입니다.

이 메서드들은 한 Client에게서 message를 수신한 다음, 다른 Client에게 broadcast합니다.

 

controller 패키지를 생성하고 ChatController 클래스를 생성합니다.

package com.example.springbootwebsocketdemo.controller;

import com.example.springbootwebsocketdemo.model.ChatMessage;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import org.springframework.messaging.simp.SimpMessageHeaderAccessor;

@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;
    }
}

 

위에서 작성한 WebSocketConfig에서

"/app"로 시작하는 대상이 있는 클라이언트에서 보낸 모든 메시지는 @MessageMapping 어노테이션이 달린 

메서드로 라우팅 됩니다.

예를 들어

"/app/chat.sendMessage" 인 메세지는 sendMessage()로 라우팅 되며

"/app/chat.addUser" 인 메시지는 addUser()로 라우팅됩니다.

 

event listner를 이용하여 소켓 연결(socket connect) 그리고 소켓 연결 끊기(disconnect이벤트를 수신하여

사용자가 채팅방을 참여(JOIN)하거나 떠날때(LEAVE)의 이벤트를 logging 하거나 broadcast 할 수 있습니다.

package com.example.springbootwebsocketdemo.controller;

import com.example.springbootwebsocketdemo.model.ChatMessage;
import com.example.springbootwebsocketdemo.model.MessageType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

@Component
public class WebSocketEventListener {

    private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);

    @Autowired
    private SimpMessageSendingOperations messagingTemplate;

    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        logger.info("Received a new web socket connection");
    }

    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());

        String username = (String) headerAccessor.getSessionAttributes().get("username");
        if(username != null) {
            logger.info("User Disconnected : " + username);

            ChatMessage chatMessage = new ChatMessage();
            chatMessage.setType(MessageType.LEAVE);
            chatMessage.setSender(username);

            messagingTemplate.convertAndSend("/topic/public", chatMessage);
        }
    }
}

@Component - 어노테이션은 자바 클래스를 스프링 빈이라고 표시하는 역할을 합니다. 이 어노테이션을 사용함으로써 스프링의 component-scanning 기술이 이 클래스를 어플리케이션 컨텍스트에 빈으로 등록하게 됩니다.

 

@EventListener - Spring 4.2부터는 이벤트 리스너가 ApplicationListener 인터페이스를 구현하는 Bean 일 필요가 없어졌습니다. @EventListener 주석을 통해 관리되는 Bean의 모든 public 메소드에 등록 할 수 있습니다.

해당 어노테이션은 Bean으로 등록된 Class의 메서드에서 사용할 수 있습니다.

해당 어노테이션이 적용되어 있는 메서드의 인수로 현재 SessionConnectedEventSessionDisconnectEvent

있습니다. 해당 클래스들의 상속관계를 거슬로 올라가다 보면 ApplicationEvent를 상속 받는것을 알 수 있습니다.(Spring 4.2 부터는 ApplicationEvent를 상속받지 않는 POJO클래스로도 이벤트로 사용가능하다고 합니다.)

https://www.baeldung.com/spring-events

 

이미 ChatControlleraddUser()메서드에서 사용자 참여 이벤트를  broadcast하였기 때문에

첫번째 메서드인 handleWebSocketConnectListener()에서 사용하는 SessionConnected 이벤트 에서는

별다른 동작 없이 logging 처리를 하겠습니다.

 

두번째 메서드인 SessionDisconnect 이벤트에서는 웹 소켓 세션에서 사용자 이름을 추출하고 연결된 모든 클라이언트에게 사용자 퇴장 이벤트를 broadcast하는 코드를 작성했습니다.

 

다음 포스팅에서는 지금까지 작성한  application을 이용하는 간단한 화면 구성을 진행하도록 하겠습니다.

 

부족한 부분 지적해주시면 더욱 공부해보고 수정해나가겠습니다.

감사합니다.

 

 

 

 

 

 

출처 : https://supawer0728.github.io/2018/03/30/spring-websocket/

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

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

출처 : https://www.callicoder.com/spring-boot-websocket-chat-example/

Comments