Dev_Henry

[Spring] 소켓통신 이용한 채팅 구현하기 1 본문

Web/Spring

[Spring] 소켓통신 이용한 채팅 구현하기 1

데브헨리 2023. 7. 21. 15:55
728x90

 

스프링에서 지원하는 웹소켓을 사용하기 위해서는 아래 라이브러리를 사용한다.

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

소켓통신을 이용하여 채팅을 구현하는 방법을 찾아보면 크게 2가지 방식이 있는데 이번 글에서는 WebSocketConfigurer을 구현하여 소켓을 직접 처리하는 방법을 다룬다.

 

먼저 소켓 사용을 위한 설정파일을 만든다.

해당방법은 직접 소켓처리를 하는방법이기 때문에 핸들러를 등록해야한다.

핸들러를 등록할때 소켓에 접속하기위한 경로 ("/ws")를 함께 설정해주고, 다른곳에서 접속이 가능하도록 .setAllowedOrigins("*")을 붙여 cors문제를 해결한다.

@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    private final WebSocketHandler webSocketHandler;

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

 

스프링에서 지원하는 소켓 핸들링 방식은 텍스트와 바이너리 두가지로 각각 TextWebSocketHandler ,BinaryWebSocketHandler 로 지원한다.

채팅을 위해서는 텍스트가 적합하므로 TextWebSocketHandler를 상속받는 핸들러를 작성해준다.

@Slf4j
@Component
public class WebSockChatHandler extends TextWebSocketHandler {

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

        TextMessage textMessage = new TextMessage("hello socket");
        session.sendMessage(textMessage);
    }
}
 

먼저 소켓접속이 잘 되는지 테스트를 위해 간단히 요청을 받으면 항상 "hello socket"을 응답해주도록 만들어보자.

 

Websocket을 테스트할 때는 크롬의 확장 프로그램중 simple websocket client를 설치해 테스트한다.

경로를 입력해서 연결후 요청으로 아무 메시지나 보내면 정상적으로 "hello socket"을 응답하는 것을 확인할 수 있다.

 

이제 진짜 채팅기능을 사용하기 위해서 DTO를 만들어준다.

@Getter
@Setter
public class ChatMessage {
    public enum MessageType {
        ENTER, TALK
    }
    private MessageType type; // 메시지 타입
    private String roomId; // 방번호
    private String sender; // 메시지 보낸사람
    private String message; // 메시지
}
 
@Getter
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 handlerActions(WebSocketSession session, ChatMessage chatMessage, ChatService chatService) {
        if (chatMessage.getType().equals(ChatMessage.MessageType.ENTER)) {
            sessions.add(session);
            chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다.");
        }
        sendMessage(chatMessage, chatService);

    }

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

채팅방은 방번호와 이름, 그리고 해당 채팅방에 참여한 세션들을 관리하기 위한 목록을 가진다.

handlerActions메서드를 통해 메시지를 받으면 해당 메시지의 타입별로 입장안내 혹은 메시지를 전송시킨다.

 

다음으로는 실제 메시지 전송 기능을 처리하기위한 ChatService와 요청을 받기위한 Controller를 만든다.

@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);
        }
    }
}
 
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {
    private final ChatService chatService;

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

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

여기까지 했다면 기능구현은 끝이다. 마지막으로 소켓 핸들러를 수정해준다.

@Slf4j
@RequiredArgsConstructor
@Component
public class WebSockChatHandler 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);

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

이제 구현은 모두 끝났다.

/chat 경로로 채팅방 이름과 함께 post요청을 날리면 방이 생성되고

 

소켓을 연결한 뒤에

{
  "type":"ENTER",
  "roomId":"채팅방 id",
  "sender":"보내는이 이름",
  "message":"채팅내용"
}
 

와 같은 형식으로 요청을 보내면 해당 채팅방에 접속되고 type을 TALK로 보내면 메시지가 전달되는것을 확인할 수 있다.

 

728x90
반응형