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