실시간 채팅 구현 - WebSocket과 HTTP API를 같이 사용해야 하는 이유

seobin·2025년 3월 6일

일단 난 websocket을 어떻게 구현하는지 모르고 뛰어들었다.
http pollig 방식?도 있다는데 공부하지 않은 채로 냅다 gpt와 함께 개발을 시작했다.
물론, websocket을 사용하는 이유 등 간단한 이론적인 부분은 학습한 후 개발을 시작했다.

💡 MessageController

package com.meossamos.smore.domain.chat.message.controller;

import com.meossamos.smore.domain.chat.message.dto.ChatMessageRequestDto;
import com.meossamos.smore.domain.chat.message.dto.ChatMessageResponseDto;
import com.meossamos.smore.domain.chat.message.entity.ChatMessage;
import com.meossamos.smore.domain.chat.message.service.ChatMessageService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;

@RestController
@RequestMapping("/api/chatrooms")
@RequiredArgsConstructor
public class MessageController {

    private final ChatMessageService chatMessageService;

    /**
     * DM 메시지 전송
     * POST /api/chatrooms/dm/{roomId}/messages
     */
    @PostMapping("/dm/{roomId}/messages")
    public ResponseEntity<ChatMessageResponseDto> sendDmMessage(
            @PathVariable("roomId") String roomId,
            @RequestBody ChatMessageRequestDto messageRequestDto) {

        ChatMessage savedMessage = chatMessageService.saveChatMessage(
                roomId,
                messageRequestDto.getSenderId(),
                messageRequestDto.getMessage(),
                messageRequestDto.getAttachment()
        );

        ChatMessageResponseDto response = ChatMessageResponseDto.builder()
                .messageId(savedMessage.getId())
                .roomId(savedMessage.getRoomId())
                .senderId(savedMessage.getSenderId())
                .message(savedMessage.getMessage())
                .attachment(savedMessage.getAttachment())
                .timestamp(savedMessage.getCreatedDate() != null ? savedMessage.getCreatedDate() : LocalDateTime.now())
                .build();

        return ResponseEntity.ok(response);
    }

    /**
     * 그룹 채팅 메시지 전송
     * POST /api/chatrooms/group/{roomId}/messages
     */
    @PostMapping("/group/{roomId}/messages")
    public ResponseEntity<ChatMessageResponseDto> sendGroupMessage(
            @PathVariable("roomId") String roomId,
            @RequestBody ChatMessageRequestDto messageRequestDto) {

        ChatMessage savedMessage = chatMessageService.saveChatMessage(
                roomId,
                messageRequestDto.getSenderId(),
                messageRequestDto.getMessage(),
                messageRequestDto.getAttachment()
        );

        ChatMessageResponseDto response = ChatMessageResponseDto.builder()
                .messageId(savedMessage.getId())
                .roomId(savedMessage.getRoomId())
                .senderId(savedMessage.getSenderId())
                .message(savedMessage.getMessage())
                .attachment(savedMessage.getAttachment())
                .timestamp(savedMessage.getCreatedDate() != null ? savedMessage.getCreatedDate() : LocalDateTime.now())
                .build();

        return ResponseEntity.ok(response);
    }
}

💡 WebsocketController

package com.meossamos.smore.domain.chat.message.controller;

import com.meossamos.smore.domain.chat.message.dto.ChatMessageRequestDto;
import com.meossamos.smore.domain.chat.message.dto.ChatMessageResponseDto;
import com.meossamos.smore.domain.chat.message.entity.ChatMessage;
import com.meossamos.smore.domain.chat.message.service.ChatMessageService;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import java.time.LocalDateTime;

@Controller
@RequiredArgsConstructor
public class WebSocketController {

    private final ChatMessageService chatMessageService;

    /**
     * 클라이언트가 "/app/chat.sendMessage"로 메시지를 전송하면
     * 이 메서드가 실행되어 메시지를 저장한 후 "/topic/chatroom"로 브로드캐스트함.
     */
    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/chatroom")
    public ChatMessageResponseDto sendMessage(ChatMessageRequestDto messageDto) {
        // 1. 메시지 저장
        ChatMessage savedMessage = chatMessageService.saveChatMessage(
                messageDto.getRoomId(),
                messageDto.getSenderId(),
                messageDto.getMessage(),
                messageDto.getAttachment()
        );

        // 2. 저장된 엔티티를 Response DTO로 변환하여 응답
        ChatMessageResponseDto response = ChatMessageResponseDto.builder()
                .messageId(savedMessage.getId())
                .roomId(savedMessage.getRoomId())
                .senderId(savedMessage.getSenderId())
                .message(savedMessage.getMessage())
                .attachment(savedMessage.getAttachment())
                // 저장된 생성일이 있다면 사용하고, 없으면 현재 시간을 사용
                .timestamp(savedMessage.getCreatedDate() != null ? savedMessage.getCreatedDate() : LocalDateTime.now())
                .build();

        return response;
    }
}

위의 백엔드 코드를 봤을 때, Chat.tsx에서 엔드포인트를 어떻게 설정하면 좋을까?라는 생각을 하게 되었다.

두 방식의 특징은 아래와 같다.
1. HTTP API 방식 → axios.post()로 메시지 전송
2. WebSocket 방식 → WebSocket이나 Stomp.js로 실시간 채팅 구현

근데, 채팅 구현시 기존 메세지를 불러와야 하는 기능은 꽤 중요한 기능인데, WebSocket만 사용하면 기존 메시지(채팅 히스토리)를 불러올 수 없음을 알게 되었다.
WebSocket은 서버와 클라이언트 간의 실시간 메시지 전송을 위한 프로토콜이므로, 클라이언트가 연결된 이후에 발생하는 이벤트만 받을 수 있기 때문이다.

즉, WebSocket은 "과거 데이터"를 제공하지 않고, "현재 이후의 이벤트"만 처리한다.
따라서 기존 메시지(과거 채팅 기록)를 보고 싶다면 HTTP API를 함께 사용해야 한다.

0개의 댓글