본문 바로가기

Spring Boot

[Spring Boot] 간단한 실시간 웹소켓 채팅 구현하기

소켓이란?

소켓(Socket)은 네트워크 상에서 서로 다른 프로그램이 데이터를 송수신하기 위한 종단점입니다.
소켓은 IP 주소와 포트 번호를 기반으로 통신하며, 클라이언트와 서버 간의 연결을 유지하거나 메시지를 교환하는 데 사용됩니다.

소켓의 주요 개념

  • IP 주소: 네트워크 상에서 컴퓨터를 식별하는 고유 주소.
  • 포트 번호: 특정 애플리케이션(프로세스)을 식별하는 번호.
  • 소켓 연결: 클라이언트와 서버가 데이터를 주고받기 위해 소켓을 통해 이루어지는 연결.

 

 

웹소켓(WebSocket)이란?

기존 HTTP 프로토콜은 요청-응답 기반으로 작동하여 실시간 양방향 통신이 어렵습니다.
웹소켓(WebSocket)은 이러한 한계를 극복하기 위해 만들어진 프로토콜로,

  • 서버와 클라이언트 간에 지속적인 연결을 유지하며,
  • 실시간으로 양방향 데이터를 주고받을 수 있도록 지원합니다.

웹소켓의 특징

  1. 양방향 통신: 서버와 클라이언트가 서로 데이터를 주고받을 수 있음.
  2. 연결 지속성: 연결이 유지되는 동안 추가적인 핸드셰이크 없이 데이터 교환.
  3. 효율성: 요청 없이 서버가 데이터를 푸시(Push) 가능.

 

 

스프링에서 웹소켓 구현하기

스프링 프레임워크는 STOMP 프로토콜을 사용하여 간단하게 웹소켓 기반의 실시간 통신을 구현할 수 있습니다.
아래는 스프링을 활용한 실시간 채팅 애플리케이션 예제입니다.

 

 

1. 프로젝트 설정

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

 

2. 웹소켓 설정

WebSocketConfig 클래스에서 웹소켓 엔드포인트와 메시지 브로커를 설정합니다.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic"); // 구독 주소
        config.setApplicationDestinationPrefixes("/app"); // 메시지 송신 주소
    }

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

 

 

3. 채팅 컨트롤러

클라이언트의 메시지를 처리하고, 구독자에게 메시지를 전달합니다.

@Controller
public class ChatController {

    @MessageMapping("/chat/{roomName}") // 클라이언트 메시지 처리
    @SendTo("/topic/{roomName}") // 구독 경로로 메시지 전송
    public String sendMessage(String message) {
        return message;
    }
}

 

 

4. HTML 클라이언트 예제

채팅방에 연결하고 메시지를 주고받는 클라이언트 코드입니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat Rooms</title>
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs/lib/stomp.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
        }
        .container {
            width: 50%;
            margin: auto;
            padding: 20px;
            text-align: center;
        }
        input, button {
            margin: 5px;
        }
        .messages {
            border: 1px solid #ccc;
            padding: 10px;
            margin-top: 10px;
            height: 200px;
            overflow-y: auto;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>Chat Rooms</h1>
    <input id="roomInput" type="text" placeholder="Enter room name" />
    <button onclick="joinRoom()">Join Room</button>
    <br>
    <input id="messageInput" type="text" placeholder="Enter your message" />
    <button onclick="sendMessage()">Send</button>
    <div class="messages" id="messages"></div>
</div>

<script>
    let stompClient = null; // STOMP 클라이언트 객체를 저장할 변수
    let currentRoom = null; // 현재 사용자가 접속한 채팅방 이름을 저장할 변수

    // 서버와 WebSocket 연결을 설정하는 함수
    function connect() {
        // 1. '/ws' 엔드포인트에 SockJS를 사용하여 WebSocket 연결 생성
        const socket = new SockJS('/ws');
        
        // 2. SockJS 객체를 사용하여 STOMP 클라이언트 생성
        stompClient = Stomp.over(socket);

        // 3. STOMP 프로토콜을 사용하여 서버와 연결 시작
        stompClient.connect({}, () => {
            console.log('Connected to WebSocket'); // 연결 성공 시 로그 출력
        });
    }

    // 사용자가 특정 채팅방에 참여하는 기능을 구현하는 함수
    function joinRoom() {
        if (stompClient) { // WebSocket 연결이 활성화된 상태인지 확인
            const roomInput = document.getElementById('roomInput'); // 입력된 채팅방 이름 가져오기
            currentRoom = roomInput.value; // 현재 채팅방 이름을 변수에 저장

            if (currentRoom) { // 채팅방 이름이 유효한 경우
                // 1. STOMP 클라이언트가 해당 채팅방 주제를 구독
                //    /topic/{roomName} 경로를 구독하여 해당 방의 메시지를 실시간으로 수신
                stompClient.subscribe(`/topic/${currentRoom}`, (response) => {
                    showMessage(response.body); // 메시지가 수신되면 화면에 표시
                });

                // 2. 구독이 성공했음을 사용자에게 알림
                alert(`Joined room: ${currentRoom}`);
            }
        }
    }

    // 사용자가 메시지를 보내는 기능을 구현하는 함수
    function sendMessage() {
        if (stompClient && currentRoom) { // WebSocket 연결과 채팅방이 활성화된 경우
            const messageInput = document.getElementById('messageInput'); // 입력된 메시지 가져오기
            const message = messageInput.value; // 메시지 값 저장

            // 1. STOMP 클라이언트를 통해 메시지를 서버로 전송
            //    /app/chat/{roomName} 경로로 메시지를 송신
            stompClient.send(`/app/chat/${currentRoom}`, {}, message);

            // 2. 입력창 초기화
            messageInput.value = '';
        }
    }

    // 수신된 메시지를 화면에 표시하는 함수
    function showMessage(message) {
        const messagesDiv = document.getElementById('messages'); // 메시지 표시 영역
        const messageElement = document.createElement('div'); // 새로운 메시지 요소 생성

        // 메시지 내용을 div 요소에 추가
        messageElement.textContent = message;
        
        // 메시지를 화면에 추가
        messagesDiv.appendChild(messageElement);
    }

    // 페이지 로드 시 WebSocket 연결 자동 설정
    connect();
</script>
</div>
</body>
</html>