미니 톰캣을 구현하면서 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()을 닫아 소켓까지 닫혀버린다는 사실을 알게 되었다.
Java의 IO는 스트림 체이닝(stream chaining) 구조를 가진다. BufferedReader는 InputStreamReader를 감싸고 있고, 이는 다시 socket.getInputStream()과 연결되어 있다.
BufferedReader
↓
InputStreamReader
↓
SocketInputStream (socket.getInputStream())
BufferedReader.close()는 내부적으로 아래 처럼, 순서대로 자원을 해제한다.
Reader 객체(InputStreamReader 등)의 close() 호출InputStreamReader.close() 역시 내부의 InputStream을 닫고, 이는 결국 SocketInputStream을 닫는다.
자바의 SocketInputStream.close()는 단순히 입력 스트림만 닫는 게 아니라, 소켓 자체를 닫는 효과를 가진다.
자바의 Socket은 getInputStream()과 getOutputStream()을 제공하는데, 소켓의 생명주기와 강한 연관을 가진다.
SocketInputStream.close() 또는 SocketOutputStream.close()는 단순히 해당 스트림만 닫는 것이 아니라, 내부적으로 socket.close()까지 호출될 수 있다.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도 닫힌다.