빌드노트
InputStream.read가 양수여도 버퍼 전체를 처리하면 안 된다
read 반환값 n보다 뒤의 버퍼 원소는 이번 입력이 아니다. ABCDE를 최대 두 바이트씩 반환한 관찰로 잔여 값을 확인하고, n만 소비하는 루프와 readNBytes 뒤의 입력 부족 검사를 선택한다.
InputStream.read(buffer)가 성공했는데 처리한 데이터에 이전 내용이 섞인다면, 바로 다음의 쓰기·복사·파싱 호출을 확인한다. 반환값이 n일 때 이번에 읽은 구간은 [0, n)뿐이다. out.write(buffer)로 배열 전체를 보내고 있다면 out.write(buffer, 0, n)으로 바꾼다. 버퍼 크기만큼 읽었다는 보장은 없다.
입력을 읽는 대로 처리할 때는 반환 수만 소비하며 반복한다. 다음 처리가 정해진 길이를 전제한다면 readNBytes로 모은 뒤 길이를 검사한다. 짧은 양수 반환과 입력의 끝인 EOF를 구별하는 것이 두 방식 모두의 출발점이다.
read 다음의 소비 범위를 고친다
아래 코드는 네 칸짜리 버퍼로 ABCDE를 읽어 메모리 출력에 모은다. 입력은 JDK의 ByteArrayInputStream으로 준비했다. JDK 21의 bin이 PATH에 있는 터미널에서 jshell --execution local --feedback concise를 실행한 뒤 붙여 넣으면 소비 루프를 확인할 수 있다. 실제 실행 환경은 Windows 11, OpenJDK 21.0.1이다.
import java.io.*;
import java.nio.charset.StandardCharsets;
byte[] buffer = new byte[4];
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream source = new ByteArrayInputStream(
"ABCDE".getBytes(StandardCharsets.US_ASCII))) {
int n;
while ((n = source.read(buffer)) != -1) {
out.write(buffer, 0, n);
}
}
System.out.println(out.toString(StandardCharsets.US_ASCII));
출력은 ABCDE였다. 자신의 코드에서는 source와 out을 실제 입력과 소비자에 대응시켜 본다. 고칠 부분은 소비자에게 배열 전체나 buffer.length를 넘기는 호출이다. 버퍼를 매번 0으로 지워도 읽지 않은 0까지 보내면 길이는 여전히 틀린다. 소비자가 배열 참조를 나중까지 보관한다면 다음 읽기가 덮어쓰기 전에 유효 구간을 복사해야 한다.
세 번째 읽기에 남은 D는 새 데이터가 아니다
Java SE 21의 read 문서 (새 탭에서 열림)는 실제 읽은 수를 반환하고 그 뒤의 배열 원소는 바꾸지 않는다고 설명한다. 이 범위를 눈으로 확인하려고 별도 실험에서는 ABCDE를 한 호출에 최대 두 바이트만 반환하는 합성 InputStream을 사용했다. 같은 네 칸 버퍼를 처음에 ####으로 채우고 반복해서 읽은 실제 출력이다.
read[1] n=2 buffer=AB## valid=AB
read[2] n=2 buffer=CD## valid=CD
read[3] n=1 buffer=ED## valid=E
read[4] n=-1 buffer=ED## valid=<EOF>
세 번째 읽기는 E 하나만 썼다. 직전의 CD##에서 첫 칸만 바뀌었으므로 D##이 남았다. 배열 전체를 매번 이어 붙인 결과는 AB##CD##ED##, 반환 수만 소비한 결과는 ABCDE였다. 앞의 소비 루프도 이 최대 두 바이트 입력에 적용해 ABCDE를 확인했다.
첫 반환이 2여도 뒤에 CDE가 더 있으므로, 짧게 읽었다는 이유로 반복을 끝내면 안 된다. 길이가 양수인 버퍼의 read(byte[])는 EOF를 -1로 알린다. 위 관찰은 반환 크기를 강제한 메모리 실험이며 실제 파일이나 네트워크의 반환 크기를 예측하는 자료는 아니다. read(buffer, off, len)을 쓰는 코드에서는 유효 구간의 시작도 옮겨져 [off, off + n)이 된다. 오프셋을 받는 read 계약 (새 탭에서 열림)
정해진 길이가 필요하면 모은 뒤 부족을 거부한다
헤더처럼 필요한 바이트가 모두 있어야 파싱할 수 있다면 readNBytes(int) (새 탭에서 열림)를 사용할 수 있다. 요청 길이를 모으거나 EOF 또는 예외를 만날 때까지 읽는다. EOF가 먼저 오면 요청보다 짧은 배열을 반환하므로, 길이 검사를 파싱·저장보다 앞에 둔다.
같은 JShell에서 다음 코드는 새 ABCDE 입력에 여덟 바이트를 요구하고, 부족한 데이터를 처리하기 전에 거부한다.
int expected = 8;
try (InputStream source = new ByteArrayInputStream(
"ABCDE".getBytes(StandardCharsets.US_ASCII))) {
byte[] data = source.readNBytes(expected);
if (data.length != expected) {
throw new EOFException("expected=" + expected + " actual=" + data.length);
}
System.out.println(new String(data, StandardCharsets.US_ASCII));
}
실행 결과는 EOFException: expected=8 actual=5였다. readNBytes(8) 자체는 길이 5의 배열을 반환했고, 작성한 길이 검사가 예외를 던졌다. expected를 5로 바꾸고 이 블록 전체를 다시 실행하면 ABCDE가 출력된다. 최대 두 바이트 합성 입력에서도 요청 5는 다섯 바이트를 모았고 요청 8은 다섯 바이트만 반환했다.
EOF 뒤에 다시 호출한 readNBytes(int)는 빈 배열, 길이가 양수인 버퍼의 read는 -1을 반환했다. 기존 배열을 받는 readNBytes(buffer, off, len)을 선택한다면 실제 반환 수를 사용해야 하며, 이 오버로드는 EOF 뒤에 0을 반환하고 미수신 구간은 그대로 둔다. 배열을 받는 readNBytes 계약 (새 탭에서 열림)
요청 길이도 EOF도 아직 오지 않으면 readNBytes는 기다릴 수 있다는 것이 문서의 계약이다. 들어오는 구간부터 처리해야 하는 코드에서는 n만 소비하는 루프를 사용한다. 이번 실행은 OpenJDK 21.0.1의 작은 합성 메모리 입력과 ASCII 바이트에 한정했다. 실제 파일·소켓, 대기 시간, I/O 오류, 비동기 종료와 멀티바이트 문자 디코딩은 검증하지 않았다. JShell은 /exit로 종료한다.