이번 글에서는 토이 프로젝트를 진행 하며 Reader(InputStreamReader, BufferedReader)를 사용할 때 실수 했던 것에 대해서 정리를 해보려고 한다.
HTTP 메세지와 같은 데이터를 읽어올때 Socket에선 InputStream으로 읽을 수 있도록 제공을 하고 있다.
이때 데이터를 읽기 위해선 어떻게 효과적으로 읽어 올 수 있을까? 생각을 하다가 나는 다양한 방법 중 버퍼링을 통한 BufferedReader로 한줄씩 읽기로 했다.
사용전에 나는 오라클 진영의 BufferedReader 공식 문서를 가볍게 살펴보고 사용하였다.
ready()
Tells whether this stream is ready to be read. A buffered character stream is ready if the buffer is not empty, or if the underlying character stream is ready.
(이 스트림을 읽을 준비가 되었는지 여부를 알려줍니다. 버퍼가 비어 있지 않거나 기본 문자 스트림이 준비되면 버퍼링된 문자 스트림이 준비된 것입니다.)
readLine()
Reads a line of text. A line is considered to be terminated by any one of a line feed ('\n'), a carriage return ('\r'), or a carriage return followed immediately by a linefeed.
(텍스트 한 줄을 읽습니다. 한 줄은 줄 바꿈('\n'), 캐리지 리턴('\r') 또는 캐리지 리턴 다음에 바로 줄 바꿈이 오는 것 중 하나로 종료된 것으로 간주합니다.)
그렇게 문제가 발생했던 코드를 간단하게 가져와 봤다.
try (final InputStream inputStream = connection.getInputStream();
final OutputStream outputStream = connection.getOutputStream()) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
if (bufferedReader.ready()) {
String line;
while ((line = bufferedReader.readLine()) != null) {
...
읽은 데이터를 담는 로직
...
}
}
}
위 코드로 하고자 하였던 나의 의도는
- bufferedReader에서 ready() 메서드를 호출해 준비가 되었으면 (읽을 데이터가 있다면)
- readLine() 메서드로 더 이상 읽을 데이터가 없을 때까지 한줄씩 읽어와 담으려고 했었다.
처음에는 잘 수행되나 싶었지만 실행하다보면 최초 요청은 잘 읽어오는 반면,
해당 index.html 파일 내부에서 Import 되어 추가적으로 필요한 파일(.js파일이나 .css 파일)들을 요청하는 추가 HTTP 메세지 요청 정보들이 계속 비어서 들어오는 것이였다.
그 이유가 궁금해서 계속해서 찾아본 결과 다음과 같은 결론에 도달했다.
public boolean ready() throws IOException {
synchronized (lock) {
ensureOpen();
/*
* If newline needs to be skipped and the next char to be read
* is a newline character, then just skip it right away.
*/
if (skipLF) {
/* Note that in.ready() will return true if and only if the next
* read on the stream will not block.
*/
if (nextChar >= nChars && in.ready()) {
fill();
}
if (nextChar < nChars) {
if (cb[nextChar] == '\n')
nextChar++;
skipLF = false;
}
}
return (nextChar < nChars) || in.ready();
}
}
위는 BufferedReader의 ready()의 코드이다.
조금만 분석해보면 내 코드서 ready()는 사실상 BufferedReader 생성 후 가장 처음 실행되는 메서드로 skipLF는 false이다.
그렇다면 결국 마지막 번째 줄 in.ready()로 boolean 여부가 결정되는 것이다.
그런데 in.ready()는 현재 inputStreamReader(하위 Reader)에서 바로 읽을 수 있는지를 확인하게 된다. 이 때 소켓통신 중 소켓에서 데이터를 아직 전달 받기 전에 해당 메서드가 호출되는 경우 false가 발생하게 된다. 이를 그림으로 그려보자면.
그렇기 때문에 ready()를 호출하였을 때 아직 소켓에서부터 데이터가 도착하기 전에 ready()를 호출하여 읽을 준비가 되었는지 물어보게 되었고 준비가 안되자 데이터가 없다고 판단하여 넘겨버린 것이다.
이는 내가 buffer에 데이터가 있는지 ready()를 사용하여 확인을 하여 벌어진 일이였다.
🤔 그렇다면 read()나 readLine()의 경우에는 왜 소켓에서 데이터가 도착하여 읽을 수 있을 때까지 기다리는 걸까?
그 원리는 아래 코드에 있다.
public class BufferedReader extends Reader {
...
String readLine(boolean ignoreLF, boolean[] term) throws IOException {
StringBuilder s = null;
int startChar;
synchronized (lock) {
ensureOpen();
boolean omitLF = ignoreLF || skipLF;
if (term != null) term[0] = false;
bufferLoop:
for (;;) {
if (nextChar >= nChars)
fill(); // 이부분
...
}
}
}
...
}
먼저 readLine를 호출하게 되면 fill() 메서드를 호출하게 된다.
public class BufferedReader extends Reader {
...
private void fill() throws IOException {
...
do {
n = in.read(cb, dst, cb.length - dst); // 이부분
} while (n == 0);
if (n > 0) {
nChars = dst + n;
nextChar = dst;
}
}
...
}
그럼 이 때 in.read() 즉 BufferedReader 자신의 read() 메서드를 호출하게 되는데
public class BufferedReader extends Reader {
...
public int read(char[] cbuf, int off, int len) throws IOException {
synchronized (lock) {
ensureOpen();
Objects.checkFromIndexSize(off, len, cbuf.length);
if (len == 0) {
return 0;
}
int n = read1(cbuf, off, len);
if (n <= 0) return n;
while ((n < len) && in.ready()) { // 여기서 실질적인 블럭킹
int n1 = read1(cbuf, off + n, len - n);
if (n1 <= 0) break;
n += n1;
}
return n;
}
}
...
}
BufferedReader.read()에서는 while 문을 통해 ready()를 계속 호출하며 메세지가 실질적으로 도착할 때 까지 여기서 블럭킹을 하며 기다리고 있게 되는 것이다.
🤔 내 생각 정리
BufferedReader의 ready()는 나처럼 이름과 기능에 대한 설명도
"버퍼가 비어 있지 않거나 기본 문자 스트림이 준비되면"
이라는 말이 bufferedReader에 읽어들일 값이 비어 있는지 확인 하는 용도로 사용하라고 충분히 착각할 수도 있을 것 같다.
물론 자바에선 사용자가 더 편하게 사용하기 위해서 만들어준 기능인건 고맙지만 앞으로 내가 만약에 회사에서 공용으로 사용하기 위한 라이브러리를 만든다면 주석이나 메소드 명을 좀 더 명확하게 지어야 겠다고 생각이 들었다.
🍀 결론
- BufferedReader의 ready()는 블로킹이 아니다.
- 때문에 BufferedReader에 읽을 값이 있는지 확인용으로는 사용하는 것은 현재 시점에는 소켓에서 데이터가 모두 도착하지 않아 false로 나올 수 있지만
- 실제 read()나 readLine() 메서드 호출 시에는 데이터가 도착할 수도 있습니다.
따라서 BufferedReader의 데이터가 비어있는지 확인 하려는 경우에 ready()의 사용은 부적절하다고 말 할 수 있습니다.
📚 참고자료
- stackoverflow - Does BufferedReader.ready() method ensure that readLine() method does not return NULL?
- Oracle - BufferedReader 문서