Spring/개인공부_실습

[Spring]WebSocket과 STOMP을 이용한 실시간 채팅 및 채팅방 생성

빙응이 2024. 10. 11. 17:11

 

📝STOMP???

STOMP는 WebSocket 상에서 메시지를 전송하기 위한 프로토콜이다.
실시간 채팅, 알림, 주식 거래 등 실시간 통신이 필요한 앱에서 활용된다.

 

📌간단한 동작 로직

  • STOMP는 간단하게 5개의 단계로 나눌 수 있다.
1. 연결
  • 클라이언트가 서버에 WebSocket 연결을 요청한다.
2. 구독
  • 클라이언트는 STOMP를 통해 특정 채팅 방(예: /chat/{roomId})을 구독한다.
3. 메시지 전송
  • 클라이언트가 방에 메시지를 전송하면, 서버는 해당 방에 속한 모든 사용자에게 메시지를 브로드캐스트한다.
4. 메시지 수신
  • 구독한 클라이언트가 서버로부터 실시간 메시지를 수신하고 화면에 표시한다.
5. 연결 해제
  • 채팅이 끝나면 클라이언트는 WebSocket 연결을 해제하여 세션을 종료한다.

 

이러한 과정을 통해 STOMP는 데이터베이스 기반으로 채팅방의 참여자를 유지하고 채팅방에 자유 참여가 가능하다.

로직적으로 들어갈때 구독을 바꾸는 형식으로 할 수 있다,

 

 

📝예시 프로젝트 만들어보기

 

프로젝트 구조

  • Backend: Spring Boot, JPA, STOMP (WebSocket), Lombok, MariaDB
  • Frontend: Thymeleaf (HTML 템플릿), JavaScript (STOMP 클라이언트)

📌 의존성 설정

dependencies {
    // Spring WebSocket & STOMP
    implementation 'org.springframework.boot:spring-boot-starter-websocket'

    // Spring JPA & H2 Database (or MySQL/Postgres if needed)
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'

    // Thymeleaf
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

    // Lombok
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

📌 WebSocket , STOMP 설정하기

  • WebSocketConfig 설정
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/chat");  // 메시지를 구독할 경로 설정
        config.setApplicationDestinationPrefixes("/app");  // 클라이언트에서 메시지 보낼 경로
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();  // WebSocket 엔드포인트 설정
    }
}

하나하나씩 알아보자

 

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/chat");
    config.setApplicationDestinationPrefixes("/app");
}
  • 해당 부부은 STOMP 메시지 브로커의 설정을 담당한다.
    • enableSimpleBroker("/chat")
      • SimpleBroker는 서버에서 클라이언트로 메시지를 브로드캐스트할 경로를 설정한다.
      • 말 그대로 구독이다.
    • setApplicationDestinationPrefixes("/app")
      • 클라이언트가 서버로 메시지를 보낼 때 사용하는 경로의 prefix(접두사)를 설정한다.
      • 간단히 말하면 메시지를 보낼 경로이다.
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/ws").withSockJS();
}
  • 해당 부분은 WebSocket 연결 엔드포인트 설정이다.
    • addEndpoint("/ws")
      • 클라이언트는 이 /ws 엔드포인트를 통해 WebSocket 연결을 시작한다.
      • 테스트 환경에서는 http://localhost:8080/ws로 연결을 시도하면 된다.
    • withSockJS()
      • SockJS는 WebSocket을 사용할 수 없는 브라우저에서 대체 통신 방식(예: HTTP 폴링, XHR 등)을 제공하는 라이브러리이다.
      • 중요한 것은 WebSocket이 지원되지 않는 환경에서도 클라이언트가 SockJS를 사용하여 통신할 수 있도록 한다.

📌 채팅방, 채팅 메시지 Entity 설정하기

채팅방 ID와 방 이름을 저장하는 채팅방 엔티티
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChatRoom {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // 채팅방 ID

    private String roomId;  // STOMP에서 사용할 채팅방 식별자

    private String name;  // 채팅방 이름

    public static ChatRoom create(String name) {
        return ChatRoom.builder()
                .roomId(UUID.randomUUID().toString())  // 랜덤 ID 생성
                .name(name)
                .build();
    }
}

 

각 채팅방에 대한 메시지를 저장하는 엔티티
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChatMessage {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // 메시지 ID

    @ManyToOne
    @JoinColumn(name = "chat_room_id")
    private ChatRoom chatRoom;  // 어떤 채팅방의 메시지인지

    private String sender;  // 메시지 보낸 사람

    private String message;  // 메시지 내용

    private LocalDateTime timestamp;  // 메시지 전송 시간

    public static ChatMessage create(ChatRoom chatRoom, String sender, String message) {
        return ChatMessage.builder()
            .chatRoom(chatRoom)
            .sender(sender)
            .message(message)
            .timestamp(LocalDateTime.now())
            .build();
    }
}

 

 

서비스 및 리포지토리는 db에 저장하는 방식 그대로 이기에 적지 않겠습니다.

 

📝 요청 컨트롤러

@Controller
@RequiredArgsConstructor
public class ChatController {

    private final ChatRoomService chatRoomService;
    private final ChatMessageService chatMessageService;

    @MessageMapping("/chat/sendMessage/{roomId}")
    @SendTo("/chat/{roomId}")
    public ChatMessage sendMessage(@DestinationVariable String roomId, ChatMessageRequest request) {
        ChatRoom chatRoom = chatRoomService.findRoomById(roomId);
        return chatMessageService.saveMessage(chatRoom, request.sender(), request.message());
    }

}
  • 해당 컨트롤러는 STOMP 기반의 실시간 채팅 기능을 제공하는 컨트롤러이다.
    • @MessageMapping
      • STOMP 메시지를 처리하는 메서드임을 나타내며, 클라이언트가 해당 경로로 전송한 메시지를 이 메서드가 처리한다.
    • @SendTo
      • 해당 메서드는 반환한 메시지를 구독 중인 클라이언트에게 보낼 경로를 설정한다.
      • 즉. /chat/{roomId}를 통해 구독한 사용자 모두 메시지를 받는다. 

 

 

📝테스트 HTML

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>채팅 애플리케이션</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
  <style>
      body { font-family: Arial, sans-serif; }
      #messages { border: 1px solid #ccc; height: 300px; overflow-y: scroll; margin-bottom: 10px; padding: 5px; }
      #messageInput { width: 80%; }
  </style>
</head>
<body>
<h1>채팅 애플리케이션</h1>
<div>
  <label for="roomId">방 ID:</label>
  <input type="text" id="roomId" placeholder="방 ID 입력">
  <button id="joinBtn">입장</button>
</div>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="메시지를 입력하세요">
<button id="sendBtn">전송</button>

<script>
    let stompClient = null;
    let roomId = '';

    function connect() {
        const socket = new SockJS('/ws'); // WebSocket 엔드포인트
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            console.log('연결됨: ' + frame);
            stompClient.subscribe('/chat/' + roomId, function (message) {
                showMessage(JSON.parse(message.body));
            });
        });
    }

    function joinRoom() {
        roomId = document.getElementById('roomId').value;
        connect();
        document.getElementById('messages').innerHTML = ''; // 메시지 초기화
    }

    function sendMessage() {
        const messageInput = document.getElementById('messageInput');
        const message = {
            sender: '사용자',
            message: messageInput.value
        };
        stompClient.send('/chat/sendMessage/' + roomId, {}, JSON.stringify(message));
        messageInput.value = ''; // 입력창 비우기
    }

    function showMessage(message) {
        const messagesDiv = document.getElementById('messages');
        messagesDiv.innerHTML += '<div><strong>' + message.sender + ':</strong> ' + message.message + '</div>';
        messagesDiv.scrollTop = messagesDiv.scrollHeight; // 스크롤을 맨 아래로
    }

    document.getElementById('joinBtn').onclick = joinRoom;
    document.getElementById('sendBtn').onclick = sendMessage;
</script>
</body>
</html>

 

 

my_Lab/WebSocket at main · quddaz/my_Lab (github.com)

 

my_Lab/WebSocket at main · quddaz/my_Lab

✔ 실습을 통한 샘플 코드 만들기. Contribute to quddaz/my_Lab development by creating an account on GitHub.

github.com

자세한 코드는 해당 리포지토리에서 확인 가능합니다.