티스토리 뷰

반응형

채팅 솔루션을 개발하며 ChatGPT처럼 한글자씩 나오는 비밀이 무엇일까 찾아보았고 그 내막에는 SSE 기술을 사용한다는 사실을 알게되었다. 따라서 SSE에 대해 공부하고 가능하다면 실제 구현까지 진행해 보도록 할 예정이다.

 

SSE(Server-Sent Events) 방식이란?

SSE 통신 출처 : https://akku-dev.tistory.com/85

 

SSE는 서버에서 클라이언트에게 메세지를 비동기적으로 전송할 때 사용하는 기술이다. 서버에서 클라이언트에게 전송해야하는 상황은 알림이나 채팅창이라든지 실시간으로 서버의 변경을 클라이언트에게 전달해야할 때 유용하다. 서버의 정책을 클라이언트에게 전달해야할때 사용해도 될 것 같다. 폴링(Polling)과 같이 클라이언트가 지속적으로 확인하는 방식이 있지만 좀더 실시간으로 반영하고 빈번하게 발생될 수 있는 케이스라면 SSE를 고려해볼 만하다.

 

SSE가 동작되는 과정은 다음과 같다.

  1. 클라이언트는 서버와의 연결을 요청 한다.
    • mediaType은 text/event-stream으로 보낸다.
  2. 서버는 연결을 받아드린다.
    • Transfer-Encoding 방식을 chunked로 한다. chunked는 데이터의 사이즈를 계산하는 과정을 거치지 않기 때문에 큰 데이터를 응답할때 효과적이다. 서버에서는 요청에 대해 응답을 계산할때 시간이 소요된다. Chunked를 통해 이와 같은 과정을 생략하는 것이다.
    • 응답 패킷의 해더에는 Content-Length가 존재하지 않는다.
  3. 이후 비동기 적으로 클라이언트에게 이벤트를 전송한다.
    • 데이터는 utf-8로 인코딩된 텍스트 데이터만 가능
    • 각 이벤트는 한 개 이상의 name:value로 이루어져 있다.

 

SSE 연결 수립 출처 : https://sothoughtful.dev/posts/sse/

 

Step1. SSE 구독 신청 (Client → Server)

클라이언트가 서버와의 연결을 요청하는 것을 구독이라는 행위로 나타낼 수 있다. 서버의 변화를 구독하여 실시간으로 관찰하고 싶다는 의미다. 우선 서버에게 구독을 요청하는 컨트롤러를 만든다.

 

@GetMapping(path="/subscribe/{id}", produces = MediaType.*TEXT_EVENT_STREAM_VALUE*)
public SseEmitter subscribe(@PathVariable Long id) {    
		return streamResponseService.subscribe(id);
}

 

컨트롤러에는 produces로 Content-Type을 MediaType.TEXT_EVENT_STREAM_VALUE 으로 지정하였다. 이렇게 지정하면 서버에서 응답 패킷의 헤더의 Content-Type이 text/event-stream으로 되고 텍스트 데이터를 연속해서 보낼 수 있다.

 

 

 

 

Step2. SSE 연결 수립 (Server)

 

1) SseEmitter 객체 생성

가장 먼저 클라이언트의 요청에 따라 서버에서는 통신 객체인 SseEmitter를 생성한다. 생성할때 만료 시간을 입력할 수 있다. 만료 시간이 너무 길면 연결 관리를 해줘야하고 짧으면 재연결 요청이 잦게 일어나므로 적절하게 설정 해야한다.

 

SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);

 

 

2) 더미 데이터 전송

서버와의 연결을 맺을때 더미 데이터를 넘겨줘야 한다. 그렇지 않고 데이터를 하나도 전송하지 않으면 재연결 요청시 503 Service Unavaliable 에러가 발생할 수 있다. 따라서 이를 방지하기 위해 Dummy 데이터를 보낸다. 여기서는 "EventStream Created, [UserId=" + id + "]" 의 데이터를 전송한다.

 

    @Override
    public SseEmitter subscribe(Long id) {
        SseEmitter emitter = createEmitter(id);
        sendMessage(id, "EventStream Created, [UserId=" + id + "]");
        return emitter;
    }

    @Override
    public void sendMessage(Long id, Object data) {
        SseEmitter emitter = emitterRepository.get(id);
        if (emitter != null) {
            try {
                emitter.send(SseEmitter.event()
                        .name("firstSSE")
                        .data(data)
                        .reconnectTime(RECONNECTION_TIMEOUT)
                );
            } catch (IOException e) {
                // 보완
                emitter.complete();
            }
        }
    }

 

 

실제 연결을 수립할 때 다음과 같이 화면에 출력된다.

 

 

 

3) Emitter 객체 관리

Emitter 를 저장하기 위해 Spring Data JPA 를 사용해서 물리적으로 저장해도 된다. 하지만 HashMap을 사용한 케이스가 많아 HashMap을 사용했다. HashMap을 사용하면 데이터 저장 위치를 해시함수를 통해 바로 알 수 있어 검색이 빠르다. 일반 HashMap은 Thread-safe 하지 않으므로 ConcurrentHashMap을 사용한다. 멀티 쓰레드 환경에서 데이터를 관리하기 위해 동기화 과정이 필요하다. 이를 위해 쓰레드가 접근하는 Map에 Lock을 걸어야하는데 ConcurrentHashMap 에서는 Bucket 단위로 관리가 되어 오버해드를 줄일 수 있다는 장점이 있다.

 

private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();

 

 

Step3. 클라이언트에게 메세지 보내기 (Server → Client)

이제 마지막으로 연결되어있는 클라이언트에게 메세지를 전달하는 컨트롤러를 만들었다.

sendMessage에 id와 전달할 data를 인자로 주어 해당 id의 emitter에 메세지를 전달한다.

 

@GetMapping(path="/sendMessage/{id}/{data}")
    public void sendMessage(@PathVariable Long id, @PathVariable String data) {
        streamResponseService.sendMessage(id, data);
    }

 

 

추가적으로 보낸 데이터가 계속해서 화면에 출력되는 것을 확인 할 수 있다.

 

 

이렇게 SSE 동작 과정에 대해 공부했고 다음에는 이를 사용해서 화면에 ChatGPT처럼 출력되도록 구현해보도록 하겠다.

 

 

 

 

 

참고

https://akku-dev.tistory.com/85

https://medium.com/@AlexanderObregon/implementing-server-sent-events-sse-with-spring-cbf283171aef

https://amaran-th.github.io/Spring/[Spring] Server-Sent Events(SSE)/#sse의-통신-과정

https://velog.io/@alsgus92/ConcurrentHashMap의-Thread-safe-원리

https://velog.io/@no-oneho/Spring-Boot로-SSE를-통한-알람-구현하기

https://velog.io/@wnguswn7/Project-SseEmitter로-알림-기능-구현하기#️-알림-repository

https://velog.io/@dhk22/TIL-Day

https://devel-repository.tistory.com/31

https://dkswnkk.tistory.com/702

https://sothoughtful.dev/posts/sse/

 

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함