토이 프로젝트/Spring&Java 갖고놀기

Spring Web Chatting : 스프링 채팅 만들기 웹소켓 맛보기

TerianP 2022. 8. 24.
728x90

시작하면서

오랜만에 돌아온 자바 갖고놀기 프로젝트!!

이번에는 예전부터 정말정말정말 해보고 싶었던 spring 과 웹소켓을 이용한 채팅 프로그램 구현을 해보려고 합니다.

 

오늘 뼈대 만들기를 시작해서 stomp 를 사용한 채팅 구현, 파일 업로드, DB 와 연결 등등 여러가지를 더해서 만들겠습니다!

추가로 아래 코드 및 설명에서 등장하는 새로 공부하게된 어노테이션과 개념들은 따로 정리하도록 하겠습니다

 

그럼 시작하겠습니다

 


필수 라이브러리 임포트

- gradle 에 websocket 임포트

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

 

 

websocket Handler 작성

  • 웹 소켓 클라이언트로부터 채팅 메시지를 전달받아 채팅 메시지 객체로 변환
  • 전달받은 메시지에 담긴 채팅방 Id 로 발송 대상 채팅방 정보를 조회
  • 해당 채팅방에 입장해 있는 모든 클라이언트(Websocket Session) 에게 타입에 따른 메시지 발송
package webChat.Handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import webChat.dto.ChatDTO;
import webChat.dto.ChatRoom;
import webChat.service.ChatService;

@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketChatHandler extends TextWebSocketHandler {

    private final ObjectMapper mapper;

    private final ChatService service;

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("payload {}", payload);

//        TextMessage textMessage = new TextMessage("Welcome Chatting Server");
//        session.sendMessage(textMessage);

        ChatDTO chatMessage = mapper.readValue(payload, ChatDTO.class);
        log.info("session {}", chatMessage.toString());

        ChatRoom room = service.findRoomById(chatMessage.getRoomId());
        log.info("room {}", room.toString());

        room.handleAction(session, chatMessage, service);
    }
}

 

 

config 설정

package webChat;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

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

    // WebSocketHandler 에 관한 생성자 추가
    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // endpoint 설정 : /ws/chat
        // 이를 통해서 ws://localhost:8080/ws/chat 으로 요청이 들어오면 websocket 통신을 진행한다.
        registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
    }
}

 

 

채팅 메시지 클래스 : ChatDTO

  • 채팅 메시지에 대한 정보를 담는 클래스 : 일종의 채팅 내용에 대한 DTO
  • 채팅 내용은 크게 들어오는 사람에 대한 환영 메시지에 대한 ENTER 과 방에 있는 사람들이 채팅을 칠 때 사용하는 TALK 두 가지로 메시지 타입을 나눈다. 이때 타입은 ENUM 으로 선언한다.
  • 다음으로 어떤 방에서 채팅이 오가는지 확인하기 위한 방번호, 채팅 보낸 사람, 메시지, 채팅 발송 시간 등을 변수로 선언한다.
    • 여기서 더 나가면 ENTER, TALK 뿐만 아니라 OUT 으로 메시지 타입을 추가해서 나가는 사람에 대한 메시지를 전달해도 좋을듯!
package webChat.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ChatDTO {
    // 메시지  타입 : 입장, 채팅
    public enum MessageType{
        ENTER, TALK
    }

    private MessageType type; // 메시지 타입
    private String roomId; // 방 번호
    private String sender; // 채팅을 보낸 사람
    private String message; // 메시지
    private String time; // 채팅 발송 시간간
}

 

 

채팅방 클래스 : ChatRoom

  • 채팅방에 대한 정보를 담는 클래스 : 역시나 채팅방 DTO 로 생각하면 편하다!
  • 채팅방 클래스는 해당 채팅방에 어떤 사람이 있는지에 대한 정보를 갖고 있어야 한다. 즉 채팅방에 입장한 클라이언트에 대한 내용, 즉 클라이언트별 세션을 갖고 있어야한다.
    • 이를 위해서 클라이언트별로 세션을 저장하기 위한 sessions 라는 이름의 HashSet 을 만든다.
  • 다음으로 채팅방의 아이디, 채팅방의 이름을 변수로 갖는다.
  • 메서드는 총 2가지를 선언한다.
    • handleAction : message type 에 따라서 session(클라이언트)에게 메시지를 전달하기 위한 메서드이다. type 이 ENTER 인 경우 채팅방에 “환영합니다”를 띄우고 TALK 인 경우 채팅방에 클라이언트가 발송한 채팅내용(message) 내용을 그대로 채팅방에 반환한다.
    • sendMessage 는 sessions 에 담긴 모든 session 에 handleAction 으로 부터 넘어온 message 를 전달할 수 있도록 하는 메서드이다.
package webChat.dto;

import lombok.Builder;
import lombok.Data;
import org.springframework.web.socket.WebSocketSession;
import webChat.service.ChatService;

import java.util.HashSet;
import java.util.Set;

@Data
public class ChatRoom {
    private String roomId; // 채팅방 아이디
    private String name; // 채팅방 이름
    private Set<WebSocketSession> sessions = new HashSet<>();

    @Builder
    public ChatRoom(String roomId, String name){
        this.roomId = roomId;
        this.name = name;
    }

    public void handleAction(WebSocketSession session, ChatDTO message, ChatService service) {
        // message 에 담긴 타입을 확인한다.
        // 이때 message 에서 getType 으로 가져온 내용이
        // ChatDTO 의 열거형인 MessageType 안에 있는 ENTER 과 동일한 값이라면
        if (message.getType().equals(ChatDTO.MessageType.ENTER)) {
            // sessions 에 넘어온 session 을 담고,
            sessions.add(session);

            // message 에는 입장하였다는 메시지를 띄운다
            message.setMessage(message.getSender() + " 님이 입장하셨습니다");
            sendMessage(message, service);
        } else if (message.getType().equals(ChatDTO.MessageType.TALK)) {
            message.setMessage(message.getMessage());
            sendMessage(message, service);
        }
    }

    public <T> void sendMessage(T message, ChatService service) {
        sessions.parallelStream().forEach(session -> service.sendMessage(session, message));
    }
}

 

 

채팅 서비스 : ChatService

  • 채팅 서비스 클래스 : 여기서 사용되는 findAllRoom, createRoom, findRoomById 등은 사실상 DB 와 연결되는 순간 DAO 로 넘어가야한다.
  • 지금은 DB 와 연결없이 만들 예정이기 때문에 일단 Service 클래스에 다 때려박아두었다.
    • DB 와 연결이 없기 때문에 일단 채팅방 정보가 HashMap 안에 저장되어 있다.
  • createRoom : UUID 를 통해 랜던으로 생성된 UUID 값으로 채팅방 아이디를 정하고, NAME으로 채팅방 이름을 정해서 채팅방을 생성한다.
  • sendMessage : 지정된 세션에 메시지를 발송한다. 여기서 사용되는 메서드들은 아래에 따로 정리 예정!
  • find~~~Room : roomId 를 기준으로 map 에 담긴 채팅방 정보를 조회한다.
package webChat.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import webChat.dto.ChatRoom;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.*;

@Slf4j
@Data
@Service
public class ChatService {
    private final ObjectMapper mapper;
    private Map<String, ChatRoom> chatRooms;

    @PostConstruct
    private void init() {
        chatRooms = new LinkedHashMap<>();
    }

    public List<ChatRoom> findAllRoom(){
        return new ArrayList<>(chatRooms.values());
    }

    public ChatRoom findRoomById(String roomId){
        return chatRooms.get(roomId);
    }

    public ChatRoom createRoom(String name) {
        String roomId = UUID.randomUUID().toString(); // 랜덤한 방 아이디 생성

        // Builder 를 이용해서 ChatRoom 을 Building
        ChatRoom room = ChatRoom.builder()
                .roomId(roomId)
                .name(name)
                .build();

        chatRooms.put(roomId, room); // 랜덤 아이디와 room 정보를 Map 에 저장
        return room;
    }

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

 

 

Controller : RestController

  • 컨트롤러는 RestController 로 구현한다.
  • 이는 메시지를 받을때도 보낼때도 json 형식을 사용하기 때문!!
package webChat.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import webChat.dto.ChatRoom;
import webChat.service.ChatService;

import java.util.List;

@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/chat")
public class ChatController {

    private final ChatService service;

    @PostMapping
    public ChatRoom createRoom(@RequestParam String name){
        return service.createRoom(name);
    }

    @GetMapping
    public List<ChatRoom> findAllRooms(){
        return service.findAllRoom();
    }
}

 


코드 구현 확인하기

- 크롬 확장 프로그램인 talend api tster 와 socket test client 를 사용해서 채팅 방을 만들고, 채팅을 시작하도록 하겠습니다.

- 아래처럼 http://localhost:8080/chat?name=채팅방명 으로 요청을 보내면 서버에서 roomId, name, session 을 보내준다

 

http://localhost:8080/chat?name=start && roomId 를 기억하자

 

 

- 받아온 roodId 를 이용해서 아래처럼 채팅구현이 가능하다. 

- roomId 를 일종의 채팅방으로 생각하면 되고, talk 와 enter 를 작성해가면서 채팅을 보내게 된다. 

웹 소켓으로 구현한 채팅 뼈대

 

- Reference

https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm

 

Talend API Tester - Free Edition

Visually interact with REST, SOAP and HTTP APIs.

chrome.google.com

 

https://chrome.google.com/webstore/detail/websocket-test-client/fgponpodhbmadfljofbimhhlengambbn

 

WebSocket Test Client

A Simple tool to help test WebSocket Service

chrome.google.com

 

https://daddyprogrammer.org/post/4077/spring-websocket-chatting/

 

Spring websocket chatting server(1) - basic websocket server

Spring에서 제공하는 Websocket을 이용하여 간단한 채팅 서버를 구현해 보도록 하겠습니다. 일반적인 http통신을 하는 서버들과 달리 채팅 서버는 socket통신을 하는 서버가 필요합니다. 통상적으로 htt

daddyprogrammer.org

https://dev-gorany.tistory.com/3

 

[Spring MVC] Web Socket(웹 소켓)과 Chatting(채팅)

 기존 공부 용도의 게시판(?)에 여러 기능을 추가하던 차, 관리자와 멤버 간 채팅 기능을 구현하고 싶었다. 채팅을 하려면 웹 소켓이 필요하다고 한다. 간단하게 구현하는 것은 어렵지 않으므로

dev-gorany.tistory.com

 

댓글