SSE (Server-Sent Events)
SSE는 서버와 클라이언트 간의 단방향 푸시 방식의 실시간 통신 방식으로, 클라이언트가 서버에 지속적으로 연결을 유지하면서 서버로부터 데이터를 수신할 수 있습니다. 이는 챗방, 주식 시세, 경기 결과 등 실시간으로 변화하는 데이터를 제공하는 데 적합합니다.
SSE는 다음과 같은 장점을 제공합니다.
- 실시간 업데이트 제공: 클라이언트가 페이지를 새로고침하지 않고도 서버로부터 최신 데이터를 받아볼 수 있습니다.
- 낮은 대역폭 사용: SSE는 HTTP 요청/응답 방식을 사용하기 때문에 WebSockets보다 대역폭 사용량이 적습니다.
- 쉽게 구현 가능: Spring SSE는 Spring 프레임워크에서 제공하는 SseEmitter 클래스를 사용하여 쉽게 구현할 수 있습니다.
SSE 와 WebSocket의 차이점
WebSocket | Sse ( = EventSource) |
양방향: 클라이언트와 서버 모두 메시지를 교환할 수 있습니다. | 단방향: 서버만 데이터를 보냅니다. |
바이너리 및 텍스트 데이터 | 텍스트만 |
웹소켓 프로토콜 | 일반 HTTP |
Spring SSE를 사용하는 방법은 다음과 같습니다.
- SseEmitter 클래스 사용: Spring MVC 컨트롤러에서 SseEmitter 클래스를 사용하여 SSE 이벤트를 생성하고 클라이언트에게 전송합니다.
- JavaScript 사용: 클라이언트 측에서는 JavaScript를 사용하여 SSE 이벤트를 수신하고 처리합니다
자바스크립트 (클라이언트)
자바스크립트에서 SSE를 다루는 방법은 다음과 같습니다
1. EventSource 객체 생성: SSE를 사용하기 위해 먼저 EventSource 객체를 생성해야 합니다. 이 객체를 사용하여 서버와의 연결을 설정하고 서버로부터 데이터를 받을 수 있습니다.
var eventSource = new EventSource('/sse_endpoint');
여기서 '/sse_endpoint'는 SSE를 지원하는 서버의 엔드포인트 주소입니다.
2. 이벤트 핸들러 등록: 서버로부터 데이터를 받으면 이를 처리하기 위해 이벤트 핸들러를 등록합니다.
eventSource.onmessage = function(event) {
var data = JSON.parse(event.data);
// 받은 데이터를 처리
};
서버가 데이터를 보내면 onmessage 핸들러가 호출되고, 이벤트 객체의 data 속성을 통해 전송된 데이터에 접근할 수 있습니다. 이 핸들러는 사용자 정의 이벤트가 아니라 모든 메시지를 다루는 것이기 때문에 특정 핸들러에 한정되지 않고 모든 종류의 메시지를 받을 수 있습니다.
3. 기타 이벤트 핸들러 등록: SSE는 연결이 끊기거나 에러가 발생할 때 이를 처리하기 위한 이벤트 핸들러도 제공합니다.
eventSource.onerror = function(event) {
// 에러 처리
};
eventSource.onopen = function(event) {
// 연결이 열릴 때 처리
};
eventSource.onclose = function(event) {
// 연결이 닫힐 때 처리
};
이러한 핸들러를 등록하여 연결 상태의 변화나 오류를 적절히 처리할 수 있습니다.
기타 이벤트 핸들러 이외에도 서버 개발자가 추가한 핸들러에서만 보내는 메세지를 받으려면 (한정적으로 받으려면)
클라이언트 측에서 해당 이벤트에 대한 이벤트 핸들러를 추가해야 합니다.
sseSource.addEventListener('connect', function(event) {
console.log('Custom 이벤트:', event.data);
// 원하는 동작 수행
});
위의 코드에서 서버는 "connect"라는 이름의 이벤트를 보내고, 클라이언트에서는 이벤트 핸들러를 등록하여 해당 이벤트를 수신하고 처리합니다. 따라서 클라이언트는 서버의 특정 핸들러가 보낸 이벤트를 수신할 수 있습니다.
4. 연결 닫기: SSE 연결을 더 이상 사용하지 않을 때는 명시적으로 닫아주는 것이 좋습니다.
eventSource.close();
스프링 (서버)
스프링에서 SSE를 다루는 방법은 다음과 같습니다
spring framework 4.2부터 SSE 통신을 지원하는 SseEmitter API를 제공합니다. 이를 이용해 SSE 구독 요청에 대한 응답을 할 수 있습니다.
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
@Slf4j
public class SseController {
private final SseEmitters sseEmitters;
public SseController(SseEmitters sseEmitters) {
this.sseEmitters = sseEmitters;
}
@GetMapping(value = "/sse_endpoint", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> connect() {
SseEmitter emitter = new SseEmitter();
sseEmitters.add(emitter);
try {
emitter.send(SseEmitter.event()
.name("connect")
.data("connected!"));
} catch (IOException e) {
throw new RuntimeException(e);
}
return ResponseEntity.ok(emitter);
}
}
생성자를 통해 만료시간을 설정할 수 있습니다. 디폴트 값은 서버에 따라 다릅니다. 스프링 부트의 내장 톰캣을 사용하면 30초로 설정됩니다. 만료시간이 되면 브라우저에서 자동으로 서버에 재연결 요청을 보냅니다.
SseEmitter emitter = new SseEmitter(60 * 1000L);
이때 생성된 SseEmitter 객체는 향후 이벤트가 발생했을 때, 해당 클라이언트로 이벤트를 전송하기 위해 사용되므로 서버에서 저장하고 있어야 합니다.
sseEmitters.add(emitter);
주의해야할 점은 Emitter를 생성하고 나서 만료 시간까지 아무런 데이터도 보내지 않으면 재연결 요청시 503 Service Unavailable 에러가 발생할 수 있습니다. 따라서 처음 SSE 연결 시 더미 데이터를 전달해주는 것이 안전합니다.
이벤트 이름을 설정해주면 클라이언트에서 해당 이름으로 이벤트를 받을 수 있습니다.
emitter.send(SseEmitter.event()
.name("connect") // 해당 이벤트의 이름 지정
.data("connected!")); // 503 에러 방지를 위한 더미 데이터
SseEmitter를 생성할 때는 비동기 요청이 완료되거나 타임아웃 발생 시 실행할 콜백을 등록할 수 있습니다. 타임아웃이 발생하면 브라우저에서 재연결 요청을 보내는데, 이때 새로운 Emitter 객체를 다시 생성하기 때문에(SseController의 connect()메서드 참조) 기존의 Emitter를 제거해주어야 합니다. 따라서 onCompletion 콜백에서 자기 자신을 지우도록 등록합니다.
주의할 점은 이 콜백이 SseEmitter를 관리하는 다른 스레드에서 실행된다는 것입니다. 따라서 thread-safe한 자료구조를 사용하지 않으면 ConcurrnetModificationException이 발생할 수 있습니다. 여기서는 thread-safe한 자료구조인 CopyOnWriteArrayList를 사용하였습니다.
@Component
@Slf4j
public class SseEmitters {
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
SseEmitter add(SseEmitter emitter) {
this.emitters.add(emitter);
log.info("new emitter added: {}", emitter);
log.info("emitter list size: {}", emitters.size());
emitter.onCompletion(() -> {
log.info("onCompletion callback");
this.emitters.remove(emitter); // 만료되면 리스트에서 삭제
});
emitter.onTimeout(() -> {
log.info("onTimeout callback");
emitter.complete();
});
return emitter;
}
}
이제 서버에서 무언가 변경 사항이 생겼을 때, 클라이언트의 요청이 없어도 데이터를 전송할 수 있습니다. 본 예시에서는 누군가 /count를 호출하면 서버에 저장된 숫자를 1 증가시키고 이를 SSE 커넥션이 열려있는 모든 클라이언트에게 전달하도록 해보겠습니다.
@RestController
@Slf4j
public class SseController {
private final SseEmitters sseEmitters;
public SseController(SseEmitters sseEmitters) {
this.sseEmitters = sseEmitters;
}
// ...
@PostMapping("/count")
public ResponseEntity<Void> count() {
sseEmitters.count();
return ResponseEntity.ok().build();
}
}
@Component
@Slf4j
public class SseEmitters {
private static final AtomicLong counter = new AtomicLong();
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
// ...
public void count() {
long count = counter.incrementAndGet();
emitters.forEach(emitter -> {
try {
emitter.send(SseEmitter.event()
.name("count")
.data(count));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
클라이언트에서는 `count`라는 이름의 이벤트가 발생할 때 콘솔에 변경된 데이터를 출력하도록 이벤트 리스너를 등록해둡니다.
sse.addEventListener('count', e => {
const { data: receivedCount } = e;
console.log("count event data",receivedCount);
setCount(receivedCount);
});
이제 만약 다른 브라우저에서 count를 호출한다면 내 브라우저에 서버에서 변경된 값이 찍히게 됩니다.
https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events/Using_server-sent_events
https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/
'STUDY > SpringLegacy' 카테고리의 다른 글
[Spring] Bean 등록하는 다양한 방법 (1) | 2024.05.21 |
---|---|
[Spring] @Resource, @Autowired, @Inject 차이 (0) | 2024.05.21 |
[Spring] Log4j2 설명 (0) | 2024.04.29 |
[Spring] Log4j2 사용해 상세한 SQL 쿼리 로그 출력 설정 (0) | 2024.04.29 |
[Spring] Lombok 설치 및 STS 연동하기 (0) | 2024.04.24 |