사이먼's 코딩노트

[SpringBoot] 멀티 채팅방 (3) 본문

Java/SpringBoot

[SpringBoot] 멀티 채팅방 (3)

simonpark817 2024. 6. 19. 21:51

[Ajax, Stomp 기반 멀티 채팅방 구현]

  • 저번 '멀티 채팅방 (2)' 포스팅에서 Ajax 기반의 멀티 채팅방을 구현하였다.
  • 하지만, Ajax를 적용했을 때 불필요한 데이터 전달때문에 서버 부하가 생길 수도 있다는 단점이 있다.
  • 해당 부분을 해결하기 위해서 웹 소켓을 사용해 볼 예정입니다.
  • 웹 소켓을 사용하면 브라우저와 서버간에 서로 데이터 전달을 하지 않고, 브라우저는 서버에게만 전달, 서버는 브라우저에게만 전달하는 형식이기 때문에 부하가 생기는 단점을 막을 수 있다.
  • Stomp가 바로 그 웹 소켓 방식 중 하나라고 생각하면 좋다.

 

[Stomp란?]

  • Simple/Stream Text Oriented Message Protocol의 약어로 웹 소켓 위에서 동작하는 문자 기반 메세징 프로토콜로써 클라이언트와 서버가 전송할 메시지의 유형, 형식, 내용들을 정의하는 매커니즘이다.
  • TCP와 웹 소켓과 같은 신뢰할 수 있는 양방향 스트리밍 네트워크 프로토콜에서 사용할 수 있다.
  • 기본적으로 Publisher / Subscriber 구조로 되어있어 메시지를 전송하고 받아 처리하는 부분이 확실하게 정해져있다.
  • 메시지의 발행자와 구독자가 존재하고 메시지를 보내는 사람과 받는 사람이 구분되어 있다고 생각하면 좋다.
  • 메시지 브로커는 발행자가 보낸 메시지를 구독자에게 전달해주는 역할을 한다. 
  • Http와 마찬가지로 frame 기반 프로토콜 command, header, body로 이루어져 있다.
  • 웹 소켓과 Stomp를 함께 사용하면 frame의 구조가 정해져있기 때문에 통신에 용이하다.
  • 아래 사진을 보면 알 수 있듯이, 수신자는 topic 경로를 구독하고 있고 발행자는 app 또는 topic으로 메시지를 보내고, 만약 발행자가 topic 경로로 메시지를 보내면 바로 수신자에게 도착하고 app 경로로 메시지를 보내면 가공을 한 다음 보내게 된다.

Stomp 통신 과정 (출처 : https://growth-coder.tistory.com/157)

 

[라이브러리 추가]

  • Stomp를 사용하기 위해서는 해당 라이브러리를 build.gradle에 아래와 같이 추가해준다.
// web socket
implementation 'org.springframework.boot:spring-boot-starter-websocket'

 

[WebSocketConfig 생성]

  • 다음은 웹 소켓에 대한 기본 설정을 위해 WebSocketConfig.java 클래스를 생성하여 아래와 같이 코드를 작성한다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

 

[room.html 수정]

  • 다음은 실시간 채팅이 실제로 이뤄지는 화면인 room.html에서 웹 소켓이 적용되지 않는 브라우저가 있기 때문에 임시적으로 상단에 script를 아래와 같이 추가해준다.
  • 또한 Ajax 방식은 이제 사용하지 않을 예정이기 때문에 전에 작성한 관련된 script는 모두 삭제해주고 Stomp와 관련된 script를 추가해준다.
  • 가장 하단에 추가 작성된 스크립트를 살펴보면 SockJS를 통해 서버와 웹 소켓을 연결하고, stompClient를 소켓 위에 생성한다.
  • connect를 통해 Stomp와 연결하고 연결에 성공했다면 콘솔창을 통해 콜백하도록 구현했다.
  • 마지막엔 특정 메시지 생성 이벤트인 /topic/chat/room/${roomId}/messageCreated를 구독하고, 메시지를 JSON 형식으로 변환하여 변환된 메시지를 다시 메시지 화면에 그려주도록 하였다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>

<h1 th:text="|${roomId}번 채팅방|"></h1>

<div>
    <a href="make">채팅방 생성</a>
    <a href="list">채팅방 목록</a>
</div>

<form onsubmit="submitWriteForm(this); return false;" method="POST">
    <input type="text" name="writerName" placeholder="작성자 명">
    <input type="text" name="content" placeholder="내용">
    <input type="submit" value="작성">
</form>

<div>
    <div>
        <button onclick="loadMoreChatMessages();" type="button">더 불러오기</button>
    </div>
    <ul class="chat__messages">
        <li th:each="chatMessage : ${room.chatMessages}">
            <th:block th:text="${chatMessage.writerName}"></th:block>
            :
            (
            <th:block th:text="${chatMessage.id}"></th:block>
            )
            <th:block th:text="${chatMessage.content}"></th:block>
        </li>
    </ul>
</div>

<script th:inline="javascript">
    const roomId = /*[[${roomId}]]*/ 0;
    let lastChatMessageId = /*[[${room.chatMessages.size > 0 ? room.chatMessages.first.id : 0}]]*/ 0;
</script>

<script>
    const chatMessagesEl = document.querySelector(".chat__messages");

    function submitWriteForm(form) {
        form.writerName.value = form.writerName.value.trim();

        if ( form.writerName.value.length == 0 ) {
            alert("작성자 명을 입력해주세요.");
            form.writerName.focus();
            return;
        }

        form.content.value = form.content.value.trim();

        if ( form.content.value.length == 0 ) {
            alert("내용을 입력해주세요.");
            form.content.focus();
            return;
        }

        const action = `/chat/room/${roomId}/write`;

        fetch(
            action,
            {
                method: 'POST',
                headers:  {
                    'accept' : 'application/json',
                    'Content-Type' : 'application/json',
                },
                body: JSON.stringify({
                    writerName: form.writerName.value,
                    content: form.content.value,
                }),
            }
        ).catch(error => alert(error));

        form.content.value = '';
        form.content.focus();
    }


    function drawMoreChatMessage(message) {
        lastChatMessageId = message.id;
        console.log(lastChatMessageId);

        chatMessagesEl
            .insertAdjacentHTML(
                "afterBegin",
                `<li>${message.writerName} : ( ${message.id} ) ${message.content}</li>`
            );
    }

    const socket = new SockJS('/ws');
    const stompClient = Stomp.over(socket);

    stompClient.connect({}, function (frame) {
        console.log('Connected: ' + frame);

        stompClient.subscribe(`/topic/chat/room/${roomId}/messageCreated`, function (data) {
            const jsonData = JSON.parse(data.body);
            drawMoreChatMessage(jsonData.data.message);
        });
    });
</script>

 

[ChatRoomController 수정]

  • 다음은 ChatRoomController.java 클래스에서 wrtie() 메서드에 웹 소켓 관련 코드를 아래와 같이 추가해준다.
  • 기존 wrtie() 메서드 아래에 작성했던 메서드들은 Ajax를 사용하기 위해 작성했던 코드기 때문에 지금은 삭제해도 무방하다.
  • 이 때, messagingTemplate의 convertAndSend() 메서드를 호출하기 위해선 SimpMessagingTemplate를 반드시 선언해줘야한다.
  • room.html에서 작성한 스크립트를 보면 화면에 메시지를 그려주는 부분인 drawMoreChatMessage() 메서드에서 인자로 JSON 형식으로 변환한 jsonData.data.message를 컨트롤러에서도 호환이 되도록 WriteResponseBody() 메서드에서 변수 선언을 message로 맞춰야한다.
  • 모든 코드를 작성하면 Ajax 방식과 같이 실시간 채팅이 원할하게 이뤄지는 것을 확인할 수 있다.
@Controller
@RequestMapping("/chat/room")
@RequiredArgsConstructor
public class ChatRoomController {
    private final ChatRoomService chatRoomService;
    private final ChatMessageService chatMessageService;
    private final SimpMessagingTemplate messagingTemplate;

    @GetMapping("/{roomId}")
    public String showRoom(
            @PathVariable("roomId") final long roomId,
            @RequestParam(value = "writerName", defaultValue = "NoName") final String writerName,
            Model model
    ) {
        ChatRoom room = chatRoomService.findById(roomId).get();
        model.addAttribute("room", room);

        return "domain/chat/chatRoom/room";
    }

    @GetMapping("/make")
    public String showMake() {
        return "domain/chat/chatRoom/make";
    }

    @PostMapping("/make")
    public String make(
            @RequestParam(value = "name") final String name
    ) {
        chatRoomService.make(name);
        return "redirect:/chat/room/list";
    }

    @GetMapping("/list")
    public String showList(Model model) {
        List<ChatRoom> chatRooms = chatRoomService.findAll();
        model.addAttribute("chatRooms", chatRooms);
        return "domain/chat/chatRoom/list";
    }

    @Getter
    @Setter
    public static class WriteRequestBody {
        private String writerName;
        private String content;
    }

    @Getter
    @AllArgsConstructor
    public static class WriteResponseBody {
        private ChatMessage message;
    }

    @PostMapping("/{roomId}/write")
    @ResponseBody
    public RsData<?> write(
            @PathVariable("roomId") final long roomId,
            @RequestBody final WriteRequestBody requestBody
    ) {
        ChatMessage chatMessage = chatRoomService.write(roomId, requestBody.getWriterName(), requestBody.getContent());

        RsData<WriteResponseBody> writeRs = RsData.of("S-1", "%d번 메시지를 작성하였습니다.".formatted(chatMessage.getId()), new WriteResponseBody(chatMessage));

        messagingTemplate.convertAndSend("/topic/chat/room/" + roomId + "/messageCreated", writeRs);

        return RsData.of("S-1", "성공");
    }
}

 

 

 

반응형

'Java > SpringBoot' 카테고리의 다른 글

[SpringBoot] 실전 서비스 배포 (1)  (2) 2024.07.24
[SpringBoot] 결제 시스템  (0) 2024.07.18
[SpringBoot] 멀티 채팅방 (2)  (0) 2024.06.19
[SpringBoot] 멀티 채팅방 (1)  (2) 2024.06.19
[SpringBoot] REST API (4)  (0) 2024.06.19