‘그룹별 실시간 위치 공유’라는 기능을 구현하기 위해 프로젝트에서 사용했던 기술은 STOMP였다. 고백하자면 STOMP를 선택하고 사용한 이유에 대해 명확한 근거가 부족했다. 당시 ‘단순히 많이 사용하고 Spring에서의 사용이 좋다’였다. 프로젝트가 끝나고 고찰을 하는 과정에서 실시간 통신에 대해 깊이 있게 탐구해보기로 했다.
우선 일반적인 요청 - 응답 모델과 실시간 통신의 차이점을 짚고 넘어가자
전통적인 클라이언트 - 서버 모델에서 동작하며 HTTP 프로토콜을 기반으로 동작한다.
- 클라이언트는 서버에 요청(Request)하고 서버는 이에 대한 응답(Response)를 제공한다.
- 특징
- 비연결성
- 단방향성
비연결성 (Stateless)
각 요청은 독립적이며 , 이전 요청에 대한 상태를 유지하지 않는다.
즉 (Request - Response)가 종료되면 클라이언트와 서버간의 연결은 유지되지 않는다.
단방향 통신
클라이언트 - 서버간의 역할이 확실하며 서버는 단순히 클라이언트의 요청에 대한 응답의 매개체 역할을 수행한다.
앞서 언급한 비연결성과 단방향 통신이 실시간 통신에 부적합한 이유이다. 실시간 통신이 수행되는 동안 양 측은 서로의 상태를 지속적으로 유지해야 한다. 즉 연결이 지속적이어야 한다. 또한 서로의 데이터를 수신하기 위해 양방향성 통신이 제공되어야 한다.
실시간 통신을 구현하기 위한 방법은 다양하다. 대표적인 방법인 Polling, Long Polling, SSE와WebSocket에 대해 알아보자
일정한 주기를 가지고 클라이언트가 서버와 응답을 주고 받는 방식
- 예를 들어 3초의 주기를 가지고 있다면 3초마다 새로운 데이터를 계속해서 얻어오는 방식이다.
즉 실시간 통신을 위해 Polling 방법을 사용한다는 것은 주기를 짧게 설정해서 지속적인 통신이 이루어지는 것처럼 보이게 한다는 것이다. 하지만 이 과정에서 지속적으로 연결을 만들고 종료하는 과정이 반복되므로 선호되는 방법은 아니다.
Polling 방식의 단점을 보완하기 위한 방법으로서 서버의 연결 시간을 좀 더 길게 유지하여 데이터를 주고받는 방식이다.
- 즉 클라이언트가 서버에 데이터를 요청했을 때 바로 반환하는 것이 아닌 서버와의 접속 시간을 길게 설정하여 서버에서 이벤트가 발생하면 데이터를 반환받도록 하는 방식이다.
Long Polling을 통해 서버에서 이벤트가 발생할때까지 기다렸다가 통신을 통해 기존 Polling의 무자비한 통신을 줄일 수 있다. 하지만 완벽한 대체라고 보기엔 몇 가지를 고려해봐야 한다.
- 이벤트 발생의 주기
- 이벤트 발생의 주기가 짧다면 지속적으로 서버 클라이언트 사이의 연결과 통신이 발생하게 된다. 즉 이벤트가 자주 발생한다면 polling 방식과 큰 차이가 없다는 것이다. (오히려 성능이 더 떨어질 수 있다)
- 다수의 사용자에게 동시에 이벤트 발생 시 ?
- 모든 클라이언트들이 동시에 요청을 보내 서버에 부담 발생
실시간 통신을 구현하는 여러가지 방법 중 SSE라는 방법이 있다. SSE를 한마디로 요약하자면 서버에서만 지속적으로 클라이언트에게 데이터를 전송하겠다는 것이다.
- 앞서 다룬 Polling과 Long Polling의 단점으로는 클라이언트가 요청할때마다 연결이 생성되고 종료된다는 것이다.
- Connection을 요청마다 만들고 종료하는 것은 지속해서 통신이 발생할 경우 큰 오버헤드로 이어질 수 있다.
- 실시간 통신을 위해 클라이언트에서 한번의 요청으로 연결을 생성하고 지속적으로 통신할 수 있다면 이러한 문제를 해결할 수 있다. (궁극적인 실시간 통신을 가능하게 한다)
SSE의 경우 한번의 요청으로 Connection을 생성하고 Server에서 event가 발생할 때마다 클라이언트에게 데이터를 전송한다.
단방향 통신
SSE의 가장 큰 특징이다. SSE는 서버 측에서의 단방향 통신만을 지원한다.
SSE를 사용할 경우 Media Type은 TEXT_EVENT_STREAM이다.
- 위의 값을 헤더를 통해 지정해야 한다. 위 값이 의미하는 것은 한번의 요청을 받고 연결을 끊지 않고 지속한다는 것이다.
사실 SSE는 실시간 통신의 방법 중 하나로 설명하였지만 구현하고자 하는 ‘실시간 위치 공유’와는 맞지 않는 방법이다. 그 이유는 가장 큰 특징인 ‘서버에서 단방향 통신’이다.
- 실시간 위치 공유를 위해서는 클라이언트가 자신의 위치를 서버 측에 지속적으로 전송해야 한다. 서버는 클라이언트로부터 전송받은 위치를 다른 사용자에게 전송해야 한다.
- 즉 위의 로직을 SSE로 구현하기 위해서는 아래 2가지 동작이 계속해서 같이 수행되어야 한다는 것이다.
- SSE 커넥션에서 서버가 데이터를 전송하는 API
- 클라이언트가 자신의 위치를 서버에 전송하는 API
앞서 살펴본 Polling은 단순히 주기를 통해 실시간으로 데이터를 주고 받는 것처럼 보이게 하는 것이었다. 그렇다면 지속적으로 연결을 유지하여 통신하는 방법은 뭐가 있을까? 바로 WebSocket이다.
- 클라이언트와 서버가 전이중(Full-Duplex) 채널을 통해 통신
동작 과정
WebSocket을 통한 통신 과정은 크게 연결 - 통신 - 연결 종료 3단계로 구분할 수 있다.
WebSocket 연결 단계에 있어 가장 중요한 점은 HTTP 위에서 동작한다는 것이다. ( 정확히는 HTTP 기반의 2-hand shake 과정을 거친다. )
- 클라이언트 Request
- 클라이언트는 HTTP 기반의 요청 메시지를 전송한다. 메시지를 요약하자면 ‘앞으로의 통신을 WebSocket 프로토콜 위에서 동작하도록 하자’ 라는 것이다.
```
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
```
서버는 클라이언트의 요청에 대해 HTTP 응답을 반환
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept : 클라이언트가 보낸 Key 값을 공개 키로 암호화한 값이다. 해당 메시지를 클라이언트에서 받았을 때 정상적인 값이라면 연결이 제대로 성공했다는 것이다.
서버와 클라이언트 간의 WebSocket 연결이 성공했다면 이제 전이중 통신이 가능하다.
- 서버와 클라이언트 간에 전송되는 메시지는 일반적인 HTTP 메시지 형식보다 가벼운 구조이다.
- 텍스트 형식이 아닌 데이터 프레임 형식이다.
- 웹 소켓의 메시지는 헤더 + 데이터 로 구분된다.
- 웹 소켓의 데이터는 텍스트 or 바이너리 데이터가 가능하다.
목표했던 ‘그룹원들 간의 위치 정보를 실시간으로 공유’하는데 있어 WebSocket은 적절한 방법이다. 연결이 지속되고 양방향 통신이 가능하기 때문이다. 하지만 WebSocket에도 아쉬운 부분은 있다.
직역하자면 ‘간단한 메시지 전송을 위한 프로토콜‘
- STOMP는 일반적으로 서브 프로토콜로서 WebSocket 위에서 동작한다.
- STOMP의 특징
- 메시지 브로커의 사용
- pub - sub 방식
비동기적 메시지 전송을 위한 중간 미들웨어
- 메시지 브로커를 사용하는 것은 일반적인 통신 방식과 차이점이 있다.
- WebSocket을 사용할 경우 Server와 클라이언트 사이에서 서로 직접 메시지를 전송한다.
- 하지만 중간단에 메시지 브로커를 배치할 경우 메시지 브로커가 각 사용자에게 메시지를 전송한다. (느슨한 결합성)
발행 구독 패턴으로서 비동기 메시징 패러다임이다.
STOMP는 기본적으로 내장되어 있는 SimpleBrocker를 지원한다. 하지만 보통 STOMP를 사용할 때 외부 메시지 브로커를 추가해 사용한다.
- 데이터가 많아진다면, 내장되어있는 SimpleBroker는 철저하게 Spring Boot가 실행되는 곳의 메모리를 잡아먹는다.
- 이를 해결하기 위해 보통 외부 메시지 큐(RabbitMQ)를 많이 사용한다.
‘그룹별 실시간 위치 공유’ 기능을 선택하는데 있어 중요하게 고려해야 하는 것은 2가지이다.