[SpringBoot] Websocket을 활용한 실시간 채팅 구현 (1) -basic

gyehwan·2023년 7월 2일
0

Websocket

목록 보기
1/3

Spring에서 제공하는 Websocket을 이용하여 간단한 채팅 서버를 구현해보자.

일반적인 http통신을 하는 서버들과 달리 채팅 서버는 socket통신을 하는 서버가 필요하다. http통신은 클라이언트의 요청이 있을 때만 서버가 응답하는 단방향 통신이다. 그에 반해 socket통신은 서버와 클라이언트가 지속적으로 연결을 유지하고 양방향으로 통신을 하는 방식이다. 따라서 실시간 통신이 필요한 채팅, 주식 프로그램 등의 서비스에서 많이 사용된다.

Websocket

Websocket은 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜이다. 일반 socket통신과 달리 HTTP 80Port를 이용하므로 방화벽에 제약이 없으며 통상 Websocket으로 불린다. 접속까지는 HTTP 프로토콜을 이용하고 그 이후의 통신은 자체적인 Websocket 프로토콜로 통신하게 된다.

Websocket Handler 작성

socket통신은 서버와 클라이언트가 1:N으로 관계를 맺는다. 따라서 한 서버에 여러 클라이언트가 접속할 수 있으며, 서버에는 여러 클라이언트가 발송한 메세지를 받아 처리해줄 Handler의 작성이 필요하다. 다음과 같이 TextWebSocketHandler를 상속받아 Handler를 작성해준다. 클라이언트로부터 받은 메세지를 Console Log에 출력하고 클라이언트로 환영 메세지를 보내는 역할을 한다.

//import 생략

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

    private final ObjectMapper objectMapper;
    private final ChatService chatService;

   @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);
    }
}

Websocket Config 작성

위에서 만든 handler를 이용하여 Websocket을 활성화하기 위한 Config파일을 작성한다. @EnableWebSocket을 선언하여 Websocket을 활성화한다. Websocket에 접속하기 위한 endpoint는 /ws/chat으로 설정하고 도메인이 다른 서버에서도 접속 가능하도록 CORS : setAllowedOrigins("*")를 추가해준다. 이제 클라이언트가 ws://localhost:8080/ws/chat으로 커넥션을 연결하고 메세지 통신을 할 수 있는 기본적인 준비가 끝이 났다.

//import 생략

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

    private final WebSocketChatHandler webSocketChatHandler;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketChatHandler, "/ws/chat").setAllowedOrigins("*");
    }
}

Websocket 테스트

Websocket 테스트를 위한 클라이언트 웹 화면이 아직 없으므로 Chrome 웹스토어에서 Simple Websocket Client를 검색해 설치한다.

채팅 고도화

위에서 만든 websocket 통신은 ws://localhost:8080/ws/chat에 연결된 클라이언트끼리만 메세지 통신이 가능하다. 간단히 말해 채팅방이 하나뿐인 채팅 서버인 것이다. 여러 개의 채팅방을 만들어서 해당 채팅방에 입장한 클라이언트들 간에 메세지를 교환하려면 고도화가 필요하다. 그래서 다음과 같은 컨셉으로 채팅방을 구현해 보겠다.

클라이언트들은 서버에 접속하면 개별의 Websocket Session을 가지게 된다. 따라서 채팅방에 입장 시 클라이언트들의 Websocket Session 정보를 채팅방에 매핑시켜서 보관하고 있으면 서버에 전달된 메세지를 특정 방의 Websocket 세션으로 보낼 수 있으므로 개별의 채팅방을 구현할 수 있다.

채팅 메세지 구현

채팅 메세지를 주고받기 위한 DTO를 만든다.

@Getter
@Setterpse
public class ChatMessage {
    public enum MessageType {
        ENTER, TALK
    }

    private MessageType messageType;
    private String id;
    private String sender;
    private String message;
}

채팅방 구현

채팅방을 구현하기 위해 DTO를 만들어준다. 채팅방은 입장한 클라이언트들의 정보를 가지고 있어야 하므로 WebsocketSession 정보 리스트를 멤버 필드로 갖는다. 나머지 멤버 필도로 채팅방 id, 채팅방 이름을 추가한다. 채팅방에서는 입장, 대화하기 기능이 있으므로 handleAction을 통해 Message Type에 따라 분기 처리한다. 입장 시에는 채팅방의 session 정보에 클라이언트의 session 리스트에 추가해 놓았다가 채팅방에 메세지가 도착할 경우 채팅방의 모든 session에 메세지를 발송한다.

//import 생략

@Getter
public class ChatRoom {

    private String roomId;
    private String name;
    private HashSet<WebSocketSession> sessions = new HashSet<>();

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

    public void handleActions(WebSocketSession session, ChatMessage chatMessage, ChatService chatService) {
        if (chatMessage.getType().equals(ChatMessage.MessageType.ENTER)) {
            sessions.add(session);
            chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다 👋🏼");
        }
        sendMessage(chatMessage, chatService);
    }

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

채팅 서비스 구현

채팅방을 생성, 조회하고 하나의 세션에 메세지 발송을 하는 서비스를 아래와 같이 구현한다. 채팅방 Map은 서버에 생성된 모든 채팅방의 정보를 모아둔 구조체이다. 채팅방의 정보 저장은 빠른 구현을 위해 일단 외부 저장소를 이용하지 않고 HashMap에 저장한다.

  • 채팅방 조회: 채팅방 Map에 담긴 정보를 조회
  • 채팅방 생성: Random UUID로 구별 ID를 가진 채팅방 객체를 새엇ㅇ하고 채팅방 Map에 추가
  • 메세지 발송: 지정한 Websocket세션에 메세지를 발송
//import 생략

@Slf4j
@RequiredArgsConstructor
@Service
public class ChatService {

    private final ObjectMapper objectMapper;

    //서버에 생성된 모든 채팅방의 정보를 모아둔 구조체
    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 randomId = UUID.randomUUID().toString();
        ChatRoom chatRoom = ChatRoom.builder()
                .roomId(randomId)
                .name(name)
                .build();

        chatRooms.put(randomId, chatRoom);
        return chatRoom;
    }

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

채팅 컨트롤러 구현

//import 생략

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

    private final ChatService chatService;

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

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

Handler 수정

위에서 만든 채팅 로직을 handler에 추가한다.

  • 웹소켓 클라이언트로부터 채팅 메세지를 전달받아 채팅 메세지 DTO 객체로 변환
  • 전달받은 메세지에 담긴 채팅방 ID로 발송 대상 채팅방 정보 조회
  • 해당 채팅방에 입장해있는 모든 클라이언트들(Websocket session)에게 타입에 따른 메세지 발송
//import 생략

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

    private final ObjectMapper objectMapper;
    private final ChatService chatService;

   @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);

       ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
       ChatRoom room = chatService.findRoomById(chatMessage.getRoomId());
       room.handleActions(session, chatMessage, chatService);
    }
}

테스트

채팅방 생성

Postman으로 채팅방을 생성한다.

채팅방 입장

아래와 같이 JSON을 구성하여 websocket으로 발송한다. roomId는 위에서 얻은 roomId를 입력한다. 이 때 해당 roomId의 채팅방의 session 리스트에 클라이언트의 websocket session을 저장하는 작업이 수행된다.

{
  "type":"ENTER",
  "roomId":"1ba86a43-99b1-4adf-84ec-38909aa25e55",
  "sender":"계환",
  "message":""
}

입장 메세지 JSON을 입력한 후 웹소켓 서버로 전달하면 채팅방의 session 리스트에 있는 클라이언트 session에 입장 환영 메세지를 보낸다.

채팅 메세지 발송

채팅 메세지 발송은 Message Type을 'TALK'으로 해서 발송한다. 'ENTER'와의 차이점은 ENTER는 입장한 클라이언트의 웹소켓 세션을 채팅방에 저장하는 작업이 있고 TALK은 이미 입장한 클라이언트에게 메세지를 보내는 요청이므로 채팅방이 가지고 있는 클라이언트 session 리스트를 조회하여 메세지를 발송하는 액션만을 수행한다.

{
  "type":"TALK",
  "roomId":"1ba86a43-99b1-4adf-84ec-38909aa25e55",
  "sender":"계환",
  "message":"안녕하세요 ~"
}

Simple Websocket Client를 두 개 띄워서 실시간 채팅 테스트가 가능하다.

0개의 댓글

관련 채용 정보