에러 기록

SSE 최종 에러 해결.. - ResponseBodyEmitter$DataWithMediaType , 그리고 Failed to load resource: net::ERR_HTTP2_PROTOCOL_ERROR

uhyvn 2024. 12. 26. 22:31

https://hyunco.tistory.com/74

 

sse 에러 해결 (AsyncRequestNotUsableException)

ERROR 2798 --- [nio-5000-exec-5] c.j.j.c.e.h.GlobalExceptionHandler : RuntimeException :ResponseBodyEmitter has already completed with error: org.springframework.web.context.request.async.AsyncRequestNotUsableException: Response not usable after response e

hyunco.tistory.com

이전 sse 에러 해결 글에서 크리티컬한 에러는 해결했지만, 또 다른 에러가 발생했다..!

 

서버 로그에 뜬 에러를 먼저 보여주자면 아래와 같다.

Dec 23 19:05:01 ip-172-31-0-18 springapp[2801]: 2024-12-23T19:05:01.842+09:00 ERROR 2801 --- [io-5000-exec-25] c.j.j.c.e.h.GlobalExceptionHandler       : RuntimeException 
: Failed to send [org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter$DataWithMediaType@2eeb358d, org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter$DataWithMediaType@4610af44, org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter$DataWithMediaType@7b395c63]

 

게시글에 좋아요를 누르거나, 취소, 혹은 댓글을 생성하려고 할 때 전부 위와 같은 에러가 뜨면서 실패했다.

처음에는 트랜잭션을 먼저 분리하고 에러 해결보다 정상적인 커뮤니티 활동부터 가능하게 할까 했지만,

새벽 안에만 끝내 보고 안되면 아침에 트랜잭션 분리 후에 고치기로 했다.

 

 

 

 

아무튼, 서칭을 해 본 결과 여러 가지 원인을 예측할 수 있었지만 우선 아래의 개선 방법을 시도해 봤다.

 

ResponseBodyEmitter$DataWithMediaType 관련 오류가 발생하므로, 데이터 전송 로직에 문제가 있을 가능성이 있다고 판단했다.

원인 : emitter.send() 호출 시 데이터 포맷

 

기존에 sendToClient 메서드에서 data를 전송할 때 Object 타입으로 받아들이고 있었다.
SSE 클라이언트는 일반적으로 JSON 포맷을 선호하며, 전송 데이터가 올바르게 직렬화되지 않으면 오류가 발생할 수 있다고 한다...

 

기존 코드

private void sendToClient(SseEmitter emitter, String emitterId, Object data) {
    try {
        emitter.send(SseEmitter.event()
            .id(emitterId)
            .name("sse")
            .data(data)
        );
    } catch (IOException exception) {
        emitterRepository.deleteById(emitterId);
        emitter.completeWithError(exception);
    }
}

 

수정 방안 : Object로 받던 data를 JSON으로 변환하여 전송했다.

private void sendToClient(SseEmitter emitter, String emitterId, Object data) {
    try {
        String jsonData = new ObjectMapper().writeValueAsString(data); // JSON 변환
        emitter.send(SseEmitter.event()
            .id(emitterId)
            .name("sse")
            .data(jsonData)
        );
    } catch (IOException exception) {
        emitterRepository.deleteById(emitterId);
        emitter.completeWithError(exception);
    }
}

 

 

 

 

 


 

 

 

이렇게 첫 번째 에러는 해결했다.

 

하지만 기존부터 뜨던 에러가 하나 있었는데..

Failed to load resource: net::ERR_HTTP2_PROTOCOL_ERROR

 

이 에러가 랜덤한 시간마다 클라이언트 페이지 console에 떠있는 걸 발견했었다.

문제는 서버 로그에는 에러가 뜨지를 않아서 더 원인을 찾기 힘들었다.

 

 

 

 

3주간의 sse 에러 해결의 마지막 서칭이 됐다.

처음에는 Nginx 설정 문제인가 해서 아주 여러 번 바꿨지만 소용없었다.

현재 잘 작동하는 설정 파일을 혹시 모르니 올려놓겠다.,

location / {
    proxy_pass          http://127.0.0.1:5000;
    proxy_http_version  1.1;

    proxy_buffering off;
    proxy_read_timeout 1800;

    proxy_set_header    Connection          $connection_upgrade;
    proxy_set_header    Upgrade             $http_upgrade;
    proxy_set_header    Host                $host;
    proxy_set_header    X-Real-IP           $remote_addr;
    proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
}

 

 

그 후로 다들 클라이언트의 CORS 문제를 언급하던데,

나는 이미 app 코드 내에서 corsConfig로 도메인을 열어뒀었다.

이 외에도 클라이언트 브라우저/네트워크 환경 문제, 데이터 형식 문제, 연결 중단 시 복구 코드, HTTP/2 및 클라이언트 호환성 문제

등등 안 해본 게 없었다.

 

 

 

그러다 마지막으로 시도해 봤던 게 해결이 됐다.

 

클라이언트-서버 연결 해제

  • 원인: SSE 연결 유지 중 네트워크 문제나 클라이언트 환경에서 연결이 중단.
  • 확인 및 해결 : Ping 또는 Keep-Alive:
    • 서버에서 주기적으로 더미 이벤트를 보내 연결을 유지: 
    •  
private void sendPing(SseEmitter emitter, String emitterId) {
    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    scheduler.scheduleAtFixedRate(() -> {
        try {
            emitter.send(SseEmitter.event()
                .id(emitterId)
                .name("ping")
                .data("ping"));
        } catch (IOException e) {
            emitter.completeWithError(e);
            scheduler.shutdown();
        }
    }, 0, 30, TimeUnit.SECONDS); // 30초 간격으로 ping 전송
}

 

위 메소드를 맨 처음 subscribe를 설정할 때 실행되도록 구현했다.

설명

  1. sendPing 메소드:
    • ScheduledExecutorService를 사용해 일정 간격(예: 30초)으로 ping 이벤트를 전송합니다.
    • 클라이언트와 서버의 연결이 끊어졌을 경우 IOException을 발생시켜 emitter.completeWithError로 연결을 종료합니다.
  2. subscribe 메소드와 통합:
    • SseEmitter를 생성한 직후 sendPing을 호출하여 ping 이벤트가 주기적으로 전송되도록 설정합니다.

 

 

 

 

다른 분들은 Nginx 환경 설정이나 헤더 설정으로 keep-alive를 충분히 구현하시던데..

나는 아무리 설정을 바꿔봐도 해결이 안 됐었다.

그러나 위와 같이 일정 시간마다 더미 데이터를 전송함으로써 에러를 해결할 수 있었다..

 

 

 

 

에러 해결!




*) 나중에 알게 된 사실인데, 현재 우리는 ALB를 사용하고 있었는데 기본적으로 ALB는 timeout이 60초라서 끊기는 거라고 한다..

nginx가 끊는 게 아니라 ALB가 끊는 거라서 nginx에서 keep alive를 해도 안되는 거였다?!