← 뒤로
프로젝트 요약
- 한 줄 요약: REST API 폴링을 제거하고 WebSocket와 SSE를 비교 검증하여, 300개 동시 연결에서 에러율 0%·메시지 손실 0%를 달성한 WebSocket(STOMP)을 선택해 실시간 메시지 수신을 구현했습니다.
- 진행/소속: 개인 프로젝트
- 키워드:
WebSocket, STOMP, SSE, 실시간통신, 성능테스트, Redis Pub/Sub, 수평확장
- 기간: 3일
- 역할: 백엔드
문제(AS-IS)

- 메시지 도메인은 REST API만 제공 (POST/GET /messages)
- 클라이언트가 새 메시지를 받으려면 폴링 또는 수동 새로고침 필요
- 메시지 서비스 핵심 가치인 즉시 전달 미달성
목표(TO-BE)
- 실시간 메시지 수신: 발송 즉시 수신자에게 푸시 (폴링 제거)
- 기술 선택 근거 마련: WebSocket vs SSE 비교 후, 서비스 요구사항에 맞는 방식 선정
설계/선택(Key decisions)

WebSocket (STOMP) 방식 최종 선택
- 비교 검증: WebSocket/SSE POC 구현 및 성능 테스트
- 수평 확장 지원:
Redis Pub/Sub+ Sticky Session으로 수평 확장 지원
- 연결 안정성: Heartbeat 설정 (10초 간격), Rolling Update & Graceful Shutdown
- 프로토콜 문서화:
AsyncAPI 3.0 기반으로 테스트해볼 수 있는 Web 문서 생성
- 깔끔한 코드 구조 채택:
AOP 어노테이션(@WebsocketSend)으로 메시지 전송부 보일러플레이트 제거
Port-Adapter 패턴으로 인프라에 의존하지 않는 도메인 로직 작성
결과(Impact)
- 정량 성과 (테스트 환경: 동시 연결 50~300개, 네트워크 지연 50ms 시뮬레이션):
- 에러율: 0%
- 메시지 손실: 0건
- 레이턴시: P99 101~263ms
- 연결 수 증가에 따른 변화가 적은 것으로 확인
- 정성 성과:
- 서버 리소스 감소: 폴링 제거로 불필요한 HTTP 요청 감소
- 사용자 경험 개선: 메시지 송신부터 실제 수신까지 딜레이 감소
- 문서화: 웹 기반 테스트가 가능한 문서 제작으로 프론트와 소통 효율화
아래 링크에서 실제 구현 확인 가능
message.rahoon.site/websocket-docs/index.html
목차
구현 상세
1) 기술 비교 및 선택
배경: 실시간 통신을 위해 WebSocket/SSE 각각의 기술을 이론 비교 및 POC 구현 후 비교
이론 비교
| 특징 |
SSE |
웹소켓 |
| 데이터 전송 방향 |
단방향 |
양방향 |
| 프론트 개발 난이도 |
(웹)간단, (모바일) 보통 |
보통 |
| 연결 감지 |
서버에서 클라이언트 종료 감지 지연 |
즉각적인 연결 상태 변화 감지 |
| 데이터 유형 |
텍스트만 지원 |
텍스트 및 바이너리 지원 |
| 프로토콜 |
HTTP 기반 |
TCP 기반 |
| 사용 사례 |
주식 시세, SNS 알림 등.. |
채팅, 멀티플레이게임, 협업도구 등.. |
참고:
- KakaoTalk: SSE -> LOCO(자체 TCP 프로토콜)
- Whats App: XMPP -> Noise(자체 TCP 프로토콜)
- Instagram, Messenger -> MQTT(앱), Websocket(웹)
- Discord -> Websocket
-> 양방향성이 중요한 메시지 서비스에서는 Websocket이 유리해보임
POC 구현 방법
- WebSocket (STOMP):
- 서버: Spring WebSocket + STOMP 메시징 프로토콜
- 클라이언트: Nodejs, Sockjs + WebStomp-client
- SSE
- 서버:
SseEmitter 기반 Server-Sent Events
- 클라이언트: Nodejs, EventSource 라이브러리
테스트 1: 연속 3개 메시지 수신 테스트
방법) 10명 구독자, 메시지 3개 연속 전송(50ms 간격), 200회 반복
결과)
- WebSocket: 성공률 100% (P99 381ms)
- SSE: 성공률 100% (P99 380ms)
결론) 큰 차이 없음
테스트 2: 동시 연결 부하 테스트
방법) 동시 연결을 50개부터 300개까지 단계별로 증가하며 메시지 수신 확인
결과)
- WebSocket: 메시지 손실 0%, P50 91~257ms
- SSE: 메시지 손실 1~4%, P50 92ms→581ms
- 재연결 타이밍에 메시지 손실 발생한 것으로 확인됨
- Websocket 수준의 안정성을 위해서는 LastEventId 구현 필요
- EventSource에서 각 세션이 시작될 때마다 서버에서 Zombie Connection 생성됨
- HTTP 기반 스트리밍 특정상 서버에서 클라이언트 종료를 즉각 감지하기 어렵기 때문
- 메시지서비스처럼 연결 밀도가 높은 경우 리스크가 될 수 있다고 생각됨
결론) Websocket을 선택할 이유가 늘어남
최종 결론: WebSocket 채택
근거)
- 검증된 산업 표준: Discord, Instagram, Messenger 등 대규모 서비스에서 WebSocket 기반이거나 비슷한 TCP 방식 통신 채택, 검증된 기술 스택
- 동시접속 300명 테스트 결과: 테스트 결과 WebSocket이 비교적 안정적이며, SSE는 동일 수준의 신뢰성을 위해 추가 구현이 필요할 것으로 예상
- 양방향 통신 기반 확장성: 읽음 처리, 타이핑 인디케이터 등 클라이언트→서버 실시간 피드백 구현 용이
- 전송 신뢰성: Heartbeat 및 ACK/Receipt 프레임으로 클라이언트 수신 여부와 세션 생존 상태를 프로토콜 수준에서 확인 가능
- 리소스 관리: 장기 연결 지향 프로토콜로 SSE 대비 안정적이며, Close Frame을 통한 좀비 커넥션 즉시 정리 가능
- 운영 안정성: SSE의 재연결 타이밍 메시지 손실 및 Zombie Connection 문제를 프로토콜 수준에서 해결
2) 깔끔한 코드 구조: @WebsocketSend AOP
목적:
- 웹소켓 전송부를 쉽게 파악하여 AsyncAPI 문서로 작성하기 위함
- 반복되는 보일러 플레이트 코드 제거하기 위함
변화
// MessageWebsocketController class -- Before
@Async
@EventListener
fun sendCreatedMessage(message: MessageEvent.Created){
simpleMessagingTemplate.convertAndSend(
"/topic/chat-rooms/${message.chatRoomId}/messages",
MessageWsSend.Detail.from(message)
)
}
// MessageWebsocketController class -- After
@Async
@EventListener
@WebsocketSend("/topic/chat-rooms/{chatRoomId}/messages")
fun sendCreatedMessage(event: MessageEvent.Created): MessageWsSend.Detail {
return MessageWsSend.Detail.from(message)
}
// WebsocketSend의 chatRoomId는 응답값 MessageWsSend.Detail을 SPEL로 조회하여 조합됨
3) 수평 확장 지원: Redis Pub/Sub
배경:
- 서버-A와 서버-B로 수평 확장된 상황에서
- 유저가 서버-A에 WebSocket을 Connect 한 상황일때
- 서버-B에서 메시지 생성 요청을 처리한 경우 MessageEvent.Created발급됨
- 이렇게 발급된 MessageEvent.Created는 서버-A까지 전달되지 않음
해결:
- 서버-A와 서버-B로 수평 확장된 상황에서
- 유저가 서버-A에 WebSocket을 Connect 한 상황일때
- 서버-B에서 메시지 생성 요청을 처리한 경우 MessageEvent.Created발급됨
- 서버-B에서는 MessageEvent.Created를 수신하여 MessageCommandEvent.Send로 변환한 뒤 Redis를 통해 적절한 서버로 전송
- 각 서버는 MessageCommandEvent.Send를 구독했다가 내부 이벤트로 발행
- MessageCommandEvent.Send를 수신하여 Websocket 전송
4) 프로토콜 문서화: AsyncAPI 3.0
배경: WebSocket 프로토콜 문서화 부재로 클라이언트 개발자와의 협업 어려움이 예상됨
접근:
AsyncAPI 3.0 스펙 기반 자동 문서 생성 (AsyncApiFullGenerator)
- Generic/List 타입 스키마 처리 및 canonical name 기반 중복 방지
- 웹 UI에서 스키마 기반 예제 자동 생성
포인트:
- WebSocket 엔드포인트와 메시지 스키마를 자동으로 문서화하여 API 변경 시 실시간 반영
@WebsocketSend 어노테이션 파싱하여 RECEIVE 액션으로 문서화
- 기존 AsyncAPI 문서와 다르게 WebSocket을 직접 테스트해볼 수 있도록 프론트엔드 구성