정의
- org.springframework.web.servlet.mvc.method.annotation.SseEmitter
- Spring에서 SSE(Server-Sent Event)를 구현하기 위한 클래스
특징
- 클라이언트로 텍스트 기반의 이벤트 전송 (String이 아닌 데이터들 toString() 필수)
- 클라이언트와의 연결 유지, 데이터 전송, 연결 종료 기능을 제공
- 실시간 업데이트 혹은 클라이언트에서 서버로부터 비동기적으로 데이터 수신 시에 주로 사용
주요 메소드
- send() = 클라이언트로 이벤트 전송
- complete() = SSE 연결 종료
- completeWithError() = SSE 연결을 오류 상태로 종료
- onCompletion() = SSE 연결이 종료될 때 수행할 작업 정의
- onError() = SSE 연결에서 오류가 발생했을 때 수행할 작업 정의
- onTimeout() = SSE 연결이 클라이언트로부터 요청을 받은 후 일정 시간 동안 반응이 없을 경우에 대한 처리
- extendResponse() = SSE 응답 확장 (사용자 정의 헤더 추가, 상태 코드 수정 등)
기본 세팅
SseApplication
@SpringBootApplication
@EnableScheduling // @Scheduled 사용을 위해 적용
public class SseApplication {
public static void main(String[] args) {
SpringApplication.run(SseApplication.class, args);
}
}
SseController
@RestController
@RequestMapping("/api")
public class SSEController {
@Autowired
private SseService sseService;
// SSE는 텍스트 기반 이벤트를 전송하기 때문에 TEXT_EVENT_STREAM_VALUE 적용
@GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handleEvents() {
// SseEmitter 객체 생성 시 Long 데이터를 입력하면 timeout(연결 종료 시간)을 지정할 수 있음
// SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
SseEmitter emitter = new SseEmitter();
sseService.addEmitter(emitter);
sseService.sendEvents();
return emitter;
}
SseService
@Service
@Slf4j
public class SseService {
// 주로 순회가 일어나는 용도로 사용할 때는 안전한 스레드 처리를 위해 CopyOnWriteArrayList를 사용
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
public void addEmitter(SseEmitter emitter) {
emitters.add(emitter); // 리스트에 SseEmitter 추가
emitter.onCompletion(() -> {
log.info("onCompletion callback");
emitters.remove(emitter); // SSE 연결 종료 시 리스트에서 삭제
});
emitter.onTimeout(() -> {
log.info("onTimeout callback");
emitter.complete(); // 지정 시간 경과 시 SSE 연결 종료
});
}
// 2초마다 해당 메소드가 자동으로 실행되도록 지정 (fixedRate 단위=밀리초)
@Scheduled(fixedRate = 2000)
public void sendEvents() {
for (SseEmitter emitter : emitters) {
try {
emitter.send("Hello, World!" + count); // 데이터 전송
} catch (IOException e) {
emitters.remove(emitter);
emitter.complete(); // 에러 발생 시 리스트에서 SseEmitter 제거 및 SSE 연결 종료
}
}
}
}
Client 예시 (Vue.js)
<template>
<div>
<div v-if="mainBoardList.length > 0">
<button @click="openModal">Open Modal</button>
</div>
<div v-else>
No data received yet.
</div>
<button @click="reqApi">Request API</button>
<button @click="stopApi">Stop API</button>
<!-- 모달 창 -->
<div class="modal" :class="{ 'is-active': showModal }">
<div class="modal-background" @click="closeModal"></div>
<div class="modal-content">
<div>
<ul class="list-group" style="max-height: 300px; overflow-y: auto;">
<li class="list-group-item" v-for="(item, index) in mainBoardList" :key="index">
{{ item }}
</li>
</ul>
</div>
<button @click="closeModal">Close Modal</button>
</div>
</div>
<div>
<label for="eventSourceUrl">EventSource URL:</label>
<input type="text" id="eventSourceUrl" v-model="eventSourceUrl">
<button @click="applyEventSource">Apply</button>
</div>
<!-- 요청 시작 및 종료 시간 -->
<div v-if="startTime">
Request started at: {{ startTime }}
</div>
<div v-if="stopTime">
Request stopped at: {{ stopTime }}
</div>
<!-- Count 입력 -->
<div>
<label for="count">Count:</label>
<input type="number" id="count" v-model="count">
</div>
</div>
</template>
<script>
export default {
data() {
return {
mainBoardList: [],
showModal: false, // 모달 창을 열고 닫기 위한 상태 변수
eventSourceUrl: 'http://172.16.100.179:8009/api/emitter', // 기본 URL 설정
eventSource: null, // EventSource 객체
startTime: null, // 요청 시작 시간
stopTime: null, // 요청 종료 시간
count: 0, // 받을 데이터 개수
};
},
methods: {
reqApi() {
if (!this.eventSourceUrl) {
console.error('EventSource URL is not provided.');
return;
}
console.log("Sending request to server");
// 기존의 EventSource가 있다면 종료
if (this.eventSource) {
this.eventSource.close();
}
// EventSource에 http 포함 SSE 통신할 URL 입력
this.eventSource = new EventSource(this.eventSourceUrl);
// 요청 시작 시간 기록
this.startTime = new Date().toLocaleTimeString();
let receivedCount = 0; // 받은 데이터 개수 초기화
this.eventSource.onmessage = (event) => {
console.log("Received data from server:", event.data);
// Process the received data
this.mainBoardList.push(event.data);
// 받은 데이터 개수 증가
receivedCount++;
// 받은 데이터 개수가 Count와 같으면 SSE 통신 종료
if (receivedCount >= this.count) {
this.eventSource.close();
// 요청 종료 시간 기록
this.stopTime = new Date().toLocaleTimeString();
}
};
this.eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
// Handle errors
this.eventSource.close();
};
},
stopApi() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null; // EventSource 객체 초기화
// 요청 종료 시간 기록
this.stopTime = new Date().toLocaleTimeString();
}
},
openModal() {
this.showModal = true; // 모달 창 열기
},
closeModal() {
this.showModal = false; // 모달 창 닫기
},
applyEventSource() {
this.reqApi(); // 설정된 EventSource를 적용
},
},
};
</script>
<style>
.modal {
display: none;
}
.modal.is-active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-background {
position: fixed;
top: 0;
left: 0;
width: 150%;
height: 10%;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
max-width: 80%;
max-height: 80%;
overflow: auto;
}
.list-group {
margin: 0;
padding: 0;
list-style: none;
}
.list-group-item {
padding: 10px;
}
</style>
'Spring' 카테고리의 다른 글
Spring Data Redis (0) | 2024.04.18 |
---|---|
Session 타임 아웃 설정 (0) | 2024.04.15 |
JDBC와 DB 연동 (0) | 2024.03.19 |
https 통신 방법 (SSL 인증서) (0) | 2024.02.14 |
The import org.springframework cannot be resolved 에러 (Gradle) (0) | 2024.02.12 |
댓글