WebSocket, SockJs, STOMP를 이용한 웹 채팅 구현 - (1) Spring boot

메밀·2022년 11월 14일
0

1. 시작하며

파이널 프로젝트로 만든 협업툴 Safari를 위해 Spring boot 환경에서 WebSocket, SockJs, STOMP를 사용해 간단한 채팅을 만들어 보았다.

2. pom.xml

pom.xml에 dependency를 추가한다.

	<dependencies>
		<!-- SOCK-JS -->
		<dependency>
		    <groupId>org.webjars</groupId>
		    <artifactId>sockjs-client</artifactId>
		    <version>1.5.1</version>
		</dependency>
		
      	<!-- stomp -->
		<dependency>
		    <groupId>org.webjars</groupId>
		    <artifactId>stomp-websocket</artifactId>
		    <version>2.3.4</version>
		</dependency>		
      
        <!-- websocket -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>
	</dependencies>

1) WebSocket

HTTP는 Request/Response 기반의 Stateless protocol로 클라이언트에서 Request를 보내면 서버가 Response를 하는 단방향 통신이다. 이 경우 서버의 데이터가 업데이트 되더라도 클라이언트가 refresh를 하지 않는 이상 변화된 데이터가 업데이트 되지는 않는다. 읽고 있던 게시판의 글이 수정되더라도 새로고침을 누르지 않으면 변경 사항이 반영되지는 않는다는 것이다.

그런데 매번 리로드를 눌러야 받은 메시지를 확인할 수 있다면 그것을 채팅이라고 부르기는 어려울 것이다. WebSocket은 이런 상황을 해결한다. WebSocket은 서버와 클라이언트 간에 Socket Connection을 유지해서 언제든 양방향 통신 또는 데이터 전송이 가능하도록 하는 기술이다.

2) SockJS

이때 모든 브라우저에서 WebSocket을 지원한다는 보장이 없으므로 WebSocket Emulation 또한 함께 dependency에 추가해준다. sockjs는 일단 WebSocket을 시도하고, 실패할 경우 HTTP Streaming, Long-Polling 같은 HTTP 기반의 다른 기술로 전환해 다시 연결을 시도한다.

대개 node.js 환경에선 Socket.io를, Spring 환경에선 SockJS를 이용한다.

3) STOMP

pub/sub 구조의 메세징 전송 프로토콜. WebSocket 위에서 동작하는 프로토콜로써 클라이언트와 서버가 전송할 메세지의 유형, 형식, 내용을 정의하는 매커니즘이다.




3. StompWebsocketConfig

package com.gd.safari;

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;

import com.gd.safari.commons.TeamColor;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
	
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/stomp/chat")
                .setAllowedOrigins("http://localhost:80", "http://safari.o-r.kr/", "http://43.200.96.123/")
                .withSockJS();
    }

    // 어플리케이션 내부에서 사용할 path를 지정할 수 있음
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
    	log.debug(TeamColor.CSK + "configureMessageBroker");
    	
    	// Client에서 SEND 요청을 처리
        registry.setApplicationDestinationPrefixes("/pub");
        registry.enableSimpleBroker("/sub");
    }
}

1) @EnableWebSocketMessageBroker와 WebSocketMessageBrokerConfigurer 인터페이스

WebSocket 서버 설정을 위해 @EnableWebSocketMessageBroker 어노테이션을 사용하였다. 이때 StompWebSocketConfig 클래스는 WebSocketMessageBrokerConfigurer 인터페이스를 구현한다.

2) registerStompEndpoints(StompEndpointRegistry registry)

클라이언트에서 WebSocket에 접속하는 endpoint를 등록하는 메소드.

- addEndpoint("endpoint")

endpoint 추가.

클라이언트 연결 후 해당 엔드포인트로 웹소켓 통신 가능 여부를 확인한 후, 응답이 websocket:true 이면 웹소켓이 연결된다.

- setAllowedOrigins

허용할 origin을 설정.

WebSocket과 SockJs는 기본적으로 동일한 Origin 요청만을 수락한다.

protocol : http
host : localhost
port : 80

3개가 모두 동일한 경우만 동일한 Origin이라고 말한다.

와일드카드("*")를 사용하여 모든 요청을 수락할 수도 있지만 보안상의 이유로 권장되지 않는다.

- withSockJS()

브라우저가 websocket을 지원하지 않을 경우 fallback 옵션을 활성화.

3) configureMessageBroker

한 클라이언트에서 다른 클라이언트로 메시지를 라우팅할 때 사용하는 브로커를 구성.

/pub 경로로 시작하는 STOMP 메세지의 "destination" 헤더는 @Controller 객체의 @MessageMapping 메서드로 라우팅된다.

내장된 메세지 브로커를 사용해 Client에게 Subscriptions, Broadcasting 기능을 제공한다. 또한 /sub로 시작하는 "destination" 헤더를 가진 메세지를 브로커로 라우팅한다.




4. RestChatController


@Controller
@Slf4j
@RestController
@RequestMapping("/member/chat")
public class RestChatController {
	@Autowired
	private SimpMessagingTemplate template; // 특정 브로커로 메시지 전달
	@Autowired
	private IChatService chatService;
	
	/*
    생략
    */
	
	// 일반 메시지 매핑
    @MessageMapping(value="/chat/message")
    public void message(Map<String, Object> map) {
       log.debug(TeamColor.CSK + "message: " + map);
       map.put("time", LocalDateTime.now());
       
       // DB에 저장
       chatService.addChatMsg(map);
        
       template.convertAndSend("/sub/chat/" + map.get("chatRoomNo"), map);
    }
}

1) @MessageMapping(value="/chat/message") public void message()

WebSocket으로 들어오는 메세지 발행을 처리하는 메소드.

Client에서 "/pub/chat/enter"로 발행 요청을 하면 StompWebSocketConfig.configureMessageBroker()의 설정값에 따라 Controller 객체의 @MessageMapping 메서드로 라우팅된다. 컨트롤러는 해당 메세지를 받아 처리하고, "/sub/chat/[roomId]"로 메세지를 전송한다. Client는 해당 주소를 SUBSCRIBE하고 있다가 메세지가 전달되면 화면에 출력한다.

기존의 핸들러 ChatHandler의 역할을 대신 해주므로 핸들러는 없어도 된다.

- @MessageMapping

Client가 SEND 하는 경로. StompWebsocketConfig에서 등록한 applicationDestinationPrefixes와 @MessageMapping의 경로가 합쳐진다.(/pub/message)

- SimpMessagingTemplate.convertAndSend()

@EnableWebSocketMessageBroker를 통해서 등록되는 Bean. 메시지를 전달한다.

- LocalDateTime.now()

화면에 채팅 메시지 시간을 띄우기 위해 현재 시각을 함께 전송한다. (ex. 5분 전, 7시간 전 - 자바스크립트 함수로 구현)

0개의 댓글