본문 바로가기
공부/springboot

스프링 - webSocket 을 이용해서 채팅 구현하기

by 샤샤샤샤 2024. 3. 8.

webSocket : 2011년에 나온 비교적 최신 기술이며, http 처럼 일종의 통신 규약이다. 한번 연결된 이후에 계속해서 연결 상태가 유지되는 통신이라고 생각하면 된다.

 

흐름:

1. 스프링 내부에서 webSocket 요청을 받을 endPoint 설정

2. 자바 스크립트 - 스프링간의 webSocket 통신 연결   

3. 요청(메시지)이 들어올시 처리될 코드 작성

 

 

실제 코드:

0. websocket 을 사용하기 위한 라이브러리 추가

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

 

1. WebSocketConfig 클래스

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

    private final WebSocketChatHandler webSocketChatHandler;
    private final HttpSessionHandshakeInterceptor handshakeInterceptor;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 이를 통해서 ws://localhost:8080/ws/chat 으로 요청이 들어오면 websocket 통신을 진행한다.
        // setAllowedOrigins("*")는 모든 ip에서 접속 가능하도록 해줌
        // addInterceptors 를 통해, 최초 websocket 연결을 위한 통신이 이뤄질때 interceptor 를 적용
        registry.addHandler(webSocketChatHandler, "/ws/chat").setAllowedOrigins("*")
                .addInterceptors(handshakeInterceptor);
    }
}

 

 

설정 클래스로, @EnableWebSocket 를 통해 웹소켓을 사용 가능하게 설정한다.

 

2. 자바스크립트 코드

       // 웹소켓을 연결한다.
        var websocket = new WebSocket("ws://localhost:8080/ws/chat");
        
        websocket.onmessage = onMessage;
        websocket.onopen = onOpen;
        websocket.onclose = onClose;

      // 웹소켓이 연결되면 실행됨
        function onOpen() {
         ...........
        }

       // 웹소켓 연결이 끊기면 실행됨
        function onClose() {
         ...........
        }
        
        // 웹소켓으로부터 메시지를 수신하면 실행됨
        function onMessage(msg) {
          ..........
        }

        // 웹소켓으로 메시지를 보냄
        function sendMessage() {
          let item = {
            clubNo: $("#clubNo").val(),
            message: $("#messageText").val(),
            messageType: "chat",
          };
          websocket.send(JSON.stringify(item));
          $("#messageText").val("");
        }

websocket을 통해 json 형태로 메시지를 보낸다.

websocket.send 함수로 보낼수 있다.

 

3. WebSocketChatHandler 클래스

@Component
@RequiredArgsConstructor
public class WebSocketChatHandler extends TextWebSocketHandler {

    // json -> ChatMessageDto 변경시키는 객체
    private final ObjectMapper mapper;
    
    // websocket 과 연결된 사용자들 세션 저장
    private final Set<WebSocketSession> sessions = new HashSet<>();
    
    // 키 값이 채팅방 번호, 밸류값이 채팅 참여 사용자 세션 저장
    private final Map<Long, Set<WebSocketSession>> chatRoomSessionMap = new ConcurrentHashMap<>();



    // 소켓 연결시 실행되는 함수
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // TODO Auto-generated method stub
        log.info("{} 연결됨", session.getId());

    }

    // 소켓 통신 시, 메시지가 들어오면 실행되는 함수
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        sessions.add(session);
        String payload = message.getPayload();
        ChatMessageDto chatMessageDto = mapper.readValue(payload, ChatMessageDto.class);
        Long chatRoomId = chatMessageDto.getClubNo();

        // 해당 번호의 채팅방이 없으면 새롭게 생성
        if (!chatRoomSessionMap.containsKey(chatRoomId)) {
            chatRoomSessionMap.put(chatRoomId, new HashSet<>());
        }

        // 해당 채팅방의 참여자들의 session
        Set<WebSocketSession> chatRoomSession = chatRoomSessionMap.get(chatRoomId);

        if(chatMessageDto.getMessageType().equals("open")){
            chatRoomSession.add(session);
            sendMessageToChatRoom(chatMessageDto, chatRoomSession);
        }
        
        if (chatMessageDto.getMessageType().equals("chat")) {
            sendMessageToChatRoom(chatMessageDto, chatRoomSession);
        }
        
        if(chatMessageDto.getMessageType().equals("close")){
            sendMessageToChatRoom(chatMessageDto, chatRoomSession);
            chatRoomSession.remove(session);
        }
    }


    // 소켓 종료 확인
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // TODO Auto-generated method stub
        log.info("{} 연결 끊김", session.getId());
        sessions.remove(session);
    }

    // ====== 채팅 관련 메소드 ======
    private void removeClosedSession(Set<WebSocketSession> chatRoomSession) {
        chatRoomSession.removeIf(sess -> !sessions.contains(sess));
    }

    private void sendMessageToChatRoom(ChatMessageDto chatMessageDto, Set<WebSocketSession> chatRoomSession) {
        chatRoomSession.parallelStream().forEach(sess -> sendMessage(sess, chatMessageDto));//2
    }


    public <T> void sendMessage(WebSocketSession session, T message) {
        try {
            session.sendMessage(new TextMessage(mapper.writeValueAsString(message)));
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }
}

 

 

TextWebSocketHandler 는 websocket 을 이용해 문자열을 받기 위해 구현된 클래스다.

연결시, 메시지 수신시, 연결 끊김시 실행될 함수가 각각 이미 구현되어 있기에, 알맞은 코드만 작성해주면 된다.

 

4. (외전) HttpSession 을 websocket 내부에서 사용하기

@Component
public class HttpSessionHandshakeInterceptor implements HandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
            HttpSession session = servletRequest.getServletRequest().getSession(false); // Get HttpSession from the request
            attributes.put("reqSession", session);
        }
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
    }
}

 

 

HandshakeInterceptor 는 처음 websocket 연결을 위한 http handshake가 이뤄질때 호출되는 함수다.

 

기존에 WebSocketChatHandler 내부에서 사용되는 websocketSession 은 웹소켓 내부에서 사용되는 세션으로, HttpServletRequest 에서 사용되는 session 과는 별개의 객체다. 따라서 HttpSession 에 저장된 데이터를 websocketSession 으로 사용하거나, websocket 내부에서 불러올 방법은 없다.

 

따라서 처음 handshake 를 위한 http 통신이 이뤄질때 httpSession 을 불러와 webSocketSession 에 저장해야 httpSession 의 값을 사용 가능하다.

 

Map<String, Object> attributes 이 WebSocketSession 으로 변하게 된다. 따라서 attributes 에 저장하면 webSocketSession 에서도 불러올수 있다.

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        HttpSession reqSession = (HttpSession) session.getAttributes().get("reqSession");
    }