[spring-vue]웹소켓으로 채팅 구현하기 (2) - 프로젝트 생성 및 스프링웹소켓

xxx-sj·2023년 11월 10일
0

웹소켓

목록 보기
2/5
post-thumbnail

📗프로젝트 생성

이 부분은 모두 하실 줄 안다고 생각하여 빠르게 넘기겠습니다. 간단하게 제가 프로젝트를 만들면서 추가한 의존성만 보여드리고 넘어가도록 하겠습니다

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-websocket'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

웹소켓을 사용하기 위해서는 하위 의존성을 추가하자.

implementation 'org.springframework.boot:spring-boot-starter-websocket'

프로젝트 생성 후 잘 생성되었는지 반드시 WebsocketApplication으로 실행해보자.

📗spring-websocket

아래코드는 스프링 doc을 참고하여 구현합니다.

@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler").setAllowedOriginPatterns("*");
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }
}
  • @EnableWebSocket : 웹소켓 서버를 사용하도록 추가합니다.
  • /myHandler : 웹소켓 서버의 endPoint를 "/myHandler" 로 설정합니다.
  • setAllowedOriginPatterns("*") : 웹소켓 서버로의 요청을 모두 수용한다. (실제로는 제한할 것)
  • myHandler() : 웹소켓 핸들러로 MyHandler 클래스를 설정한다.

MyHandler의 클래스는 다음과 같다. 간단히 TextWebSocketHandler extends하여 정의한다.

public class MyHandler extends TextWebSocketHandler {


    //최초 연결 시
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {

    }

    //양방향 데이터 통신할 떄 해당 메서드가 call 된다.
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //do something
    }
    
    //웹소켓 종료
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

    }

    //통신 에러 발생 시
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {


    }
}

MyHandler에서는 4개의 메서드를 구현하는데

  • afterConnectionEstablished: 최초 연결시 call 된다.
  • handleTextMessage: 연결 후 메세지를 주고 받을 때 call 된다.
  • afterConnectionClosed: 웹소켓이 끊키면 call 된다.
  • handleTransportError: 통신 에러 발생시 call 된다.

여기까지 기초적인 설정은 끝났으니 postman으로 테스트를 진행하면서 메서드에 어떠한 데이터들이 들어오는지 확인해보자.
postman으로 websocket 테스트 하는 방법은 많은 블로그에서 소개하고 있으니 따로 적진 않겠습니다.

📘postman으로 테스트 하기

postman에서 websocket로 연결 요청을 보내면 다음과 같이 afterConnectionEstalished메서드에 break point가 걸리는 것을 확인할 수 있다.

해당 메서드의 session 인자의 데이터를 확인해보면 다음과 같다.

session 객체에 id가 보이는데, 이 session id를 key로 갖는 map을 통해 session을 관리해보도록 하자.

인스턴스 필드로 map을 선언하고, afterConnectionEstablished 메서드가 호출될 때 id 와 session을 map에 put 한다. 코드로 보면 다음과 같다.

📘session객체 저장하기

    private final Map<String, WebSocketSession> sessions = new HashMap<>();
    
    //최초 연결 시
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        final String sessionId = session.getId();

        sessions.put(sessionId, session);

    }

다음으로는 최초 연결 시 session객체를 저장하면서, 저장되어있는 다른 session 객체들에게 알림을 보내는 코드를 추가해보자.

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        final String sessionId = session.getId();
		final String enteredMessage = sessionId + "님이 입장하셨습니다.";
        sessions.put(sessionId, session);

        sessions.values().forEach((s) -> {

            try {
                if(!s.getId().equals(sessionId) && s.isOpen()) {
                    
                    s.sendMessage(new TextMessage(enteredMessage));
                }
            } catch (IOException e) {}
        });


    }

여기까지 완료했다면 postman을 통해 tab을 2개를 열어서 코드 테스트를 진행해보자.

📘postman 테스트

시나리오는 다음과 같다.

  • 한쪽을 연결해둔 상태에서 다른 한 탭에서 웹소켓 연결 시 최초 연결했던 탭에 연결되었다는 메시지를 받아야 한다.

이제 다른 탭에서 새로 연결을 해보면~

위 화면과 같이 입장 메시지를 받을것을 확인할 수 있다.

📘handleTextMessage 메서드 구현

다음으로는 연결된 웹소켓들이 메시지를 주고받을 때 call되는 메서드를 구현한다.
이것도 별 다르지 않게 연결되어있는 모든 session에게 메시지를 보낸다.

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //do something
        final String sessionId = session.getId();
        sessions.values().forEach((s) -> {

            if (!s.getId().equals(sessionId) && s.isOpen()) {
                try {
                    s.sendMessage(message);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

📘handleTextMessage 메서드 테스트

동일하게 postman으로 테스트를 진행해보자. 이번에는 두 탭 모두 연결을 한 후, 한 탭에서 메시지를 보내 메시지가 오는지 확인해보자.

일반적인 text를 입력한 후 send를 보내면 다른 쪽에 메시지가 도착하는 것을 확인할 수 있다.

📘afterConnectionClosed 메서드 구현

해당 메서드에서는 session이 끊기게 되면 map에 저장되어있는 객체를 remove 하고, 다른 접속자들에게 leave message를 보낸다.

해당 메서드의 인자 중 CloseStatus status에는 종료상태에 대해 정의되어 있으니 필요에 따라 분기를 통해 정의할 수 있다.

//웹소켓 종료
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        final String sessionId = session.getId();
        final String leaveMessage = sessionId + "님이 떠났습니다.";
        sessions.remove(sessionId); // 삭제
        
        //메시지 전송
        sessions.values().forEach((s) -> {

            if (!s.getId().equals(sessionId) && s.isOpen()) {
                try {
                    s.sendMessage(new TextMessage(leaveMessage));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

테스트로 잘 되는 것도 확인하였다.

📕리팩토링

간단하게 구현하긴 했지만 각각의 메서드에서 메시지를 전송하는 부분은 message를 제외하면 모두 같기 때문에 하나의 메서드를 정의하고 메서드를 call하도록 수정한다. 전체코드는 다음과 같다.


package com.sj.websocket.handler;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MyHandler extends TextWebSocketHandler {

    private final Map<String, WebSocketSession> sessions = new HashMap<>();

    //최초 연결 시
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        final String sessionId = session.getId();
        final String enteredMessage = sessionId + "님이 입장하셨습니다.";

        sessions.put(sessionId, session);

        sendMessage(sessionId, new TextMessage(enteredMessage));

    }

    //양방향 데이터 통신할 떄 해당 메서드가 call 된다.
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //do something
        final String sessionId = session.getId();
        sendMessage(sessionId, message);
    }
    
    //웹소켓 종료
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        final String sessionId = session.getId();
        final String leaveMessage = sessionId + "님이 떠났습니다.";
        sessions.remove(sessionId); // 삭제

        sendMessage(sessionId, new TextMessage(leaveMessage));

    }

    //통신 에러 발생 시
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}

    private void sendMessage(String sessionId, WebSocketMessage<?> message) {
        sessions.values().forEach(s -> {
            if(!s.getId().equals(sessionId) && s.isOpen()) {
                try {
                    s.sendMessage(message);
                } catch (IOException e) {}
            }
        });
    }
}

정리

websocket을 가지고 간단하게 구현하였는데, 위 코드는 입장 시, 퇴장 시, 메시지 보낼 때 모두 다른 세션에 메시지를 보내고 있다. 만약, 특정 사람에게 보내거나, 그룹에 보내야 한다면 최초 웹소켓 연결 시 서버에 object 형식의 데이터를 보내어 그룹을 짓거나 특정 사용자에게 메시지를 보낼 수 있다.

위의 코드는 websocket repository 에서 볼 수 있습니다.

참고

https://brunch.co.kr/@springboot/695
https://docs.spring.io/spring-framework/reference/web/websocket/server.html

profile
틀려도 일단 기록하자

0개의 댓글