00. 서버에서 클라이언트로 이벤트를 보내야 하는 필요성
서버에서 클라이언트에게 이벤트 처리를 해주어야 하는 경우는 생각보다 쉽게 떠올릴 수 있다.
예를 들어 네이버 카페에서의 채팅 기능을 생각해보자.
클라이언트가 특정 유저에게 채팅을 보내면, 해당 정보는 상대방에게 바로 가지 않고 먼저 서버로 전송된다.
이후 상대 유저가 채팅 내용을 얻기 위해서 서버에 해당 채팅 내용을 요청해야한다.
여기서 한가지 쟁점은 상대 유저는 자신에게 채팅이 온 사실을 어떻게 인식하는가? 이다.
이러한 상황에서는 서버에서 클라이언트에게 이벤트를 전송해야한다.
서버가 채팅 전송 요청을 받으면, 상대 유저에게 채팅이 왔다는 이벤트 처리를 해주면 위의 문제가 해결된다.
이 외에도 푸쉬알림, 메일, 공지사항 등 다양한 기능에서 서버의 이벤트 처리를 필요로 한다.
필자의 경우는 보드게임 레지스탕스 아발론을 클라이언트와 협업하며, 각 클라이언트의 상태 관리를 하기 위한 용도로 서버의 이벤트 처리를 필요로 했다.
01. 서버에서 이벤트 전송하는 방법들
서버의 이벤트 처리 방식은 아래와 같은 것들이 있다.
간략하게 살펴보자
- Polling
클라이언트가 일정 시간 간격으로 서버에 계속 요청을 보내는 방식.
구현이 간편하지만, 서버의 부담이 크다. - Long Polling
- 위의
Polling
방식과 유사하다.Polling
은 이벤트가 발생하든 말든, 서버에 계속해서 요청을 보낸다면,Long Polling
은 서버가 요청을 받으면 응답을 바로 보내지 않고, 이벤트가 발생할 때 까지 기다린 뒤 응답하는 방식이다. - 즉 클라이언트는 계속해서 요청을 보내는 것이 아니라, 하나의 요청을 보내고 응답이 올 때 까지 대기한다. 이후 응답이 오면 새로운 요청을 보낸다.
- 해당 방법은 이벤트 발생량이 적다면
Polling
보다는 서버 부하가 적을 수 있지만, 반대로 이벤트가 자주 발생하는 경우에는 오히려 부하가 더 커진다.
- 위의
- 소켓통신
- 양방향 통신, 연결된 서로가 서로에게 모두 데이터를 보낼 수 있다.
즉 서버도 클라이언트에게 요청을 보낼 수 있다.
- 양방향 통신, 연결된 서로가 서로에게 모두 데이터를 보낼 수 있다.
- SSE
- Server Sent Event*, 서버에서 클라이언트로 실시간 이벤트를 전달하는 웹 기술
02. SSE(Server Sent Event)란?
SSE 개념
SSE
는 서버에서 클라이언트로 실시간으로 데이터를 전송하는 기술이다.
아래와 같은 특징들이 있다.
- 단방향 통신: 서버에서 클라이언트 방향으로만 통신 가능
- 간편한 구현: HTTP 프로토콜을 사용하여 간편하게 구현할 수 있다.
- 브라우저 호환성: 대부분의 브라우저에서 해당 기능을 지원한다.
SSE 응답 구조
Content-Type
:text/event-stream
- 응답 구조:
- 각 이벤트는 여러 줄의 텍스트로 이루어져 있음
- 각 이벤트 끼리는 비어있는 라인을 사용하여 구분
- 각 이벤트는 아래의 필드를 포함한다.
data
: 실제로 전송 할 데이터 값event
: 이벤트의 유형을 구분id
: 이벤트의 고유 id값
- 콜론(:) 이후의 문자는 주석으로 처리됨
- ex
event: update
data: {"temperature": 25.5, "humidity": 60}
: This is a comment
event: alert
data: System maintenance scheduled
data: This is a message without an event type
03. 코드
HttpServletResponse를 사용해서 직접 구현한 코드
@RestController
public class TestController {
@GetMapping("/sse")
public void handleMultiEventSse(HttpServletResponse response) throws IOException, InterruptedException {
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
PrintWriter writer = response.getWriter();
for (int i = 0; i < 3; i++) {
// 일반 메시지 이벤트
writer.write("id: 1\n");
writer.write("event: 1번\n");
writer.write("data: 1번 케이스입니다.\n\n");
writer.flush();
Thread.sleep(1000);
// 경고 이벤트
writer.write("id: 2\n");
writer.write("event: 2번\n");
writer.write("data: 2번 케이스 입니다.\n\n");
writer.flush();
Thread.sleep(1000);
// JSON 데이터 이벤트
writer.write("id: 3\n");
writer.write("event: 3번\n");
writer.write("data: 3번 케이스 입니다.\n\n");
writer.flush();
Thread.sleep(1000);
}
writer.close();
}
}
다양한 케이스에 대하여 1초 텀을 두고 이벤트를 발생시키는 예제이다.
SseEmitter를 사용한 코드
스프링부트에서 지원하는 SseEmitter를 사용하여 더욱 간편하게 만들 수도 있다.
@RestController
public class TestController {
private final ExecutorService executorService = Executors.newCachedThreadPool();
@GetMapping(path = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamSseMvc() {
SseEmitter emitter = new SseEmitter();
executorService.execute(() -> {
try {
for (int i = 0; i < 5; i++) {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.data("테스트 " + i)
.id(String.valueOf(i))
.name("test");
emitter.send(event);
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return emitter;
}
}
SseEmitter
패키지를 사용해서 더욱 간편하게 구현할 수 있다.
위 예제에서는 응답을 비동기로 처리하기 위해 executeService
를 사용했다.
실제 사용에서는 emitter
를 먼저 리턴 후, 이벤트 발생시 emitter.send()
메소드를 호출하면 이벤트가 클라이언트에게 전송된다.
모든 이벤트 전송 후 연결을 종료할 때는 emitter.complete()
를 호출하면 된다.
실행 결과
HttpServletResponse를 사용해 원시적으로 구현한 코드
SseEmitter를 사용해 구현한 코드
Reference
'Back End > Spring && Spring Boot' 카테고리의 다른 글
[Spring Boot 3.~] 스프링부트에서 JPA 사용하기 (0) | 2024.10.11 |
---|---|
[EC2, Spring boot] 스프링 부트 배포시, 우분투 환경에서 이미지 경로 인식 못하는 문제 (0) | 2024.08.15 |
[Spring boot] @Scheduled 스케줄러를 사용해서 Refresh 토큰 관리하기 (0) | 2024.08.10 |
[Spring Boot] CORS 설정하기 (0) | 2024.08.05 |
[Spring boot / JWT] Spring security를 사용해서 Refresh Token 구현하기 (2) | 2024.07.13 |