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

윤재열·2022년 9월 17일
3

Spring

목록 보기
66/72
post-custom-banner

WebSocket 구현 테스트

1. 라이브러리 추가

2. 채팅 메시지 클래스 : ChatDTO

  • 채팅 메시지에 대한 정보를 담는 클래스 : 채팅 내용에 대한 DTO

  • 채팅 내용은 크게 들어오는 사람에 대한 환영 메시지에 대한 ENTER과 방에 있는 사람들이 채팅을 칠 때 사용하는 TALK 두 가지로 메시지 타입을 나눕니다. 이때 타입은 ENUM으로 선언합니다.

  • 다음으로 어떤 방에서 채팅이 오가는지 확인하기 위한 방번호,채팅을 보낸 사람,메시지,채팅발송 시간 등을 변수로 선언합니다.

    • 여기서 더 나가면 ENTER,TALK 뿐만 아니라 OUT 으로 메시지 타입을 추가해서 나가는 사람에 대한 메시지를 전달해도 좋을것 같습니다.
package org.codej.websocket.domain;

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; // 채팅 발송 시간

}

3.채팅 서비스 : ChatService

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

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.codej.websocket.domain.ChatRoom;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

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 을 Build
        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);
        }
    }



}

4.채팅방 클래스 : ChatRoom

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

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

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(sessions -> service.sendMessage(sessions,message));
    }
}

페이로드(paylaod)란?

  • 페이로드란 전송되는 데이터를 의미합니다.

  • 데이터를 전송 할 때, Header와 META데이터, 에러체크 비트 등과 같은 다양한 요소들을 함께 보내 데이터 전송 효율과 안정성을 높히게 됩니다.

  • 이때, 보내고자 하는 데이터 자체를 의미하는 것이 페이로드입니다.

  • 예를 들어 택배 배송을 보내고 받을 때 택배 물건이 페이로드고 송장이나 박스 등은 부가적인 것이기 때문에 페이로드가 아닙니다.

  • 다음 JSON에서 페이로드는 'data'입니다. 나머지는 통신을 하는데 있어 용이하게 해주는 부가적 정보들입니다.

{
"status":
"from":"localhost",
"to":"http://codej.com/chatroom/1",
"method":"GET",
"data":{"message":"Welcome my room!"}
}

5. WebSocket Handler

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

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.codej.websocket.domain.ChatDto;
import org.codej.websocket.domain.ChatRoom;
import org.codej.websocket.service.ChatService;
import org.springframework.stereotype.Component;
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.util.ArrayList;
import java.util.List;

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

    private  final ObjectMapper mapper;
    private final ChatService service;

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        String payload = (String) message.getPayload();
        log.info("payload : {}",payload);

//        TextMessage intialGretting = new TextMessage("Welcome to Chat Server");
        //JSON -> Java Object
        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);

    }
    /** Client가 접속 시 호출되는 메서드*/
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info(session + " 클라이언트 접속");
    }
    /** client가 접속 시 호출되는 메서드*/
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        log.info(session + " 클라이언트 접속 해제");
    }
}

6. WebSocket Config

  • 핸들러를 이용하여 WebSocket을 활성화하기 위한 Config를 작성합니다.
  • @EnableWebSocket어노테이션을 사용하여 WebSocket을 활성화 하도록 합니다.
  • WebSocket에 접속하기 위한 Endpoint는 /chat으로 설정합니다.
  • 도메인이 다른 서버에서도 접속 가능하도록 CORS : setAllowedOprigins(" * ");를 추가해줍니다.
  • 이제 클라이언트가 ws:localhost:8080/chat으로 커넥션을 연결하고 메세지 통신을 할 수 있는 준비를 마쳤습니다.
package org.codej.websocket.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.codej.websocket.handler.ChatHandler;
import org.springframework.context.annotation.Configuration;
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
@RequiredArgsConstructor
@EnableWebSocket
@Slf4j
public class WebSocketConfig implements WebSocketConfigurer {
    
    // WebSocketHandler 에 관한 생성자 추가
    private final ChatHandler chatHandler;

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

Endpoint 와 API의 차이

  • API는 두 시스템(어플리케이션)이 상호작용(소통) 할 수 있게 하는 프로토콜의 총 집합이라면, Endpoint는 API가 서버에서 리소스에 접근할 수 있도록 하는 URL입니다.
  • 메서드는 같은 URL들에 대해서도 다른 요청을 하게끔 구별하게 해주는 항목, 각각 GET,PUT,DELETE 메서드에 따라 다른 요청을 하는 것을 알 수 있습니다.
  • Endpoint란 API가 서버에서 자원(resource)에 접근할 수 있도록 하는 URL 입니다.

CORS 란?

  • 교차 출처 리소스 공유(Cross-Origin Resource Sharing,CORS)는 추가 HTTP 헤더를 사용하여 한 출처에서 실행 중인 웹 어플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 부라우저에 알려주는 체제입니다.
  • 웹어플리케이션은 리소스가 자신의 출처(domain,protocol,port)와 다를 시에 교차 출처 HTTP 요청을 실행합니다. 이에 대한 응답으로 서버는 Access-Control-Allow-Origin 헤더를 다시 보냅니다.

7. ChatController

package org.codej.websocket.controller;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.codej.websocket.domain.ChatRoom;
import org.codej.websocket.service.ChatService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

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

8.코드 구현 확인

  • 크롬 확장프로그램인talend api tester와 postman을 사용하여 roomid를 얻어와줍니다.

  • 작동이 잘 되는 것을 알 수 있습니다.

profile
블로그 이전합니다! https://jyyoun1022.tistory.com/
post-custom-banner

0개의 댓글