본문 바로가기
Spring

SseEmitter

by Mecodata 2024. 4. 29.

정의

- 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

댓글