채팅 기능 구현을 위한 WebSocket + STOMP 구조 정리

2026. 2. 17. 23:59·개발/CS

안녕하세요. 노흐입니다.

지난 한달동안 야구 관람·경험 플랫폼 팀프로젝트를 진행했습니다.

 

프로젝트 회고 (미작성)
https://snack.tistory.com/38

 

 

해당 프로젝트에서 채팅 기능을 맡아 Spring + STOMP 기반 WebSocket 구조로 구현했습니다.

하지만 프론트단에서 '작동이 안 된다'라는 피드백이 여러번 들어왔고,

그 원인을 설명하는 데에 예상보다 많은 시간을 소모했습니다.

 

돌이켜보면 원인은 기술 자체보다 다음 세 가지 였습니다.

  1. WebSocket의 동작 원리를 정확히 이해하지 못한 점
  2. /ws, /app, /topic에 대한 설명 문서 부족
  3. 모바일(flutter) 환경에서 발생할 수 있는 문제에 대한 대비 부족

재발을 막기 위해 WebSocket과 STOMP 구조를 정리해보고자 합니다.

 

1. WebSocket이란 무엇인가?

WebSocket은 한 번의 연결로 서버와의 지속적으로 통신할 수 있는 프로토콜입니다.

정확히 말하자면 '웹 브라우저와 서버 간에 지속적이고 동적인 양방향 통신 채널을 제공하는 표준 프로토콜'이라고 할 수 있습니다.

 

기존 HTTP 방식은 요청(Request)이 있어야 서버가 응답(Response) 해주는 구조입니다.

 

채팅처럼 새 메시지가 왔는지 지속적으로 요청을 날려 확인해야하는 상황에서는 번거롭고 비효율적입니다. 이를 해소하기 위해 polling이나 streaming과 같은 방법이 제안되었지만 WebSocket보다 비효율적이므로 WebSocket이 지원되지 않는 브라우저이거나 방화벽에서 차단당하는 경우가 아닌 이상 사용하지 않습니다.

 

2. 채팅은 대부분 WebSocket으로 구현하나요? : 네

이유를 정리하자면 아래와 같습니다.

1) 실시간 양방향 통신 (Full-Duplex)

클라이언트와 서버가 서로 원할 때 언제든 데이터를 보낼 수 있습니다.

2) 낮은 대기 시간 (Low Latency)

최초 한 번의 핸드쉐이크(Handshake)로 연결이 유지됩니다.

매번 연결을 맺고 끊는 과정이 없으므로 응답 속도가 매우 빠릅니다.

3) 효율적인 리소스 사용 (Reduced Overhead)

HTTP는 매 요청마다 방대한 헤더(Header) 정보를 포함해야 하지만,

웹소켓은 연결 후 아주 작은 프레임 단위로 통신하여 네트워크 트래픽을 줄여줍니다.

https://www.youtube.com/watch?v=rvss-_t6gzg&t=420s

 

채팅, 알림, 실시간 업데이트 기능에는 WebSocket이 적합하다는 걸 알 수 있습니다.

 

3. WebSocket은 어디서든 다 지원하나요? : 아니오

위에서 말했듯이 WebSocket을 지원하지 않는 구형 브라우저가 존재합니다.

이번 프로젝트에서는 모바일 어플리케이션 환경이라 순수 WebSocket으로 구현했으나 이왕이면 호환성을 위해 SockJS와 같은 라이브러리를 고려해봐야합니다. 

 

SockJS는 WebSocket Emulation 기술을 제공하는 라이브러리 입니다.

WebSocket Emulation은 우선적으로 WebSocket을 활용하여 통신을 시도하고, 실패할 경우 HTTP 기반의 다른 기술로 전환하여 재연결을 시도하는 방식입니다.

 

2026년 현재 WebSocket을 지원하지 않을 정도의 구형 브라우저를 타겟으로 하는 경우가 적기 때문에 무용하다는 의견도 존재합니다. 최신 브라우저/모바일 앱/내부망이 안정적인 환경이면 WebSocket만으로 충분한 경우가 많고, SockJS가 제공하는 핵심 가치(구형/제한망 fallback)가 거의 발동하지 않습니다. 어느정도 사실이지만 안정성이 매우 중요한 프로젝트라면 여전히 유효한 선택이라 생각합니다.

 

4. 이번 프로젝트 내에서의 WebSocket 통신 흐름

실제 프로젝트 기준으로 통신 흐름을 정리해보았습니다.

1단계 : TCP 연결

WebSocket은 TCP 기반 프로토콜입니다. 따라서 연결을 맺을 떄 먼저 TCP 3-Way Handshake가 발생합니다.

이 과정은 WebSocket만의 과정이 아니라 일반적인 HTTP, FTP, SSH 등 TCP기반 통신에서 공통으로 발생합니다.

개발자가 직접 구현하는 부분은 아니니 걱정하지 않아도됩니다.

2단계 : HTTP Upgrade

WebSocket도 처음엔 HTTP 요청으로 시작합니다.

 

Flutter가 /ws 로 연결 요청을 보내면

GET /ws
Upgrade: websocket

 

서버가 아래와 같이 응답하고 프로토콜이 전환됩니다.

101 Switching Protocols

 

이후부터는 HTTP가 아니라 WebSocket 프로토콜로 동작합니다.

/ws는 REST API가 아니라 WebSocket 연결을 생성하는 엔드포인트입니다.

 

3단계 : 지속적인 양방향 통신

HTTP Upgrade가 성공했다면 이제부터는 HTTP 요청/응답 모델이 아닙니다.

하나의 TCP 연결 위에서 지속적인 양방향(Full-Duplex) 통신이 가능합니다. 이 상태에서는 클라이언트가 요청하지 않아도 서버가 먼저 메시지를 보낼 수 있습니다. 

 

4단계 : STOMP 프로토콜 동작

이번 프로젝트에서는 단순 WebSocket이 아니라 STOMP 기반 메시징 구조를 사용했습니다.

 

STOMP(Simple Text Oriented Messaging Protocol)는 원래 메시지 브로커와 통신하기 위한 텍스트 기반 프로토콜입니다.

풀어서 설명하자면 WebSocket 통신 위에서 그 메시지를 어떻게 주고받을지 정의하는 규칙이라고 할 수 있습니다.

 

헷갈리는 점이 몇가지 있는데요.

  • STOMP는 WebSocket 전용 프로토콜이 아닙니다.
  • 원래 TCP 위에서 동작하도록 설계되었습니다.
  • Spring이 WebSocket 위에서 STOMP를 사용할 수 있게 지원합니다.


하지만 그렇다고 TCP 위에서 동작할 때와 WebSocket위에서 동작할 때의 STOMP의 계층이 바뀌거나 하지는 않습니다.

 

  1. TCP위에서 STOMP를 쓰는 경우
Application Layer → STOMP
Transport Layer   → TCP
Network Layer     → IP
  1. WebSocket 위에서 STOMP를 쓰는 경우
Application Layer → STOMP
Application Layer → WebSocket
Transport Layer   → TCP
Network Layer     → IP

 

WebSocket위에 얹어진 STOMP는 그저 또다른 응용 프로토콜 위에 올라타게 됩니다.

 

STOMP 연결 흐름

WebSocket 연결이 완료된 뒤, Flutter에서는 stomp_dart_client를 통해 다음과 같은 과정을 거칩니다.

  1. CONNECT 프레임 전송
  2. 서버가 CONNECTED 프레임 응답
  3. SUBSCRIBE 
  4. SEND
  5. MESSAGE 브로드캐스트 수신

프로젝트 내부를 표현하면 다음과 같습니다.

Flutter
   ↓ CONNECT
Spring STOMP Broker
   ↓ CONNECTED
   ↓ SUBSCRIBE (/topic/room)
   ↓ SEND (/app/chat)
@MessageMapping Controller
   ↓
Service → DB 저장
   ↓
/topic/room 으로 브로드캐스트
   ↓
구독 중인 모든 클라이언트 수신

 

여기서 중요한 포인트는WebSocket은 '통신 채널'이고 STOMP는 '메시지 규칙'이라는 점입니다.

WebSocket만 사용하면 단순 바이트 통신입니다.
STOMP를 사용하면 목적지 기반(pub/sub) 메시징이 가능해집니다.

 

Spring에서의 STOMP 구조

기존 HTTP 메서드와 다른 방식이다보니 처음엔 생소하고 헷갈립니다.

Spring에서의 STOMP 구조에 대해  좀더 자세히 짚어보겠습니다.

/app,/topic은 STOMP 표준 개념이 아니라 Spring MessageBroker 설정에 의해 정의된 prefix입니다.

 

Spring에서는 다음과 같이 설정합니다.

registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");

 

1) /app은 무엇인가?

/app으로 시작하는 메시지는 서버의 @MessageMapping 메서드로 전달됩니다.

 

예를 들어 클라이언트가 아래 메시지를 보내면

SEND /app/chat

  

@MessageMapping("/chat") 메서드가 실행됩니다.

 

즉, /app은 서버 로직으로 전달되는 경로 prefix입니다.

이건 브로커로 가는 주소가 아니라 컨트롤러로 매핑되는 주소입니다.

 

 

2) /topic은 무엇인가?

/topic으로 시작하는 destination은 Spring의 브로커(SimpleBroker)가 처리합니다.
 
SUBSCRIBE /topic/room/1

 

이라고 전송하면

해당 경로로 발행된 메시지를 브로커가 구독자들에게 전달합니다.

 

3) 정리하면 

  • /app → 서버 로직(@MessageMapping)으로 전달되는 경로
  • /topic → 브로드캐스트용. 브로커가 구독자에게 전달하는 경로

왜 /app/chat으로 보내면 /topic/room/1으로 오는지.

이 부분을 명확히 이해하지 못하면 왜 /app/chat으로 보내고 /topic/room/1을 구독하는지 혼란이 생깁니다.

 

이 차이를 저는 API 명세서에 설명하지 않았고 이때문에 협업에서 혼란을 만들었습니다.

 

 

왜 WebSocket만 쓰지 않고 STOMP를 사용할까?

WebSocket만 사용하면 단순히 '연결된 상태에서 데이터를 주고받을 수 있는 채널'만 제공됩니다.

이 경우 서버는 모든 라우팅 로직을 직접 구현해야 합니다.

  • 어떤 사용자가 어떤 채널에 속하는지 관리
  • 메시지를 누구에게 보낼지 직접 판단
  • 브로드캐스트 로직 구현
  • 세션 관리

 

반면 STOMP를 사용하면 상당히 간단해집니다.

  • destination 기반 pub/sub 구조 사용 가능
  • 메시지 라우팅을 브로커가 처리
  • @MessageMapping 같은 애노테이션으로 서버 로직 연결
  • /topic을 통한 브로드캐스트 지원

 

WebSocket은 통신 채널이고 STOMP는 그 채널 위에서 '어디로 메시지를 보낼지 정해주는 규칙'입니다.

 

5. 그렇다면 왜 '채팅이 안 돼요' 문제가 발생했을까

1) /app과 /topic의 역할을 명확히 설명하지 않음

프론트 입장에서 '어디로 SEND 해야 하는지, 어디를 SUBSCRIBE 해야 하는지, 왜 /app/chat으로 보내면 /topic/room/1로 오는지'

이 구조를 이해하기 어려웠을 것입니다.

2) 메시지 Payload 명세 부족

REST API는 명세서에 정리했지만, 'WebSocket 메시지는 어떤 JSON 구조인지, roomId는 어디에 포함되는지, 필수 필드는 무엇인지' 명확히 문서화하지 않았습니다.

 

돌이켜보면 원인은 기술적 결함보다 설명 부족이었습니다.

 

6. 이번 경험을 통해 배운 점

  1. WebSocket은 단순한 “실시간 기술”이 아니라 계층적 구조를 가진 프로토콜이다.
  2. 3-Way Handshake는 WebSocket의 개념이 아니라 TCP의 개념이다.
  3. STOMP는 WebSocket 위에서 동작하는 메시징 규칙이다.
  4. 문서화되지 않은 WebSocket은 협업에 치명적이다.

마무리

채팅 기능은 생각보다 복잡했습니다.

  • TCP 연결
  • HTTP Upgrade
  • WebSocket 세션
  • STOMP 브로커
  • 메시지 라우팅
  • DB 저장
  • 브로드캐스트

여러 계층이 유기적으로 동작하는 구조입니다. 

 

이번 글을 정리하면서 자신이 개발한 기술에 대해 확신을 가지고 제대로된 설명을 하지 못하면 문제상황에서 정체된다는 걸 깨달았습니다. 

앞으로 동작원리를 다른 사람에게 쉬운 말로 설명할 수 있도록 글로 남기며 되새기고자 합니다.

 

 

추가로 17년도 배민 기술블로그에 WebSocket에 대한 이야기가 있어 한번 읽어보고자 합니다. https://techblog.woowahan.com/2547/

'개발 > CS' 카테고리의 다른 글

서버는 왜 죽는가? - Nginx 로드밸런싱  (4) 2026.04.02
그래서 분산이 뭔데  (1) 2026.02.26
'개발/CS' 카테고리의 다른 글
  • 서버는 왜 죽는가? - Nginx 로드밸런싱
  • 그래서 분산이 뭔데
소연노흐
소연노흐
소연노흐가 개발자가 되는 날까지 열심히 달리는 블로그. SpringBoot, Flutter, AI 활용 분야에 관심이 있습니다. 취미는 수영, 친구사냥, 웹소설입니다.
  • 소연노흐
    31세 소연노흐의 마지막 끌어치기
    소연노흐
  • 전체
    오늘
    어제
    • 분류 전체보기 (30)
      • 공지사항 (1)
      • 일상 (0)
        • 일기 (0)
        • SSAFY (0)
      • 개발 (5)
        • TIL (2)
        • CS (3)
        • SpringBoot (0)
        • Flutter (0)
        • AI (0)
      • 프로젝트 (0)
        • 보다펫 (0)
        • 전장가자 (0)
        • 마양식 (0)
        • 뽈뽈뽈 (0)
        • 소원을 빌어봐 (WISH HOUR) (0)
      • 프로그래밍언어 (1)
        • JAVA (0)
        • C++ (1)
      • 코딩테스트 (14)
        • 백준 (13)
      • 인터넷강의 (5)
        • JavaScript (5)
      • 관심있는 포스팅 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    채팅
    백엔드
    백준
    WebSocket
    브론즈
    nginx
    CS기초
    Stomp
    Spring
    시스템설계
    로드밸런싱
    인프라
    웹소켓
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
소연노흐
채팅 기능 구현을 위한 WebSocket + STOMP 구조 정리
상단으로

티스토리툴바