티스토리 뷰

동일한 방을 구독하고 있는 팀원들에 대해서 아이템 구매 상황스펠 체크 상황의 실시간 공유를 해야하는데 이를 위해서 WebSocket을 사용하기로 했다.

  • 스프링 웹 애플리케이션이므로 SockJS + STOMP를 이용해서 웹소켓을 구현.
  • 프론트쪽과 SockJS로 연결을 하게 된다.
  • 복수의 방들이 생성될 수 있으므로 여러개의 통신을 위한 방들이 생성되어야 한다.
  • 각 방들의 엔드포인트는 [matchId + 진영 코드]로 한다.

이전에 웹소켓 정리한 글 참고 : [Web/정리글] - HTTP vs. WebSocket 정리

 

일단 먼저 블로그를 참고해서 웹소켓을 이용한 채팅 서비스를 구현해보고 이를 바탕으로 만들고자 하는 서비스를 구현하도록하겠다.

1. 웹소켓을 이용한 채팅 구현

 

의존성 추가

// webSocket 의존성 , freemarker, vue, stomp, sockjs
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-freemarker'
implementation 'org.springframework.boot:spring-boot-starter-devtools'
implementation 'org.webjars.bower:bootstrap:4.3.1'
implementation 'org.webjars.bower:vue:2.5.16'
implementation 'org.webjars.bower:axios:0.17.1'
implementation 'org.webjars:sockjs-client:1.1.2'
implementation 'org.webjars:stomp-websocket:2.3.3-1'
implementation 'com.google.code.gson:gson:2.8.0'

 

WebSocketHandler 클래스 작성

소켓통신은 기본적으로 서버와 클라이언트간에 1 : N 관계를 맺는다.

  • 따라서 한 서버에 여러 클라이언트가 접속할 수 있으므로, 서버는 여러 클라이언트가 발송한 메시지를 받아서 처리해줄 Handler가 필요하다.
  • TextWebSocketHandler를 상속받아서 Handler를 작성하자.
@Slf4j
@Component
public class SwoomiWebSocketHandler extends TextWebSocketHandler {

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

        TextMessage initialGreeting = new TextMessage("Welcome to Swoomi Chat Server ~O_O~");
        session.sendMessage(initialGreeting);
    }
}

현재 Handler는 Client로 부터 받은 메시지를 log로 출력하고 Client로 환영 메시지를 보내는 역활을 한다.

 

WebSocket Config 클래스 작성

위에서 작성한 Handler를 이용해서 WebSocket을 활성화하기 위한 Config 파일을 작성한다.

  • @EnableWebSocket을 선언하여 WebSocket을 활성화시킨다.
  • WebSocket에 접속하기 위한 endpoint는 "/ws/chat"으로 설정하고, 도메인이 다른 서버에서도 접속 가능하도록 CORS : setAllowedOrigins("*")를 설정에 추가한다.
  • 클라이언트가 ws://localhost:8070/ws/chat으로 커넥션을 연결하고 메시지 통신을 할 수 있는 기본적인 준비이다.
@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    private final SwoomiWebSocketHandler webSocketHandler;

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

 

WebSocket 테스트

webSocket 테스트를 위한 클라이언트 웹 화면을 만들지 않았으므로 Chorme Extentions를 통해 테스트해보자.

 

  1. Boot 서버를 구동한다.
  2. URL에 ws://localhost:8070/ws/chat을 입력해서 open을 누른다.
    • (참고) websocket은 http가 아닌 ws로 시작하는 주소체계를 독자적으로 갖는다 (wss == https)

 

성공적으로 연결된다면 OPENED가 밑에 뜬다.

 

요청 보내기

연결된 상태에서 Request에 메시지를 입력하고 [Send]를 누르면 Message Log에 클라이언트가 보낸 메시지가 주황색으로 출력된다.

서버에서 이에 대한 응답으로 준 메시지는 검은색 문자로 출력된다.

 

현재는 WebSocket을 이용해서 매우 간단한 client-server 통신을 구현해보았다. 하지만 지금은 [ws:://localhost:8070/ws/chat]에 연결된 클라이언트끼리만 메시지 통신이 가능한 경우이다.

 

즉, 채팅방이 단 하나뿐인 채팅서버인 것이다. 따라서 여러개의 채팅방을 만들어서 각 채팅방에 입장한 클라이언트 끼리만 메시지를 교환하려면 고도화가 필요하다.

 

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

 


2. 여러개의 채팅방 만들기 (고도화)

 

채팅 메시지를 주고받기 위한 DTO

  • 채팅방 입장, 채팅방에 메시지 보내기 두가지 상황에 맞춰서 enum을 구현
  • 방번호, 보내는이, 내용 포함
@Getter
@Setter
public class Message {
    public enum MessageType {
        ENTER, COMM
    }

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

 

채팅방을 위한 DTO

  • 채팅방은 현재 방에 입장한 클라이언트의 Session 정보를 가져야 한다.
  • 채팅방 id (== 추후 MatchId + 진영 코드 + 소환사 명을 구별 ID로 한다)
  • 채팅방에는 입장 / 통신 기능이 있으므로 handleAction을 통해 분기 처리한다.
  • 입장시에는 채팅방의 session정보 리스트에 클라이언트의 session을 추가해놓고, 채팅방에 메시지가 도착할 경우 채팅방의 모든 session에 메시지를 발송하면 된다.
@Getter
@Setter
public class MsgRoom {
    private String roomId;
    private Set<WebSocketSession> sessions = new HashSet<>();

    @Builder
    public MsgRoom(String roomId) {
        this.roomId = roomId;
    }

    public void handleActions(WebSocketSession session, Message message, MsgService msgService) {
        if (message.getMessageType().equals(Message.MessageType.ENTER)) {
            sessions.add(session);
            message.setMessage(message.getSender() + "님이 입장했습니다.");
        }
        sendMessage(message, msgService);
    }

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

 

메시징 서비스 구현

채팅방을 생성, 조회하고 하나의 세션에 메시지 발송을 하는 서비스를 구현한다.

  • 채팅방 Map은 서버에 생성된 모든 채팅방의 정보를 모아둔 구조체.
  • 채팅방의 정보저장은 일단은 HashMap에 저장하도록 구현 (추후 DB로 옮겨야함)

채팅방 조회 : 채팅방 Map에 담긴 정보를 조회

채팅방 생성 : Random UUID로 구별 ID를 가진 채팅방 객체를 생성하고 채팅방 Map에 추가

  • 추후 MatchId + 진영 코드 + 소환사 명을 구별 ID로 변경

메시지 발송 : 지정한 Websocket 세션에 메시지를 발송

@Slf4j
@Service
@RequiredArgsConstructor
public class MsgService {
    private final ObjectMapper objectMapper;
    private Map<String, MsgRoom> msgRooms;

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

    public List<MsgRoom> findAllRoom() {
        return new ArrayList<>(msgRooms.values());
    }

    public MsgRoom findById(String roomId) {
        return msgRooms.get(roomId);
    }

    public MsgRoom createRoom(String name) {
        // TODO : roomId == Summoner MatchId + blue/red Team code + Summoner Name
        String roomId = name;
        return MsgRoom.builder().roomId(roomId).build();
    }

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

 

메시징 컨트롤러 구현

채팅방의 생성 및 조회는 REST API로 구현한다.

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

    private final MsgService msgService;

    @PostMapping
    public MsgRoom createRoom(@RequestParam String name) {
        // TODO : name is Summoner Name
        return msgService.createRoom(name);
    }

    @GetMapping
    public List<MsgRoom> findAllRoom() {
        return msgService.findAllRoom();
    }
}

 

WebSocket Handler 수정

  • 웹 소켓 클라이언트로 부터 메시지를 전달받아 Message 객체로 변환
  • 전달받은 Message 객체에 담긴 roomId로 발송 대상 채팅방 정보를 조회한다.
  • 해당 채팅방에 입장해 있는 모든 클라이언트들(Set<WebSocketSession>)에게 타입에 맞는 메시지를 전송
@Slf4j
@Component
@RequiredArgsConstructor
public class SwoomiWebSocketHandler extends TextWebSocketHandler {

    private final MsgService msgService;
    private final ObjectMapper objectMapper;

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

        Message msg = objectMapper.readValue(payload, Message.class);
        MsgRoom room = msgService.findById(msg.getRoomId());
        room.handleActions(session, msg, msgService);
    }
}

3. 테스트

채팅방 생성

name에 roomName을 전달한 결과 roomId가 roomName인 방이 생성된다.

 

채팅방 입장

{
  "type":"ENTER",
  "roomId":"roomName",
  "sender":"thisIsSummonerName",
  "message":""
}

위와 같은 json데이터를 만들어서 request메시지로 주면 objectMapper.readValue(payload, Message.class) 를 통해서 Message객체로 매핑이 이뤄지고, roomId를 꺼내서 Type에 따라

  • ENTER
  • COMM

으로 나뉘어서 진행된다.

현재는 ENTER 이므로 결과는 아래처럼 나온다.

 

메시지 전송

채팅방에 메시지를 보내기 위해서 TYPE을 COMM으로 변경한다.

  • type이 ENTER일 경우에는입장한 클라이언트의 웹소켓 세션을 채팅방 ID 내 Set<>에 저장하고, COMM인 경우에는 바로 message내용만 출력하게 된다.
  • 이때 message는 채팅방 Set<>에 존재하는 모든 세션에 보낸다.
{
  "type":"COMM",
  "roomId":"roomName",
  "sender":"thisIsSummonerName",
  "message":"hi nice to meet you"
}

 

두개의 클라이언트를 띄워서 양방향 통신을 테스트 해볼 수 있다.

 

웹 소켓을 이용한 채팅을 구현해보았는데 STOMP를 이용해서 한층 더 고도화하도록 하겠다.
반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday