TroubleShooting & Study/SpringBoot

[SSE] java.io.IOException : Broken pipe

DH_0518 2024. 9. 29. 23:54

 

이번에는 Spring에서 SSE를 사용하여 데이터를 전송할 때 발생하는 'java.io.IOException:Broken pipe'를 대응법을 알아보겠다.

왜 해결법이 아니라 대응법이라 적었냐면... 진짜 말 그대로 'Broken pipe'가 발생하지 않게끔 해결하는 것이 아니라, 발생했을 때 대응하는 방법을 설명할 것이기 때문이다.

 

 

 

 

 

 

 

java.io.IOException:Broken pipe

 

 

 

이 Exception은 다음과 같은 상황에서 발생한다

  1. 데이터 송수신 중 연결이 끊어진 경우
    : 클라이언트와 서버에서 유지 중인 connection이 끊어졌을 때, 이를 통해 데이터를 전송하려 하면 발생
  2. 네트워크가 서버 CPU 문제로 송신받은 데이터를 처리하지 못한 경우
    : 송신받은 데이터를 적절하게 처리하지 못하는 상황에서 계속 요청이 들어올 때 발생

 

Exception이 발생하는 상황을 보면 서버와 클라이언트의 connection에 문제가 생겨 데이터 전송이 실패했지만, 언제 connection이 끊어졌는지 서버에서는 알 방법이 없고, 클라이언트에서 의도한 것이 아닌 비정상적으로 연결이 끊어졌다면 더더욱 원인을 알기 힘든 상황인 것을 알 수 있다. 따라서 Exception이 발생하지 않도록 해결하는 것이 아니라, 발생했을 때 어떻게 처리할지를 알아보겠다.

 

 

 

 

 

 

 

대응 방법

 

 

 

'SSE Broken pipe' 키워드로 구글링을 하면 다음과 같은 해결 방법들이 많이 나올 것이다. Untitled 상황을 고려해서 어떤 방법을 사용해야할지 생각해 보겠다.

 

 

1. 요청 이후 응답을 기다린다

: 클라이언트에서 서버로 데이터 요청을 계속 보내는 것이 아니라, 요청을 보낸 후 응답이 돌아올 때까지 기다리는 방식이다.

 

=> 우리 서비스에서는 '알림' 기능이나 '공유' 기능에서 SSE를 사용하고 있는데, 이렇게 동기적으로 작동하도록 한다면 기능 자체의 속도가 느려져서 유저 경험상 좋지 못할 것이다.

 

 

2. Exception을 무시한다 ***

: 어떠한 이유로 'Broken pipe Exception'이 발생했을 때, 서버에서는 그냥 신호를 무시하는 방법이다

 

=> 데이터를 전송할 때, 데이터 유실을 막기 위해 emitterId와 전송할 data를 캐시에 저장하고 있고, 클라이언트에서 서버로 연결할 때 유실된 데이터가 존재한다면 재전송해주는 로직이 설정되어 있으므로 이 방법을 사용해 보겠다

 

 

3. 중복 요청을 막는다

: 클라이언트에서 연속적인 클릭(따닥)을 방지하는 방식이다

 

=> 서버 로그 확인 결과, 연속적인 클릭이 없었음에도 예외가 발생하므로 해당 사항이 없다

 

 

4. Timeout 값을 늘린다

: proxy의 Read Timeout이나 서버에서 SSE를 생성할 때 Timeout을 늘리는 방법이다

 

=> Timeout 조절은 데이터 패킷 유실을 막기 위한 기본적인 방법이지만, 우리는 데이터 유실보다는 비정상적으로 connection이 끊어지는 상황이 좀 더 근본적인 원인이므로 큰 도움이 되지 않을 것이다. 그리고 SSE 생성 시 설정한 Timeout을 넘기면 서버에서 자동으로 재연결을 시도하기에 크게 신경 쓰지 않아도 될 것 같다.

 

 

 

 

 

 

Code

 

 

코드를 통해 어떻게 해결했는지 확인해 보자

 

기존 코드
수정된 코드

 

기존 코드를 보면 예외가 발생했을 때, 'SSE_SEND_FAIL' 예외를 던져주며 데이터 전송에 실패했다는 로그만을 띄웠다. 하지만 수정된 코드에서는 'Broken pipe'를 포함한 예외나 'Response body is already written'을 포함한 예외에서는, 따로 예외를 던져서 에러 메시지를 보여주지 않고 단순 log만 남기도록 처리했다.

 

이렇게 exception을 던지지 않아도 되는 이유는 다음 코드를 보면 알 수 있다.

 

 

 

데이터를 전송하는 코드
Client와 Server의 SSE 연결 코드
유실된 데이터를 전송하는 코드

 

  1. 데이터 전송 전 SseRepository에 'ConcurrentHashMap<유저 Uuid, Data>' 형태로 존재하는 EventCache에 예외 상황을 대비해서 데이터를 저장한다. 이때, 데이터 전송에 성공했다면 방금 저장한 데이터를 캐시에서 삭제한다
  2. 전송에 실패한 경우 계속 eventCache에 데이터를 쌓아두다가, Client에서 다시 SSE 연결을 시도하면 '유저 Uuid'를 사용해서 유실된 데이터가 존재하는지 조회한다
  3. 만약 캐시를 조회했을 때 데이터가 존재한다면 이는 전송에 실패한 유실된 데이터라는 뜻이므로, 다시 전송을 시도한다

 

이러한 과정에 따라 'broken pipe'이 발생하더라도 데이터 유실 걱정 없이 sse로 데이터를 주고받을 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Reference

 

🧨Broken pipe Exception을 해결해보자

Broken pipe Exception이란? 나의 문제 + 해결방법

velog.io

 

[Java/Error] Broken Pipe 파이프가 깨어짐

java.io.IOException: Broken pipe 해당 오류는 크게 세 가지 원인으로 발생한다. 1. Receiver에서 송신받은 데이터를 제때 처리하지 못하는데도 Sender가 계속 데이터를 보내는 경우에 발생한다. 예를 들어 네

acetes-mate.tistory.com