본문 바로가기
공부/Spring

[Spring] 스프링 웹 소켓 TextWebSocketHandler 사용법

by 웅대 2023. 5. 17.
728x90
반응형

이번 포스팅에서는 스프링에서 제공하는 TextWebSocketHandler를 사용해서 채팅 기능을 구현해보려고 한다.

 

대부분의 서버와의 통신 프로토콜은 HTTP를 사용할 것이다.

 

HTTP 통신은 다음과 같은 특징이 있다.

 

  1. 비연결성 (connectionless) : 연결을 맺고 요청을 하고 응답을 받으면 연결을 끊어버린다.
  2. 무상태성 (stateless) : 서버가 클라이언트의 상태를 가지고 있지 않는다.
  3. 단방향 통신이다.

이러한 HTTP 통신의 경우 채팅과 같은 실시간 통신에 적합하지 않다.

 

물론 HTTP 통신으로 실시간 통신을 흉내낼 수는 있으나 완벽하지는 않다.

 

실시간 통신이 필요할 때 사용하는 통신을 소켓 통신이라고 한다.

 

HTTP 통신과 다르게 연결을 맺고 바로 끊어버리는 게 아니라 계속 유지를 하기 때문에 실시간 통신에 적합하다.

 

이제 스프링에서 간단하게 웹 소켓으로 통신을 해보려고 한다.

 

이번 포스팅에서 만들어 볼 기능은 다음과 같다.

 

  1. 채팅 채널에 연결하면 연결 중인 다른 사람들에게 입장 메시지를 보낸다.
  2. 채팅 채널과 연결을 종료하면 연결 중인 다른 사람들에게 퇴장 메시지를 보낸다.
  3. 메시지를 보내면 이름과 보낸 시간을 볼 수 있다.

 

 

다음은 start.spring.io 설정이다.

그 다음은 build.gradle에서 gson 관련 라이브러리를 받아야 한다.

 

<build.gradle>

dependencies {
	.
	.
	.
   implementation 'com.google.code.gson:gson:2.10.1'

}

 

gson에 대해 간략하게 설명하자면 객체와 JSON을 상호 변환해주는 라이브러리라고 보면 된다.

 

우리가 만들어 볼 기능 3번을 보면 메시지만 보내는 것이 아닌 작성 시간 또한 같이 보내줘야 한다.

 

그래서 클라이언트에서는 JSON 형태로 메시지와 시간을 보내면 이를 다른 사용자에게 보여주는 형식으로 구현하시 위해서 gson을 사용한다.

 

해당 ChatMessage 객체는 다음과 같다. 현재 시간과 메시지 내용이 담겨있다.

@Getter
@Setter
public class ChatMessage{
    private String time;
    private String content;
}

 

다음은 WebSocketHandler이다. TextWebSocketHandler를 상속받았다.

@Component
public class WebSocketHandler extends TextWebSocketHandler {
    private List<WebSocketSession> sessionList = new ArrayList<>();
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
        String name = session.getHandshakeHeaders().get("name").get(0);
        sessionList.add(session);
        System.out.println("sessionList = " + sessionList.size());
        sessionList.forEach(s-> {
            try {
                s.sendMessage(new TextMessage(name+"님께서 입장하셨습니다."));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
        Gson gson = new Gson();
        String name = session.getHandshakeHeaders().get("name").get(0);
        sessionList.forEach(s-> {
            try {
                ChatMessage chatMessage = gson.fromJson(message.getPayload(), ChatMessage.class);
                s.sendMessage(new TextMessage(name + " : "+chatMessage.getContent()+"["+chatMessage.getTime()+"]"));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
        sessionList.remove(session);
        System.out.println("session = " + sessionList.size());
        String name = session.getHandshakeHeaders().get("name").get(0);
        sessionList.forEach(s-> {
            try {
                s.sendMessage(new TextMessage(name+"님께서 퇴장하셨습니다."));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

하나씩 살펴보자.

 

먼저 sessioinList이다. WebSocketSession을 리스트로 가지고 있는 필드이다.

    private List<WebSocketSession> sessionList = new ArrayList<>();

여기에는 현재 연결 중인 클라이언트들이 존재한다고 보면 된다.

 

누군가가 접속을 한다면 그 사람의 WebSocketSession을 리스트에 저장하고 접속을 끊는다면 그 사람의 WebSocketSession을 리스트에서 제거한다.

 

이제 3가지 메소드에 대해서 알아보자. 메소드 이름이 곧 역할이기 때문에 이해가 어렵지 않을 것이다.

 

그 전에 자세히 보면 WebSocketSession 이란 것이 보인다.

728x90
반응형

WebSocketSession

 

WebSocketSession은 연결을 맺고나서 유지되는 세션이라고 보면 된다.

 

이는 연결을 끊기 전까지 유지되기 때문에 연결을 맺을 때 전달한 정보를 사용할 수 있다.

 

만약 연결을 맺을 때 유저 정보를 헤더에 담아서 맺었다면 이 정보를 계속 사용할 수 있는 것이다.

 

이번 포스팅에서는 간단하게 header에 유저 이름을 담아서 사용할 것이다.

 

그리고 WebSocketSession에는 sendMessage를 통해 메시지를 보낼 수 있다.

 

이제 TextWebSocketHandler의 메소드를 알아보자.

 

1. afterConnectionEstablished

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
        String name = session.getHandshakeHeaders().get("name").get(0);
        sessionList.add(session);
        System.out.println("sessionList = " + sessionList.size());
        sessionList.forEach(s-> {
            try {
                s.sendMessage(new TextMessage(name+"님께서 입장하셨습니다."));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

이름에서 알 수 있듯이 연결을 맺고나서 실행되는 메소드이다.

 

WebSocketSession의 getHandShakeHeaders 메소드를 실행하면 헤더를 가져올 수 있다.

 

그 헤더 중 key가 name인 value를 가져온다. 이는 곧 연결 중인 사용자의 이름을 의미한다.

 

그리고 sessionList에 해당 유저의 WebSocketSession을 추가한다. 

 

이는 이 유저가 채팅에 참여하고 있다는 것을 의미하고 이 채팅에 참여 중인 다른 유저의 메시지를 받을 수 있다.

 

이렇게 sessionList에 추가하고나면 sessionList에 있는 모든 WebSocketSession에 메시지를 보낸다.

 

현재 채팅에 참여 중인 모든 사람들에게 입장 메시지를 보내는 것이다.

 

2. handleTextMessage

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
        Gson gson = new Gson();
        String name = session.getHandshakeHeaders().get("name").get(0);
        sessionList.forEach(s-> {
            try {
                ChatMessage chatMessage = gson.fromJson(message.getPayload(), ChatMessage.class);
                s.sendMessage(new TextMessage(name + " : "+chatMessage.getContent()+"["+chatMessage.getTime()+"]"));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

메시지를 다루는 메소드이다. 우리는 메시지를 보낼 때 누가 보냈는지와 보낸 시간을 함께 출력해야한다.

 

메시지를 다음과 같이 변환한다.

s.sendMessage(new TextMessage(name + " : "+chatMessage.getContent()+"["+chatMessage.getTime()+"]"));

 

출력 

minsoo : 안녕하세요~ [5월 16일 17시 20분]

 

3. afterConnectionClosed

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
        sessionList.remove(session);
        System.out.println("session = " + sessionList.size());
        String name = session.getHandshakeHeaders().get("name").get(0);
        sessionList.forEach(s-> {
            try {
                s.sendMessage(new TextMessage(name+"님께서 퇴장하셨습니다."));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

afterConnectionEstablished 메소드와 거의 동일하다. 채팅에 참여 중인 모든 유저에게 퇴장 메시지를 보낸다.

 

이제 이 핸들러를 추가해주는 설정을 할 차례이다.

@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private final WebSocketHandler chatHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler, "/websocket-test").setAllowedOrigins("*");
    }

}

"/websocket-test" 경로가 곧 연결을 맺는 경로이다. 정확한 경로는 "ws://localhost:8080/websocket-test"가 될 것이다.

 

이 경로로 우리가 만든 핸들러를 추가한다.

 

이제 postman을 통해 테스트를 해본다.

 

좌측 상단의 New에서

WebSocket Request를 선택한다.

이제 경로에 "ws://localhost:8080/websocket-test"를 입력하고 Headers에 name을 추가한다. 

 

이는 사용자를 식별할 이름이다. 이제 connect를 클릭한다.

입장 메시지가 잘 뜨는 모습을 확인할 수 있다.

 

<철수>

연결을 그대로 두고 새로운 WebSocketRequest를 띄운다.

 

역시 똑같이 연결하는데 이번에는 Headers의 name의 value를 다르게 한다.

 

서로 다른 두 사용자간의 채팅이 되는지 확인하기 위함이다.

<민수>

역시 민수에게도 입장 메시지가 보인다.

<민수>

철수에게도 민수의 입장 메시지가 보이는 모습을 확인할 수 있다.

 

<철수>

이제 철수가 메시지를 보낸다. 철수와 민수 모두 철수의 메시지를 확인할 수 있어야 한다.

 

ChatMessage 형태에 맞게끔 JSON 메시지를 작성한다.

 

지금은 테스트라 그냥 String으로 보낸 시각을 표현했지만 실제로는 현재 시간을 구하는 로직을 작성한 다음 정확한 현재 시간을 구하도록 구현해야 한다.

 

<철수>

민수에게도 메시지가 보이는지 확인한다.

 

<민수>

웹 소켓을 사용하여 여러 사용자간 실시간 채팅을 구현해보았다.

 

그런데 웹 소켓만을 사용하면 주고 받는 메시지의 형식이 구체적으로 정의되지 않는다.

 

그래서 나온 프로토콜이 바로 STOMP(Simple Text Oriented Messaging Protocol)이다.

 

형식이 정해져있기 때문에 여러 장점들이 존재한다.

 

다음 링크는 STOMP에 대한 설명과 간단한 사용법이다.

 

https://growth-coder.tistory.com/157

 

[Spring][WebSocket] 스프링 STOMP와 웹 소켓 개념 및 사용법 (Web Socket with STOMP) (1)

클라이언트와 서버가 통신할 때 HTTP 통신을 주로 사용한다. HTTP 통신은 다음과 같은 특징이 있다. 비연결성 (connectionless) : 연결을 맺고 요청을 하고 응답을 받으면 연결을 끊어버린다. 무상태성 (

growth-coder.tistory.com

 

728x90
반응형

댓글