BufferedReader를 닫으면 왜 소켓도 닫힐까?

김수환·2025년 7월 13일

was 구현기

목록 보기
3/11

미니 톰캣을 구현하면서 keep-alive 옵션을 지원하기 위해 하나의 소켓으로 여러 요청을 처리하는 구조를 만들었다. RequestHandler는 InputStream과 OutputStream을 재사용하면서 여러 요청을 반복 처리하고, 마지막에 소켓을 닫는 방식으로 설계했다.

그런데 요청을 파싱하는 과정에서 BufferedReader를 열고 닫았더니, 다음 요청을 받기 전에 소켓이 이미 끊겨버리는 문제가 발생했다. 처음엔 단순한 버그라고 생각했지만, 이는 Java의 스트림 체이닝과 소켓의 생명주기에 대한 이해가 부족했던 탓이었다.

이번 글에서는 직접 삽질하며 배운 "왜 BufferedReader를 닫았더니 소켓까지 닫혔는가?"에 대해 정리하고자 한다.


내 삽질

처음에는 요청을 읽기 위해 BufferedReader를 매 요청마다 새로 생성하고, 사용 후 닫는 방식으로 구현했었다:

BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// ...
reader.close(); // 소켓까지 닫힘

그런데 reader.close() 이후 다음 요청을 처리하려 하자, 소켓이 이미 닫혀 있었다. 이로 인해 InputStream.read()는 EOF를 반환하거나 IOException을 던지며 실패했다.

문제를 추적한 끝에 BufferedReader.close()가 내부 스트림까지 닫고, 결국 socket.getInputStream()을 닫아 소켓까지 닫혀버린다는 사실을 알게 되었다.


Stream 닫기의 연쇄 효과

Java의 IO는 스트림 체이닝(stream chaining) 구조를 가진다. BufferedReaderInputStreamReader를 감싸고 있고, 이는 다시 socket.getInputStream()과 연결되어 있다.

BufferedReader
    ↓
InputStreamReader
    ↓
SocketInputStream (socket.getInputStream())

BufferedReader.close()는 내부적으로 아래 처럼, 순서대로 자원을 해제한다.

  1. 자신의 버퍼 비우기
  2. 내부 Reader 객체(InputStreamReader 등)의 close() 호출

InputStreamReader.close() 역시 내부의 InputStream을 닫고, 이는 결국 SocketInputStream을 닫는다.

자바의 SocketInputStream.close()는 단순히 입력 스트림만 닫는 게 아니라, 소켓 자체를 닫는 효과를 가진다.


소켓의 입출력 스트림과 close 동작

자바의 SocketgetInputStream()getOutputStream()을 제공하는데, 소켓의 생명주기와 강한 연관을 가진다.

  • SocketInputStream.close() 또는 SocketOutputStream.close()는 단순히 해당 스트림만 닫는 것이 아니라, 내부적으로 socket.close()까지 호출될 수 있다.
  • Java의 Socket 구현체는 스트림을 닫으면 socket 자체의 read나 write 연결을 닫는다. 즉, 한쪽이 닫히면서 통신이 불가능해진다.

socket.close()는 입출력 스트림 둘 다 닫는다. 반면 InputStream.close() 또는 OutputStream.close()는 반쪽 연결만 끊을 수는 없으며, 실제 구현체에 따라 socket 전체를 닫는 동작을 한다.


해결

결국 나는 스트림을 매 요청마다 새로 열지 않고, RequestHandler의 run() 메서드에서 상위에서 InputStream과 OutputStream을 열고, 이를 재사용하도록 변경했다

try (
    InputStream in = connection.getInputStream();
    OutputStream out = connection.getOutputStream()
) {
    processConnectionLoop(in, out); // 요청 반복 처리
}

BufferedReader는 매 요청마다 열되, 닫지는 않는다. 최상위의 InputStream만 전체 커넥션이 끝날 때 닫히도록 제어한다.

이렇게 바꾸고 나서야 keep-alive가 정상적으로 동작했고, 소켓이 예기치 않게 끊기는 문제도 해결됐다.


결론

  • BufferedReader.close()는 내부 스트림까지 닫는다.
  • 스트림이 Socket과 연결되어 있다면, 결국 socket도 닫힌다.
  • 자바의 네트워크 스트림은 소켓 생명주기와 밀접하게 연결되어 있어, 스트림을 닫는 행위가 소켓 종료로 이어질 수 있다.
  • 안전한 관리를 위해서는 소켓을 기준으로 리소스를 관리하는 것이 좋다.

profile
hello human

0개의 댓글