Backend
home
📩

Server Sent Events (10분 테코톡)

생성 일시
2025/06/15 15:46
태그
SpringBoot
게시일
2025/06/16
최종 편집 일시
2025/06/15 16:28

궁금증

웹 개발을 하면서 가장 궁금했던 것은 따로 새로고침을 하지 않아도 웹 페이지가 변한다는 것이었다.
새로고침을 한다? ⇒ 새로운 요청을 보낸다.
하지만 새로고침을 하지 않아도 근데 요청없이 응답이 온다.
체스 플랫폼 사례 - 실시간으로 화면이 바뀐다.

HTTP 프로토콜

요청이 있어야 응답을 보낼 수 있다.
그러면 클라이언트에서 요청이 없는데 서버에서 응답을 보낼 수 있을까? ⇒ 불가능하다.

웹 소켓

양방향 통신을 위해 도입된 프로토콜이다.
http 프로토콜이 아닌 ws 프로토콜을 사용한다.

HTTP 프로토콜을 이용한 실시간 소통 방법

Polling

클라이언트에서 주기적으로 요청을 보내 응답을 받는다.
일반적인 http 통신과 같이 클라이언트에서 리퀘스트(요청)를 보내고 서버에서 응답을 보낸다.
그리고 주기적으로 n초마다 요청과 응답을 반복한다.
단점
주기적으로 일어나기 때문에 실시간이 아니다.
쓸데없는 요청이 계속 발생해서 서버에 부하가 생긴다.

Long Polling

응답이 도착하면 요청을 보내고 대기한다.
클라이언트에서 요청을 보내고 서버에서 응답이 올 때까지 기다린다.
서버에서 변화가 생겨서 응답을 보내면 그 즉시 클라이언트에서 다시 요청을 보내고 대기하는 방법이다.
기존 폴링 방법의 단점을 보완했지만 요청과 응답이 여전히 1대 1이라는 점에서 서버에 부하가 발생할 수 있다.

SSE(Server Sent Events)

한 번 연결 후 지속적으로 응답을 받는다.
클라이언트에서 단 한 번의 요청으로 연결을 지속하고 서버 측에서 변화가 생길 때마다 응답을 반복해서 보낼 수 있다.
서버에서 실시간으로 이벤트를 전송한다.
polling 기법보다 적은 통신 횟수를 가진다.
새로운 프로토콜을 익힐 필요가 없다.
서버 측에서의 단방향 통신이다.

SSE(Server Sent Events) 코드

기본적으로 MediaType으로 TEXT_EVENT_STREAM_VALUE 라는 값을 사용한다.
이 값이 ContentType 헤더에 들어오게 되면 클라이언트는 Server Sent Events라는 걸 인식하게 되고 한 번의 응답으로 연결을 끊지 않는다.
다음과 같이 response에서 가져온 writer로 writer.flush()를 할 때마다 클라이언트 측에 메시지를 보내게 된다.

SSE(Server Sent Events) 적용 예시

오른쪽이 웹 브라우저
커넥트에 접속했을 때 실시간으로 페이지가 업데이트 된다.

SseEmitter

이전과 같은 방법으로 실용적인 구현을 하기가 어려운데 스프링에서는 Server Sent Event를 위해 emitter인 SseEmitter 객체를 지원한다.
생성자에 파라미터의 타임아웃 시간을 넣을 수 있다.
setConfing는 emitter의 특성을 세팅하기 위한 임의의 메소드이다.
SseEmitter에서 제공하는 콜백 함수를 지정할 수 있다.
순서대로 타임아웃 했을 때 콜백, 완료했을 때 콜백, 에러가 났을 때 콜백이다.
해당 라인은 세션 매니저와 같은 emitter 레포지토리에 현재 emitter를 추가하는 과정이다.
반환값으로 emitter 객체를 반환하면 이 emitter에 메시지가 발생할 때마다 해당 클라이언트에게 응답을 보내게 된다.
응답을 확인하기 위한 임의의 send uri이다.
레포지토리에 모든 emitter에게 이벤트를 발송하는 메소드이다.
SseEmitter는 다음과 같이 이벤트를 발생시키기 위한 정적 메소드를 제공한다.
다음 메서드는 id에 LocalDateTime을 넣어줬고, 이름에는 메시지, 데이터에는 “Hello, world!”를 넣어준 모습이다.
예시 (오른쪽이 브라우저)
send uri에 get 요청을 보낼 때마다 sse connecto에는 다음과 같은 응답이 가게 된다.
뒷부분의 시간이 변화하는 것을 확인할 수 있다.

SSE 적용 후 예상되는 문제

emitter Repository는 JVM 메모리에서 작동하기 때문에 컬렉션에서 동시성 문제가 발생할 수 있다.
ThreadSafe한 컬렉션을 사용한다.
ConcurrentHashMap이나 CopyOnWriteArrayList를 사용할 수 있다.
JVM 메모리를 사용하다 보면 다중 WAS 환경일 때 문제가 발생할 수 있다.
세션 매니저나 refreshToken을 관리하는 것과 같이 다중 WAS 환경에 대한 대책을 강구해야 한다.
Redis pub/sub 라는 구독 발행툴을 사용한다.
Open In View의 커넥션 점유 문제가 발생한다(JPA를 사용할 때).
Open In View는 기본적으로 true인데 이는 트랜잭션 단위가 아닌 세션 단위에서 영속성 컨텍스트를 공유하기 때문에 그 영속성 컨텍스트를 띄워놓는 동안 데이터베이스의 커넥션을 계속해서 점유하게 된다.
따라서 SSE를 사용할 때는 Open In View를 필수적으로 false로 설정해야 한다.

적용 후 예상되는 문제 재정리

1. emitter Repository는 JVM 메모리에서 작동하기 때문에 동시성 문제가 발생할 수 있다.
SSE에서는 사용자마다 SseEmitter 객체를 만들어 서버에서 유지한다. 이걸 관리하기 위해 보통 Map<Long, SseEmitter> 같은 형태로 emitter들을 저장합니다.
하지만 이 Map이 단순한 HashMap일 경우, 여러 스레드(클라이언트)가 동시에 접근해서 데이터를 넣거나 꺼낼 때 충돌(경쟁 상태)이 생길 수 있다. 즉, 동시성 문제가 발생할 수 있다.
해결 방법
ConcurrentHashMap: 여러 스레드가 동시에 안전하게 접근할 수 있도록 만들어진 Map
CopyOnWriteArrayList: 읽기 중심이고 변경이 적은 상황에서 쓰기 좋은, 스레드 안전한 List
이런 Thread-safe 컬렉션을 사용하면 여러 유저가 동시에 SSE 연결을 시도하더라도 문제가 발생하지 않는다.
2. JVM 메모리를 사용하다 보면 다중 WAS 환경에서 문제가 발생할 수 있다.
SSE 연결은 서버가 클라이언트에게 실시간으로 데이터를 보내는 기능이다. 그런데 WAS가 여러 개 있는 (예: 서버 2대 이상 사용하는) 환경에서는 이런 문제가 발생할 수 있다:
클라이언트 A는 서버1에 연결되었는데, 관련 이벤트가 서버2에서 발생했다면 서버 1은 이 이벤트를 감지하지 못할 수 있다.
JVM 메모리 내에서 emitter가 관리 중이기 때문이다.
즉, 서버 간 emitter 정보가 공유되지 않음 → 실시간 이벤트 누락
해결 방법
Redis Pub/sub 사용
서버 간 메시지를 주고받을 수 있는 시스템
서버1에서 이벤트를 발행(Publish) 하면 서버2도 그 메시지를 구독(Subscribe)하고 같은 메시지를 클라이언트에게 전달 가능
서버 여러 대가 있어도 이벤트를 동기화할 수 있다.
3. Open In View의 커넥션 점유 문제 (JPA 관련)
Spring Boot에서는 기본적으로 Open-In-View 옵션이 켜져 있다 (spring.jpa.open-in-view=true).
이 설정은 컨트롤러 실행이 끝난 후에도 DB 연결(Session)을 열어둔다.
그래서 View(JSP, Thymeleaf 등)에서도 DB에 접근 가능하게 해준다.
하지만 SSE는 연결을 몇 분, 몇 시간 동안 유지한다.
즉, 이 설정을 켜두면 DB 커넥션이 그 시간 동안 계속 점유됨 → DB 커넥션 고갈 위험
해결 방법
spring.jpa.open-in-view=false 로 설정
영속성 컨텍스트는 서비스 계층에서 트랜잭션 단위로만 사용
SSE에서는 DTO로 필요한 데이터만 미리 가져와서 보내야 함
SSE에서는 가능한 한 빨리 DB 커넥션을 끊는 것이 중요하다.
문제
설명
해결
JVM 내 동시성
emitter 컬렉션은 여러 스레드 접근 → 충돌 위험
ConcurrentHashMap 등 사용
다중 WAS 환경
서버 간 emitter 정보 공유 안 됨
Redis Pub/Sub로 이벤트 공유
Open In View
DB 연결이 너무 오래 유지됨
open-in-view=false 설정