궁금증
•
웹 개발을 하면서 가장 궁금했던 것은 따로 새로고침을 하지 않아도 웹 페이지가 변한다는 것이었다.
•
새로고침을 한다? ⇒ 새로운 요청을 보낸다.
•
하지만 새로고침을 하지 않아도 근데 요청없이 응답이 온다.
•
체스 플랫폼 사례 - 실시간으로 화면이 바뀐다.
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
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로 필요한 데이터만 미리 가져와서 보내야 함
문제 | 설명 | 해결 |
JVM 내 동시성 | emitter 컬렉션은 여러 스레드 접근 → 충돌 위험 | ConcurrentHashMap 등 사용 |
다중 WAS 환경 | 서버 간 emitter 정보 공유 안 됨 | Redis Pub/Sub로 이벤트 공유 |
Open In View | DB 연결이 너무 오래 유지됨 | open-in-view=false 설정 |