[Springboot] WebSocket으로 Binary 데이터 받아오기

LTT·2025년 3월 22일

WebSocket은 언제 사용해야 할까?


일반적으로 HTTP 기반 애플리케이션은 클라이언트가 요청을 보내면 서버가 응답하는 방식으로 동작한다. 하지만 실시간 양방향 통신이 필요한 경우, HTTP만으로는 한계가 발생할 수 있다. 이러한 상황에서는 WebSocket을 활용하면 보다 효율적인 통신이 가능하다. WebSocket이 필요한 대표적인 사례는 다음과 같다.

  1. 실시간 채팅 애플리케이션
    • 사용자가 입력한 메시지가 즉시 다른 사용자에게 전달되어야 하는 경우.
    • HTTP 요청-응답 방식보다 WebSocket을 활용하면 네트워크 오버헤드를 줄이고 빠른 응답을 보장할 수 있다.
  2. 라이브 데이터 스트리밍
    • 주식 가격, 스포츠 경기 점수, 실시간 센서 데이터 등 빠르게 갱신되는 정보를 제공해야 하는 경우.
    • 클라이언트가 주기적으로 요청을 보내는 Polling 방식보다 WebSocket을 활용하면 불필요한 요청을 줄이고 더욱 효율적인 데이터 전송이 가능하다.
  3. 멀티플레이어 온라인 게임
    • 게임 내 캐릭터의 움직임이나 상태를 실시간으로 동기화해야 할 때.
    • 낮은 지연 시간과 빠른 데이터 전송이 중요하므로 WebSocket이 적절한 해결책이 될 수 있다.
  4. 파일 전송 및 미디어 스트리밍
    • 대용량 파일 전송이나 오디오/비디오 스트리밍을 처리할 때.
    • 특히 Binary 데이터를 효율적으로 주고받기 위해 WebSocket을 활용할 수 있다.

WebSocket을 사용하면 서버와 클라이언트가 지속적으로 연결을 유지하면서 데이터를 실시간으로 주고받을 수 있기에 위와같은 용도에서 사용한다.

다음 섹션에서는 WebSocket을 활용하여 Binary 데이터를 처리하는 방법을 살펴보자.

⭐️ 구현해보기


의존성부터 주입해보자.

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-websocket'

그 다음에는 Configuration 파일을 만들어보자.

WebSocketConfig.java

@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {

    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/test").setAllowedOrigins("*");
    }
}

여기서 "/test" 엔드포인트는 이후 ws://localhost:8080/test와 같이 WebSocket 연결을 설정하는 데 사용된다. 또한 setAllowedOrigins("*")는 CORS 정책을 우회하기 위해 모든 도메인을 허용하는 설정이지만, 실제 서비스에서는 보안 문제로 인해 와일드카드를 사용하는 것은 지양해야 한다.

이제 WebSocket을 처리하는 핸들러 클래스를 만들어보자. 쉽게 말해 REST API 에서 Controller라고 생각하면 된다.

WebSocketHandler

@Component
public class WebSocketHandler extends BinaryWebSocketHandler {

    private static final ConcurrentHashMap<String, WebSocketSession> clients = new ConcurrentHashMap<String, WebSocketSession>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        clients.put(session.getId(), session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        clients.remove(session.getId());
    }

    @Override
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
        // Binary 데이터를 처리하는 로직을 추가할 수 있다.
        // 예를 들어, 음성 데이터라고 가정하고 byte[]로 변환해보자.
        byte[] audioData = message.getPayload().array();
        
        // 클라이언트에게 응답을 보낼 수도 있다.
        session.sendMessage(new TextMessage("Binary data received"));
    }
}

이 코드에서 BinaryWebSocketHandler를 상속하여 WebSocket 연결을 관리하고 Binary 데이터를 처리한다. 중요한 메서드는 다음과 같다 :

  • afterConnectionEstablished(WebSocketSession session): 새로운 WebSocket 연결이 생성되었을 때 호출되며, 연결된 세션을 저장한다.
  • afterConnectionClosed(WebSocketSession session, CloseStatus status): WebSocket 연결이 종료될 때 호출되며, 저장된 세션을 제거한다.
  • handleBinaryMessage(WebSocketSession session, BinaryMessage message): 클라이언트로부터 Binary 데이터를 받을 때 실행되는 메서드이다.

이제 WebSocket을 활용한 Binary 데이터 처리의 기본적인 설정이 완료되었다. 이후에는 해당 데이터를 원하는 방식으로 활용하는 로직을 추가하면 된다.

보너스


나는 마이크를 통한 음성 데이터를 실시간으로 처리하기 위한 기능을 구현하던 중이었기 때문에 샘플용 프론트 코드 또한 보여주자면,

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket Mic Test</title>
  </head>
  <body>
    <h2>WebSocket Mic Test</h2>
    <button onclick="startRecording()">Start</button>
    <button onclick="stopRecording()">Stop</button>
    <script>
      let ws;
      let mediaRecorder;

      function connectWebSocket() {
        ws = new WebSocket("ws://localhost:8080/test");

        ws.onopen = () => console.log("WebSocket 연결됨");
        ws.onmessage = (event) => console.log("서버 응답:", event.data);
        ws.onerror = (error) => console.error("WebSocket 오류:", error);
        ws.onclose = () => console.log("WebSocket 연결 종료");
      }

      async function startRecording() {
        connectWebSocket();

        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
        });
        mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm" });

        mediaRecorder.ondataavailable = (event) => {
          if (ws.readyState === WebSocket.OPEN) {
            ws.send(event.data);
            console.log("음성 데이터 전송:", event.data);
          }
        };

        mediaRecorder.start(500); // 500ms마다 데이터 전송
        console.log("녹음 시작");
      }

      function stopRecording() {
        mediaRecorder.stop();
        ws.close();
        console.log("녹음 종료 및 WebSocket 닫음");
      }
    </script>
  </body>
</html>

이 화면에서 테스트해보면 된다.

profile
개발자에서 엔지니어로, 엔지니어에서 리더로

2개의 댓글

comment-user-thumbnail
2025년 3월 22일

좋은 글 감사합니다.

질문이 있는데, 만약 WebSocket 연결이 비정상적으로 종료되는 상황을 처리하려면 어떻게 해야 하나요?

1개의 답글