[KB IT's Your Life TIL] 오늘의 학습 내용 : HTTP Polling, WEB socket

JUN·2024년 8월 25일
0

KB IT's your life

목록 보기
8/16
post-thumbnail

웹에서 실시간 이벤트를 처리해야할때 두가지 기술에 대한 이해를 정리했다.

주요 키워드

  • WebSocket
  • HTTP Polling
  • 실시간 양방향 통신
  • STOMP (Simple Text Oriented Messaging Protocol)
  • Spring WebSocket
  • 메시지 브로커

학습한 내용

HTTP Polling

HTML5 표준 기술인 웹 소켓 방식의 기술이 등장하기 전까지는 웹에서 실시간 데이터를 처리해야할 때 마치 실시간인 것처럼 작동하게 하는 방법들이 있었다. 그것이 바로 Polling 방식이다.

HTTP polling 과 HTTP long pollnig

HTTP polling과 HTTP long polling은 웹 애플리케이션에서 실시간 데이터 업데이트를 처리하는 두 가지 다른 기술이다.

  1. HTTP polling:
  • 클라이언트가 서버에 주기적으로 요청을 보내 새로운 데이터가 있는지 확인.
  • 간단하지만 불필요한 요청이 많이 발생할 수 있어 오버헤드가 발생한다.
  1. HTTP long polling:
  • 클라이언트가 서버에 요청을 보내고, 서버는 새로운 데이터가 있을 때까지 응답을 보류합니다. (Pending)
  • 데이터가 준비되면 서버가 응답을 보내고, 클라이언트즉시 새로운 요청을 보냅니다.
  • polling보다 효율적이지만, 여전히 연결을 주기적으로 열고 닫아야 합니다.

두 방식 모두 실시간 통신을 모방하지만, WebSocket에 비해 효율성이 떨어진다.

WebSocket

HTML5의 출시와 함께 등장한 실시간 양방향 데이터 전송 기술이다.

웹소켓의 연결

  1. 웹소켓 연결은 HTTP 프로토콜을 사용하여 시작된다. (Opening Handshake)
    1. 클라이언트가 서버에 HTTP upgrade request 를 보낸다.
    2. 서버는 이에 대한 응답으로 101 response를 보낸다.
  2. 연결이 수립된 후에는 ws 프로토콜(또는 wss 프로토콜)로 전환된다.
  3. 연결이 성공적으로 수립되면, 클라이언트와 서버는 양방향으로 실시간 데이터를 주고받을 수 있게 된다.
    1. 서버와 클라이언트는 연결 상태를 확인하기 위해 주기적으로 heartbeat 패킷과 ping을 주고받는다.
  4. 연결 종료 시 Closing Handshake가 진행되며, 클라이언트와 서버 모두 컨트롤 프레임을 전송하여 연결을 종료할 수 있다.

WebSocket은 HTTP와 호환되도록 설계되었지만, 두 프로토콜의 아키텍처와 애플리케이션 프로그래밍 모델은 매우 다르다

  • HTTP/REST: URL, 메서드, 헤더를 기반으로 요청을 적절한 핸들러로 라우팅
  • WebSocket: 초기 연결을 위한 URL이 하나 존재하고, 모든 애플리케이션 내 메시지는 동일한 TCP 연결을 통해 전송

주요 특징

  • 실시간 양방향 통신: 서버와 클라이언트 간 지속적인 연결을 유지하여 즉각적인 데이터 교환이 가능하다.
  • 효율적인 리소스 사용: 한 번 연결이 수립되면 추가적인 HTTP 요청 없이 데이터를 주고받을 수 있어 오버헤드가 감소한다.
  • 낮은 지연 시간: 실시간 통신으로 인해 데이터 전송 지연이 최소화된다.

스프링에서 WebSocket 통신 구현하기

STOMP (Simple Text Oriented Messaging Protocol)

STOMP는 WebSocket의 서브 프로토콜로, 메시지 통신을 위한 형식과 규칙을 정의한다. 기본적으로 Publish-Subscribe 구조를 따르며, frame을 사용해 전송하는 프로토콜이다.

주요 특징:

  • WebSocket이 정의하지 않는 메시지 내용, 형식, 유형을 정의
  • 클라이언트와 서버 간 통신 시 메시지 구조화 가능
  • 프레임을 이용한 전송 (명령, 헤더, 본문으로 구성)
  • 명령(command)과 헤더(header), 바디(body)로 구성된 frame 사용
    COMMAND 
    header1:value1 
    header2:value2 
    
    Body^@
  • Pub-Sub 구조 기반

Pub-Sub (Publish-Subscribe) 구조

메시지 통신 패턴으로, 발신자(publisher)가 특정 수신자를 지정하지 않고 메시지를 발행하면,

해당 주제(topic)를 구독한 수신자들(subscribers)에게 자동으로 메시지가 전달되는 방식

구독 예시:

>> SUBSCRIBE
id:sub-0
destination:/sub/chat/room/07905aff-a14a-4162-b065-14418519c9d5

메시지 전송 예시:

>> SEND
destination:/pub/chat/message
content-length:104

{"chatRoomNo":"07905aff-a14a-4162-b065-14418519c9d5","chatMessage":"예시 메세지","chatWriter":"메시지 작성자"}

STOMP를 사용하면 개발자가 메시지 처리 로직을 일일이 구현할 필요 없이, 표준화된 방식으로 실시간 통신을 구현할 수 있다.

STOMP는 Rabbit MQ, Active MQ 등을 사용하여 Pub/Sub 서비스를 이용할 수 있다.

기본적으로 In Memory Broker를 사용할 수 있지만 다음과 같은 단점이 있다:

  • 세션을 수용할 수 있는 크기가 제한되어 있다.
  • 장애 발생 시 메시지의 유실 가능성이 높다.
  • 따로 모니터링하는 것이 불편하다.
  • 서버가 여러대 떠있다면 Message Broker끼리 동기화가 되지 않는다.

메시지 브로커의 역할

메시지 브로커는 발신자와 수신자 사이에서 중개자 역할을 함.

메시지의 저장, 전달, 변환 등을 담당

→ 시스템 간의 결합도를 낮추고 확장성을 높일 수 있다.

대규모 실시간 데이터 처리가 필요한 경우, 메시지 브로커로 Apache Kafka를 사용할 수 있다.

(카카오의 대용량 데이터를 처리하는 웹소켓 시스템에 Kafka를 메시지 브로커로 채용한 사례)

Spring WebSocket과 STOMP 통합

Spring에서 WebSocket과 STOMP를 통합하여 사용하는 경우, 다음과 같은 순서로 주요 특징과 구성 요소를 설정해야한다.

  1. WebSocket 설정
    • @EnableWebSocket 또는 @EnableWebSocketMessageBroker 어노테이션으로 활성화
    • /ws-stomp 같은 엔드포인트를 통해 클라이언트-서버 연결
  2. STOMP 메시지 처리
    • 클라이언트는 특정 경로(예: /app/hello)로 메시지 전송
    • 서버는 메시지를 처리하고 다른 클라이언트에게 브로드캐스트
  3. 메시지 브로커 설정
    • 내부 SimpleBroker 또는 외부 브로커(예: RabbitMQ) 사용 가능
    • 클라이언트 간 메시지 중계 및 복잡한 메시징 시나리오 구현
  4. SockJS 지원 설정
    • WebSocket을 지원하지 않는 환경에서 대체 기술 제공
  5. 클라이언트 측 구현
    • 브라우저에서 WebSocket 객체 생성 및 서버 연결
    • 서버와의 양방향 메시지 송수신

스프링에서 WebSocket 실습하기

1. 의존성 추가하기

먼저 build.gradle 파일에 WebSocket 과 Messaging 관련 의존성을 추가해야한다.

dependencies {
	...
  //webSocket
  implementation 'org.springframework:spring-websocket:5.3.29'
  implementation 'org.springframework:spring-messaging:5.3.29'
  ...
}

2. WebSocket 설정 클래스 만들기

WebSocket을 설정하기 위해 WebSocketConfig 클래스를 생성합니다. 이 클래스에서 WebSocket과 STOMP의 주요 설정을 진행합니다:

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 메시지 브로커 설정
        config.enableSimpleBroker("/topic", "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 웹소켓 엔드포인트 설정
        registry.addEndpoint("/ws")
                .setAllowedOrigins("*")
                .withSockJS();
    }
}
  • configureMessageBroker: 메시지 브로커를 설정 /topic/queue는 메시지가 전달될 수 있는 목적지
  • registerStompEndpoints: 클라이언트가 웹소켓 서버에 연결할 수 있는 엔드포인트를 정의한다. /ws 엔드포인트를 설정하고, SockJS 폴백을 지원하도록 설정했다. setAllowedOrigins("*")를 통해 모든 도메인에서의 접근을 허용했지만, 실제 운영 환경에서는 보안을 위해 특정 도메인만 허용하는 것이 좋다.

3. 메시지 핸들러 (컨트롤러) 구현하기

이제 WebSocket 메시지를 처리하는 컨트롤러를 만들 차례다.

@MessageMapping@SendTo를 이용해 STOMP 메시지를 처리한다.

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Controller
public class ChatController {

    @MessageMapping("/chat")
    @SendTo("/topic/messages")
    public OutputMessage send(Message message) {
        String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
        return new OutputMessage(message.getFrom(), message.getText(), time);
    }
}
  • @MessageMapping("/chat"): 클라이언트에서 /app/chat으로 전송된 메시지를 이 메서드가 처리한다.
  • @SendTo("/topic/messages"): 처리된 메시지를 /topic/messages를 구독하고 있는 모든 클라이언트에게 브로드캐스트한다.

4. 메시지 도메인 클래스 정의하기

전송할 메시지의 구조를 정의하는 클래스 Lombok을 사용하여 보일러 플레이트를 줄였따.

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    private String from;
    private String text;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OutputMessage {
    private String from;
    private String text;
    private String time;
}

5. 클라이언트 측 구현하기 (JavaScript)

이제 클라이언트 측에서 WebSocket 통신을 설정한다.

클라이언트는 웹 페이지에서 메시지를 보내고 받을 수 있다.

let stompClient = null;

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

    stompClient.connect({}, function (frame) {
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/messages', function (message) {
            showMessage(JSON.parse(message.body));
        });
    }, function(error) {
        console.log('STOMP error ' + error);
    });
}

function sendMessage() {
    const message = {
        from: document.getElementById('from').value,
        text: document.getElementById('text').value
    };
    stompClient.send("/app/chat", {}, JSON.stringify(message));
    document.getElementById('text').value = '';
}

function showMessage(message) {
    const response = document.getElementById('response');
    const p = document.createElement('p');
    p.style.wordWrap = 'break-word';
    p.appendChild(document.createTextNode(`${message.from}: ${message.text} (${message.time})`));
    response.appendChild(p);
    response.scrollTop = response.scrollHeight; // 새 메시지가 오면 스크롤을 아래로 이동
}

// 페이지 로드 시 WebSocket 연결
window.onload = connect;
  • connect(): 서버와의 WebSocket 연결을 설정한다. 연결 실패 시 에러 처리도 추가했다.

6. HTML 페이지 작성하기

다음과 같은 HTML 페이지를 작성하여 클라이언트 측에서 메시지를 주고받을 수 있다:

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>실시간 채팅 애플리케이션</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>
    <script src="/js/app.js"></script>
    <style>
        #response {
            height: 300px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
    <h1>실시간 채팅</h1>
    <div>
        <input type="text" id="from" placeholder="이름" required>
        <input type="text" id="text" placeholder="메시지" required>
        <button onclick="sendMessage();">전송</button>
    </div>
    <div id="response"></div>
</body>
</html>

7. 실행 및 테스트

모든 설정이 완료되면 스프링 애플리케이션을 실행해 웹소켓 기반 실시간 채팅 앱을 테스트할 수 있다. 여러 브라우저 창에서 같은 URL로 접속해 실시간으로 메시지를 주고받을 수 있다.

오늘의 회고

STOMPSpring WebSocket을 사용해 양방향 실시간 통신을 구현해 보았다.

이 방법으로 클라이언트와 서버 간 실시간 메시지 전송이 가능하며, STOMP로 메시지 브로커와 쉽게 통합할 수 있다. (실제 프로젝트에 적용할 때는 보안, 에러 처리, 스케일링 등을 고려해야 한다.)

최근 공모전에서 쳇봇을 구현하면서 관심있게 찾아본 기술인데 KB_IT's Your LIfe 교육을 듣던 중에 나와서 반가워서 조금 열심히 파봤다.

곧 다가오는 최종 프로젝트에서 이 기술을 적용해 실시간 통신 기능이 있는 서비스를 구현해보고 싶다.
특히 챗봇이나 실시간 알림 시스템 등을 구현하면서, 실제 서비스 환경에서의 문제점들을 발견하고 해결해나가는 경험을 쌓을 수 있으면 좋겠다.


참고

Spring Websocket 이론과 간단한 구현

실시간 댓글 개발기(part.1) - DAU 60만 Alex 댓글의 실시간 댓글을 위한 이벤트 기반 아키텍처

profile
순간은 기록하고 반복은 단순화하자 🚀

0개의 댓글