알림 기능을 구현해보자 - SSE(Server-Sent-Events)!
Spring에서 Server-Sent-Events 구현하기
진행 중인 프로젝트에 실시간 알림 기능 구현이 필요해졌습니다.
실시간 알림 기능은 클라이언트가 서버에게 요청을 보내는 기존 기능과는 달리, 서버가 클라이언트에게 데이터를 보내면 클라이언트가 이를 인지하고 알림을 띄워줘야하는 구조를 가지고 있습니다.
HTTP 프로토콜의 주요 특징은 비연결성 입니다.
따라서 위와 같은 경우, Server가 전송하고 싶어도 해당 Client와 지속적으로 연결이 되어있지 않기 때문에 보낼 수 없는 상황이 발생하게 되는 것입니다.
이를 해결하는 방식으로 폴링, 긴 폴링, 소켓, SSE 총 네 가지가 존재합니다.
하단 이미지는 알림 기능을 구현해보자 - SSE(Server-Sent-Events)! 블로그의 예제를 차용했습니다!!
Client가 주기적으로 Server로 요청을 보내는 방식입니다.
일정시간마다 client가 Server로 요청을 보내 데이터 갱신이 있는지 확인하고, 갱신이 되면 응답을 받는 방식입니다.
구현이 단순하지만, 계속 요청을 해야한다는 점에서 리소스 낭비가 발생합니다.
구현이 단순하다는 장점이 있기 때문에, 요청하는데 부담이 크지 않고, 요청 주기를 넉넉하게 잡아도 될 정도로 실시간성이 중요하지 않다면 , 그리고 데이터 갱신이 특정 주기를 갖는다면 해당 방법이 적합할 수 있습니다.
유지 시간을 조금 더 길게 갖는다는 점에서 Polling과 차이점이 존재합니다.
즉, 요청을 보내고 서버에서 변경이 일어날 때까지 대기하는 방법입니다.
처음에 긴 Connection을 갖게 Request를 보내고, 이 지속 시간 동안 이벤트가 발생하면 그 이벤트에 대한 값을 유지되고 있는 Connection 통해서 보내면 됩니다.
이렇게 된다면, Connection이 연결되는 동안은 이벤트 발생을 실시간으로 감지할 수 있어집니다. 그리고 지속적으로 요청을 계속 보내지 않기 때문에 Polling 방식보다는 부담이 덜합니다.
하지만 유지 시간을 짧게 갖는다면 Polling 방식과 차이점이 없고, 지속적으로 연결되어 있기 때문에 다수의 클라이언트에게 동시에 이벤트가 발생하면 순간적 부담이 급증합니다. 즉, 이벤트가 자주 발생하면 부담이 도리 수 밖에 없는 것입니다.
Long Polling 방식은 실시간 전달이 중요한데 상태가 빈번하게 갱신되진 않을 때 적합합니다.
웹소켓은 HTTP와 같은 프로토콜의 일종으로 양방향 통신을 실현하기 위한 구조입니다.
최초 접속은 일반 HTTP 요청을 이용한 handshaking으로 이뤄지는데, HTTP와 같이 연결 후 끊어버리는 것이 아니라 계속적으로 Connection을 지속하므로 연결에 드는 불필요한 비용을 제거할 수 있습니다.
또한 웹소켓을 활용하면, (용량이 큰) Http Header를 최초 접속시에만 보내고 더이상 보내지않으므로 리소스면에서 이득을 볼 수 있습니다. 그리고 웹소켓 포트에 접속해있는 모든 클라이언트에게 이벤트 방식으로 응답할 수 있습니다.
SSE는 웹소켓과 달리, Client가 Server로부터 데이터만 받을 수 있는 방식입니다.
SSE는 별도의 프로토콜을 사용하지 않고 HTTP 프로토콜만으로 사용할 수 있기 때문에 구현이 용이합니다.
접속에 문제가 있으면 자동으로 재연결 시도하지만, 클라이언트가 close해도 서버에서 감지하기 어렵습니다.
구현하고자하는 실시간 알림 기능은
1. 서버에서 클라이언트로 데이터를 전송해야하는데
2. 클라이언트는 서버로 데이터를 전송할 필요가 없고
3. 실시간성이 중요한
기능입니다.
실시간성이 중요하므로 Polling, Long Polling 방식은 적합하지 않다고 판단하였고
서버 -> 클라이언트인 단방향통신을 요구하므로, WebSocket보다 구현이 단순한 SSE가 적합하다고 판단하였습니다.
클라이언트측에서 우선 서버의 이벤트 구독을 위한 요청을 보내야 합니다.
이벤트의 미디어 타입은 text/event-stream
이 표준으로 정해져있습니다.
Response의 미디어 타입은 text/event-stream
입니다. 이때 Transfer-Encoding
헤더의 값을 chunked
로 설정하는데, 왜냐하면 서버는 동적으로 생성된 컨텐츠를 스트리밍 하기 때문에 본문의 크기를 미리 알 수 없기 때문입니다.
클라이언트에서 subscribe를 하면, 서버는 해당 클라이언트에게 비동기적으로 데이터를 전송할 수 있습니다.(데이터는 utf-8로 인코딩된 텍스트 데이터만 가능합니다.)
서로 다른 이벤트는 \n\n 로 구분되며, 각각의 이벤트는 한 개 이상의 name:value
로 구성됩니다. (이벤트 안의 내용은 \n으로 구분됩니다.)
Spring Framework는 4.2부터 SseEmitter
클래스를 제공하여, 서버 사이드에서의 SSE 통신 구현이 가능해졌습니다.
Spring Boot에서의 SSE 기능 구현은 다음 게시글에 이어서 작성하겠습니다 :)