본문 바로가기

바이트 스트림과 소유권

파일과 메모리를 같은 스트림 추상화로 다루고 부분 읽기, EOF, 예외 시 닫기, 입력 크기 상한을 지키는 바이트 입출력 루프를 설계합니다.

InputStream과 OutputStream은 데이터의 출처와 목적지를 바이트 흐름이라는 공통 규칙으로 감춥니다.

파일, 메모리, 소켓이 서로 다른 장치여도 읽는 쪽은 read, 쓰는 쪽은 write를 사용합니다.

이 추상화 덕분에 변환 로직을 장치에서 분리할 수 있지만, 반환값과 자원 소유권을 정확히 지켜야 안전합니다.

입력의 read()는 바이트 하나를 0부터 255 범위의 int로 반환하고 끝에서는 -1을 반환합니다.

byte를 반환하지 않는 이유는 256개 바이트 값과 EOF 표식을 동시에 표현해야 하기 때문입니다.

배열 읽기는 실제로 채운 길이를 반환하며 요청한 배열을 매번 가득 채운다는 보장이 없습니다.


available을 전체 길이로 보는 오해

available()은 블로킹 없이 즉시 읽을 수 있다고 추정되는 양이지 전체 스트림 길이가 아닙니다.

메모리 스트림에서는 남은 길이처럼 보여도 네트워크나 압축 스트림에서는 아직 도착하지 않은 데이터가 있습니다.

값이 0이라는 이유로 EOF라고 판단하면 정상 메시지를 중간에서 자릅니다.

bad/AvailableIsNotLength.java
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

public final class AvailableIsNotLength {
    static byte[] wrongRead(InputStream input) throws IOException {
        byte[] assumed = new byte[input.available()];
        int count = input.read(assumed);
        System.out.println("assumed=" + assumed.length + " actual=" + count);
        return assumed;
    }

    public static void main(String[] args) throws Exception {
        try (InputStream input = new ByteArrayInputStream(new byte[] {10, 20, 30})) {
            wrongRead(input);
        }
    }
}

이 입력에서는 ByteArrayInputStream의 계약에 따라 남은 세 바이트를 모두 읽습니다. 이 결과를 전체 길이나 한 번 읽기의 완전성을 보장하지 않는 일반 스트림에 적용할 수는 없습니다.

고정 길이 형식은 헤더의 길이만큼 정확히 읽고, EOF 기반 형식은 read가 -1을 반환할 때까지 반복합니다.

무한 입력에는 별도의 메시지 구분이 반드시 필요합니다.


메모리 스트림의 장치 독립 검사

ByteArrayOutputStream은 쓰인 바이트를 내부 배열에 모으고, ByteArrayInputStream은 배열을 입력처럼 제공합니다.

운영 파일을 만들지 않고도 인코더와 파서의 바이트 규칙을 확인하기 좋습니다.

닫아야 할 외부 자원은 없지만 다른 스트림과 같은 API를 사용하므로 테스트 대역으로 자연스럽습니다.

src/MemoryPacketRoundTrip.java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;

public final class MemoryPacketRoundTrip {
    static byte[] encode(int version, byte[] payload) throws Exception {
        var bytes = new ByteArrayOutputStream();
        try (var output = new DataOutputStream(bytes)) {
            output.writeInt(version);
            output.writeInt(payload.length);
            output.write(payload);
        }
        return bytes.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        byte[] packet = encode(3, new byte[] {4, 5, 6});
        try (var input = new DataInputStream(new ByteArrayInputStream(packet))) {
            int version = input.readInt();
            int length = input.readInt();
            byte[] payload = input.readNBytes(length);
            System.out.printf("version=%d length=%d first=%d%n",
                    version, payload.length, payload[0]);
        }
    }
}

위 예제는 자신이 만든 정상 패킷만 읽습니다. 외부 패킷을 받는 파서에서는 헤더 길이를 신뢰하기 전에 음수와 허용 최댓값을 검사해야 합니다.

readNBytes(length)도 EOF에서 요청량보다 짧은 배열을 반환할 수 있으므로 결과 길이를 대조합니다.

프로토콜 파서는 정상 데이터보다 잘린 헤더, 과도한 길이, 뒤에 남은 형식에 없는 바이트에서 더 많은 결함이 드러납니다.


파일 스트림 소유권

스트림을 연 메서드가 닫는 책임도 가지는 것이 기본입니다.

try-with-resources는 정상 반환과 예외 모두에서 close를 호출하고, 본문 예외와 닫기 예외가 동시에 나면 닫기 예외를 억제 예외로 보존합니다.

finally에서 수동으로 닫는 코드보다 원래 실패를 잃을 가능성이 작습니다.

src/FileSignature.java
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HexFormat;

public final class FileSignature {
    public static void main(String[] args) throws Exception {
        Path file = Files.createTempFile("signature-", ".bin");
        try {
            try (OutputStream output = Files.newOutputStream(file)) {
                output.write(new byte[] {(byte) 0xCA, (byte) 0xFE, 1, 2});
            }
            try (InputStream input = Files.newInputStream(file)) {
                byte[] signature = input.readNBytes(2);
                if (signature.length != 2) throw new IllegalStateException("short header");
                System.out.println(HexFormat.of().formatHex(signature));
            }
        } finally {
            Files.deleteIfExists(file);
        }
    }
}

상위 메서드가 전달한 스트림은 일반적으로 빌린 자원입니다.

변환 함수가 임의로 닫으면 호출자가 뒤이어 읽거나 여러 데이터를 이어 쓸 수 없습니다.

소유권을 받는 API라면 이름이나 문서에 닫기 책임을 드러내고, 그렇지 않으면 flush와 close를 호출자에게 맡깁니다.


FileInputStream의 역할

FileInputStream은 파일의 바이트를 읽는 구체적인 InputStream 구현입니다.

파일 경로나 File을 열어도 호출 코드는 InputStream의 read, readNBytes, transferTo 같은 공통 규칙으로 처리할 수 있습니다.

기존 API가 File을 받거나 파일 스트림 자체가 필요할 때 직접 사용하고, 새 코드에서 Path와 열기 옵션을 다룬다면 Files.newInputStream을 우선합니다.

src/FileInputStreamHeader.java
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HexFormat;

public final class FileInputStreamHeader {
    public static void main(String[] args) throws Exception {
        Path file = Files.createTempFile("file-input-", ".bin");
        try {
            Files.write(file, new byte[] {(byte) 0xCA, (byte) 0xFE, 1, 2, 3});

            try (FileInputStream fileInput = new FileInputStream(file.toFile())) {
                InputStream input = fileInput;
                byte[] header = input.readNBytes(2);
                if (header.length != 2) throw new IllegalStateException("short header");
                System.out.println("header=" + HexFormat.of().formatHex(header));
                System.out.println("remaining=" + input.readAllBytes().length);
            }
        } finally {
            Files.deleteIfExists(file);
        }
    }
}
header=cafe
remaining=3

생성자는 파일이 없을 때뿐 아니라 디렉터리이거나 운영체제가 열기를 거부할 때도 FileNotFoundException 계열 실패를 낼 수 있습니다.

성공적으로 열었다고 파일 내용이 고정되는 것은 아니며 다른 프로세스가 동시에 바꿀 수 있습니다.

available()은 남은 파일 전체 길이를 보장하는 API가 아니므로 크기나 EOF 판단에 사용하지 않습니다.

스트림을 다른 계층에 넘길 때는 누가 닫는지 명시하고, 빌린 FileDescriptor로 만든 스트림을 닫아 다른 소유자의 자원까지 무효화하지 않도록 주의합니다.


부분 읽기를 전제로 한 복사 루프

비어 있지 않은 배열에 대한 read(buffer)는 데이터가 있으면 1부터 배열 길이 사이의 값을 반환하고, 끝에서는 -1을 반환합니다.

마지막 조각뿐 아니라 중간에도 짧은 읽기가 가능합니다.

쓰기는 항상 write(buffer, 0, count)처럼 유효 구간만 전달해야 합니다.

배열 전체를 쓰면 이전 반복에서 남은 데이터가 결과 뒤에 섞입니다.

src/BoundedStreamCopy.java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;

public final class BoundedStreamCopy {
    static long copy(InputStream input, OutputStream output, long limit)
            throws IOException {
        byte[] buffer = new byte[4];
        long total = 0;
        int count;
        while ((count = input.read(buffer)) != -1) {
            total = Math.addExact(total, count);
            if (total > limit) throw new IOException("input limit exceeded");
            output.write(buffer, 0, count);
        }
        return total;
    }

    public static void main(String[] args) throws Exception {
        byte[] source = {1, 2, 3, 4, 5, 6, 7};
        var target = new ByteArrayOutputStream();
        System.out.println("count=" + copy(new ByteArrayInputStream(source), target, 10));
        System.out.println(Arrays.equals(source, target.toByteArray()));
    }
}

마지막 읽기의 유효 구간

같은 buffer의 read 4→read 3→EOF에서 유효 범위·누적 출력 4/7이 바뀝니다. 마지막의 옛 4를 쓰지 않는 지점을 고정된 슬롯에서 추적합니다.

마지막 읽기의 유효 구간

BoundedStreamCopy의 7바이트 메모리 입력과 4바이트 버퍼에서 반환된 개수만 출력하는 이유입니다.

부분 읽기 · 유효 바이트만 쓰기

마지막 읽기의 유효 구간

두 번째 읽기 뒤 버퍼의 마지막 4는 이전 데이터이며, count=3이면 쓰지 않습니다.

버퍼의 유효 구간과 출력량같은 버퍼의 배열 위치 0~3이다. 첫 read는 네 칸, 다음 read는 세 칸만 유효하다. 마지막 4는 이전 값이므로 출력하지 않는다. EOF에서는 배열과 출력이 그대로다.01122334출력에 1·2·3·4누적 4바이트05162734출력에 5·6·7누적 7바이트05162734EOF: write 없이 종료누적 7바이트 유지첫 read · count=4write(buffer, 0, 4)둘째 read · count=3write(buffer, 0, 3) · 마지막 4 제외다음 read · count=−1인덱스는 같은 배열 위치 · 화살표는 유효 구간의 출력
버퍼의 유효 구간과 출력량같은 버퍼의 배열 위치 0~3이다. 첫 read는 네 칸, 다음 read는 세 칸만 유효하다. 마지막 4는 이전 값이므로 출력하지 않는다. EOF에서는 배열과 출력이 그대로다.01122334출력에 1·2·3·4누적 4바이트05162734출력에 5·6·7누적 7바이트05162734write 없이 종료누적 7바이트 유지첫 read · count=4write(buffer, 0, 4)둘째 read · count=3마지막 4는 이전 값선택: 인덱스 0~2다음 read · count=−1EOF · 배열은 그대로이 원 메모리 입력의 값
  1. 첫 번째 · count = 4

    읽은 뒤 버퍼
    [1, 2, 3, 4]
    출력에 전달하는 값
    인덱스 0~3 · 1, 2, 3, 4
  2. 두 번째 · count = 3

    읽은 뒤 버퍼
    [5, 6, 7, 4]
    출력에 전달하는 값
    인덱스 0~2 · 5, 6, 7
  3. 세 번째 · count = −1

    읽은 뒤 버퍼
    [5, 6, 7, 4] 그대로
    출력에 전달하는 값
    EOF이므로 write 없이 종료

ByteArrayInputStream과 이 입력에서 도출한 값입니다. 두 번째 읽기 뒤의 마지막 4는 이전 데이터이므로 다시 쓰지 않습니다.

이 예의 조건

  • 같은 4바이트 배열을 재사용하며 read count만 유효합니다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보마지막 읽기의 유효 구간 마지막 읽기의 유효 구간 BoundedStreamCopy의 7바이트 메모리 입력과 4바이트 버퍼에서 반환된 개수만 출력하는 이유입니다. 부분 읽기 · 유효 바이트만 쓰기 마지막 읽기의 유효 구간 두 번째 읽기 뒤 버퍼의 마지막 4는 이전 데이터이며, count=3이면 쓰지 않습니다. 첫 번째 · count = 4 읽은 뒤 버퍼 [1, 2, 3, 4] 출력에 전달하는 값 인덱스 0~3 · 1, 2, 3, 4 두 번째 · count = 3 읽은 뒤 버퍼 [5, 6, 7, 4] 출력에 전달하는 값 인덱스 0~2 · 5, 6, 7 세 번째 · count = −1 읽은 뒤 버퍼 [5, 6, 7, 4] 그대로 출력에 전달하는 값 EOF이므로 write 없이 종료 ByteArrayInputStream과 이 입력에서 도출한 값입니다. 두 번째 읽기 뒤의 마지막 4는 이전 데이터이므로 다시 쓰지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 read

count 4인 1,2,3,4를 모두 씁니다.

  • 배열: [1,2,3,4]
  • 누적 출력: 4바이트
  • 유효 구간 0..3만 write합니다.
둘째 read

count 3인 5,6,7을 쓰고 마지막의 옛 4는 제외합니다.

  • 배열: [5,6,7,4]
  • 누적 출력: 7바이트
  • 배열 길이가 아닌 이번 count가 쓰기 범위를 정합니다.
EOF

read가 −1이면 write 없이 종료합니다.

  • 배열: [5,6,7,4] 유지
  • 출력: 7바이트 유지
  • EOF에서 이전 버퍼를 다시 쓰지 않습니다.

마지막 읽기의 유효 구간

같은 buffer의 read 4→read 3→EOF에서 유효 범위·누적 출력 4/7이 바뀝니다. 마지막의 옛 4를 쓰지 않는 지점을 고정된 슬롯에서 추적합니다.

마지막 읽기의 유효 구간

BoundedStreamCopy의 7바이트 메모리 입력과 4바이트 버퍼에서 반환된 개수만 출력하는 이유입니다.

부분 읽기 · 유효 바이트만 쓰기

마지막 읽기의 유효 구간

두 번째 읽기 뒤 버퍼의 마지막 4는 이전 데이터이며, count=3이면 쓰지 않습니다.

버퍼의 유효 구간과 출력량같은 버퍼의 배열 위치 0~3이다. 첫 read는 네 칸, 다음 read는 세 칸만 유효하다. 마지막 4는 이전 값이므로 출력하지 않는다. EOF에서는 배열과 출력이 그대로다.01122334출력에 1·2·3·4누적 4바이트05162734출력에 5·6·7누적 7바이트05162734EOF: write 없이 종료누적 7바이트 유지첫 read · count=4write(buffer, 0, 4)둘째 read · count=3write(buffer, 0, 3) · 마지막 4 제외다음 read · count=−1인덱스는 같은 배열 위치 · 화살표는 유효 구간의 출력
버퍼의 유효 구간과 출력량같은 버퍼의 배열 위치 0~3이다. 첫 read는 네 칸, 다음 read는 세 칸만 유효하다. 마지막 4는 이전 값이므로 출력하지 않는다. EOF에서는 배열과 출력이 그대로다.01122334출력에 1·2·3·4누적 4바이트05162734출력에 5·6·7누적 7바이트05162734write 없이 종료누적 7바이트 유지첫 read · count=4write(buffer, 0, 4)둘째 read · count=3마지막 4는 이전 값선택: 인덱스 0~2다음 read · count=−1EOF · 배열은 그대로이 원 메모리 입력의 값
  1. 첫 번째 · count = 4

    읽은 뒤 버퍼
    [1, 2, 3, 4]
    출력에 전달하는 값
    인덱스 0~3 · 1, 2, 3, 4
  2. 두 번째 · count = 3

    읽은 뒤 버퍼
    [5, 6, 7, 4]
    출력에 전달하는 값
    인덱스 0~2 · 5, 6, 7
  3. 세 번째 · count = −1

    읽은 뒤 버퍼
    [5, 6, 7, 4] 그대로
    출력에 전달하는 값
    EOF이므로 write 없이 종료

ByteArrayInputStream과 이 입력에서 도출한 값입니다. 두 번째 읽기 뒤의 마지막 4는 이전 데이터이므로 다시 쓰지 않습니다.

이 예의 조건

  • 같은 4바이트 배열을 재사용하며 read count만 유효합니다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보마지막 읽기의 유효 구간 마지막 읽기의 유효 구간 BoundedStreamCopy의 7바이트 메모리 입력과 4바이트 버퍼에서 반환된 개수만 출력하는 이유입니다. 부분 읽기 · 유효 바이트만 쓰기 마지막 읽기의 유효 구간 두 번째 읽기 뒤 버퍼의 마지막 4는 이전 데이터이며, count=3이면 쓰지 않습니다. 첫 번째 · count = 4 읽은 뒤 버퍼 [1, 2, 3, 4] 출력에 전달하는 값 인덱스 0~3 · 1, 2, 3, 4 두 번째 · count = 3 읽은 뒤 버퍼 [5, 6, 7, 4] 출력에 전달하는 값 인덱스 0~2 · 5, 6, 7 세 번째 · count = −1 읽은 뒤 버퍼 [5, 6, 7, 4] 그대로 출력에 전달하는 값 EOF이므로 write 없이 종료 ByteArrayInputStream과 이 입력에서 도출한 값입니다. 두 번째 읽기 뒤의 마지막 4는 이전 데이터이므로 다시 쓰지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 read

count 4인 1,2,3,4를 모두 씁니다.

  • 배열: [1,2,3,4]
  • 누적 출력: 4바이트
  • 유효 구간 0..3만 write합니다.
둘째 read

count 3인 5,6,7을 쓰고 마지막의 옛 4는 제외합니다.

  • 배열: [5,6,7,4]
  • 누적 출력: 7바이트
  • 배열 길이가 아닌 이번 count가 쓰기 범위를 정합니다.
EOF

read가 −1이면 write 없이 종료합니다.

  • 배열: [5,6,7,4] 유지
  • 출력: 7바이트 유지
  • EOF에서 이전 버퍼를 다시 쓰지 않습니다.

입력 상한은 메모리 고갈과 무제한 업로드를 막는 제품 규칙입니다.

이 함수는 상한을 넘긴 청크를 읽은 뒤 쓰기 전에 중단합니다. 이미 출력한 앞부분은 되돌리지 않으므로 부분 결과를 공개할 수 없다면 임시 경로에 쓴 뒤 성공했을 때만 게시합니다. 원자적 이동이 필요한 경우에는 파일 시스템의 지원 여부도 확인합니다.

예시

원 7바이트 메모리 입력에서 limit만 6으로 바꿔 생각해 보세요. 첫 read는 네 바이트를 읽어 total=4로 만든 뒤 출력합니다. 다음 read는 남은 세 바이트를 읽어 total=7이 되므로 상한 검사에서 실패하며 이 청크는 쓰지 않습니다. 따라서 입력에서 소비한 양은 7바이트이지만 출력에 남은 양은 4바이트입니다. 예외를 받았다고 입력 위치나 출력의 앞부분이 자동 복구된 것은 아닙니다. 일반 스트림의 짧은 읽기에서는 남는 출력량이 달라질 수 있으므로 이 숫자는 ByteArrayInputStream과 원 입력의 조건에 한정합니다.

제한을 초과한 요청의 스트림을 어디까지 비울지 여부는 연결 재사용 정책에 맞춰 정합니다.


readAllBytes를 선택할 수 있는 범위

작고 크기 상한이 확실한 설정 파일은 readAllBytes가 가장 읽기 쉬울 수 있습니다.

하지만 사용자 업로드나 압축 해제 결과처럼 크기를 신뢰할 수 없는 입력을 모두 메모리에 올리면 힙을 소진합니다.

파일 크기 메타데이터도 읽는 동안 바뀔 수 있고 압축 데이터의 해제 크기를 알려주지 않으므로 스트리밍 상한을 별도로 둡니다.

transferTo는 단순 복사를 간결하게 표현하지만 업무별 제한, 진행률, 해시 계산이 필요하면 명시적 루프가 더 적합합니다.

도구 선택은 줄 수가 아니라 관찰해야 할 중간 조건에 달려 있습니다.

어떤 구현을 써도 입력과 출력의 닫기 순서를 호출 규칙에 맞게 유지합니다.


연습 문제

두 바이트 길이 헤더와 본문으로 구성된 프레임을 읽으세요.

최대 본문은 1024바이트이며, 헤더나 본문이 잘리면 예외를 발생시켜야 합니다.

한 프레임 뒤의 바이트는 다음 판독을 위해 남겨 둡니다.

정답과 해설
exercise/ExactFrameReaderSolution.java
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

public final class ExactFrameReaderSolution {
    static byte[] readFrame(InputStream input) throws IOException {
        int high = input.read();
        int low = input.read();
        if (high == -1 || low == -1) throw new EOFException("short length header");
        int length = (high << 8) | low;
        if (length > 1024) throw new IOException("frame too large: " + length);
        byte[] payload = input.readNBytes(length);
        if (payload.length != length) throw new EOFException("short payload");
        return payload;
    }

    public static void main(String[] args) throws Exception {
        byte[] wire = {0, 3, 9, 8, 7, 99};
        var input = new ByteArrayInputStream(wire);
        System.out.println(Arrays.toString(readFrame(input)));
        System.out.println("next=" + input.read());
    }
}

한 프레임의 소비 범위

길이 00 03→payload 3바이트→현재 위치 5→뒤 read 99의 소비 범위입니다. 위치와 미소비 바이트를 단계로 구분하면 프레임 경계가 보입니다.

한 프레임의 소비 범위

ExactFrameReaderSolution의 wire에서 길이 헤더와 본문만 읽고 다음 바이트를 남깁니다.

프레임 경계 · 헤더와 본문

한 프레임의 소비 범위

길이 헤더 00 03과 본문 09 08 07만 읽고, 다음 바이트 63은 남겨 둡니다.

헤더와 본문을 소비하고 다음 바이트를 남긴다wire의 바이트0~1은00 03,2~4는09 08 07,5는63이다.헤더에서길이3을정한뒤본문3바이트만읽어반환한다.다음바이트63은readFrame가소비하지않고다음input.read가99로반환한다.wire[0..1]00 03 · 길이 3크기 검사 통과readNBytes(3)wire[2..4]09 08 07 반환다음 위치: 5readFrame 종료wire[5] · 남은 바이트63 (16진수) = 99아직 읽지 않음다음 input.read() → 99실선: 이번 readFrame가 처리한 범위 · 남은 바이트로 가는 소비 간선 없음
헤더와 본문을 소비하고 다음 바이트를 남긴다wire의 바이트0~1은00 03,2~4는09 08 07,5는63이다.헤더에서길이3을정한뒤본문3바이트만읽어반환한다.다음바이트63은readFrame가소비하지않고다음input.read가99로반환한다.wire[0..1] · 헤더00 03 → 길이 3readNBytes(3)길이 상한 검사를 통과wire[2..4] · 본문09 08 07세 바이트를 반환wire[5] · 남은 바이트63 (16진수) = 99아직 읽지 않음다음 위치: 5readFrame 종료위치 5의 값은 아래에 남음그다음 input.read() → 99

0~1 · 헤더

값
00 03 (16진수)
readFrame의 처리
본문 길이 3으로 조합

2~4 · 본문

값
09 08 07 (16진수)
readFrame의 처리
3바이트를 읽어 반환

5 · 다음 데이터

값
63 (16진수) = 99
readFrame의 처리
남겨 둠 · 다음 input.read()가 반환

길이가 1024를 넘으면 본문을 읽기 전에 실패하며, 헤더 또는 본문이 부족하면 EOFException을 던집니다.

이 예의 조건

  • wire=00 03 09 08 07 63이며 16진수 63은 십진수 99입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2wire의 바이트 0~1은 00 03, 2~4는 09 08 07, 5는 63이다. 헤더에서 길이 3을 정한 뒤 본문 3바이트만 읽어 반환한다. 다음 바이트 63은 readFrame이 소비하지 않고 다음 input.read가 99로 반환한다.
15실선: 이번 readFrame이 처리한 범위 · 남은 바이트로 가는 소비 간선 없음
원 도표의 설명과 주석
구분내용
설명wire의 바이트 0~1은 00 03, 2~4는 09 08 07, 5는 63이다. 헤더에서 길이 3을 정한 뒤 본문 3바이트만 읽어 반환한다. 다음 바이트 63은 readFrame이 소비하지 않고 다음 input.read가 99로 반환한다.
전체 읽기 정보한 프레임의 소비 범위 한 프레임의 소비 범위 ExactFrameReaderSolution의 wire에서 길이 헤더와 본문만 읽고 다음 바이트를 남깁니다. 프레임 경계 · 헤더와 본문 한 프레임의 소비 범위 길이 헤더 00 03과 본문 09 08 07만 읽고, 다음 바이트 63은 남겨 둡니다. 0~1 · 헤더 값 00 03 (16진수) readFrame의 처리 본문 길이 3으로 조합 2~4 · 본문 값 09 08 07 (16진수) readFrame의 처리 3바이트를 읽어 반환 5 · 다음 데이터 값 63 (16진수) = 99 readFrame의 처리 남겨 둠 · 다음 input.read()가 반환 길이가 1024를 넘으면 본문을 읽기 전에 실패하며, 헤더 또는 본문이 부족하면 EOFException을 던집니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
헤더 소비

wire 0..1의 00 03으로 길이 3을 읽습니다.

  • 길이: 3
  • 검사: 크기 통과
  • 본문 읽기량은 헤더가 정한 3입니다.
본문 반환

readNBytes(3)가 wire 2..4를 읽습니다.

  • 반환: 09 08 07
  • readFrame은 다음 프레임 앞 바이트까지 소비하지 않습니다.
남은 바이트

종료 뒤 별도 input.read가 wire 5를 읽습니다.

  • readFrame 뒤: 63 남음
  • 다음 read: 99 반환
  • 프레임 함수의 소비 범위와 후속 읽기를 구별합니다.

한 프레임의 소비 범위

길이 00 03→payload 3바이트→현재 위치 5→뒤 read 99의 소비 범위입니다. 위치와 미소비 바이트를 단계로 구분하면 프레임 경계가 보입니다.

한 프레임의 소비 범위

ExactFrameReaderSolution의 wire에서 길이 헤더와 본문만 읽고 다음 바이트를 남깁니다.

프레임 경계 · 헤더와 본문

한 프레임의 소비 범위

길이 헤더 00 03과 본문 09 08 07만 읽고, 다음 바이트 63은 남겨 둡니다.

헤더와 본문을 소비하고 다음 바이트를 남긴다wire의 바이트0~1은00 03,2~4는09 08 07,5는63이다.헤더에서길이3을정한뒤본문3바이트만읽어반환한다.다음바이트63은readFrame가소비하지않고다음input.read가99로반환한다.wire[0..1]00 03 · 길이 3크기 검사 통과readNBytes(3)wire[2..4]09 08 07 반환다음 위치: 5readFrame 종료wire[5] · 남은 바이트63 (16진수) = 99아직 읽지 않음다음 input.read() → 99실선: 이번 readFrame가 처리한 범위 · 남은 바이트로 가는 소비 간선 없음
헤더와 본문을 소비하고 다음 바이트를 남긴다wire의 바이트0~1은00 03,2~4는09 08 07,5는63이다.헤더에서길이3을정한뒤본문3바이트만읽어반환한다.다음바이트63은readFrame가소비하지않고다음input.read가99로반환한다.wire[0..1] · 헤더00 03 → 길이 3readNBytes(3)길이 상한 검사를 통과wire[2..4] · 본문09 08 07세 바이트를 반환wire[5] · 남은 바이트63 (16진수) = 99아직 읽지 않음다음 위치: 5readFrame 종료위치 5의 값은 아래에 남음그다음 input.read() → 99

0~1 · 헤더

값
00 03 (16진수)
readFrame의 처리
본문 길이 3으로 조합

2~4 · 본문

값
09 08 07 (16진수)
readFrame의 처리
3바이트를 읽어 반환

5 · 다음 데이터

값
63 (16진수) = 99
readFrame의 처리
남겨 둠 · 다음 input.read()가 반환

길이가 1024를 넘으면 본문을 읽기 전에 실패하며, 헤더 또는 본문이 부족하면 EOFException을 던집니다.

이 예의 조건

  • wire=00 03 09 08 07 63이며 16진수 63은 십진수 99입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2wire의 바이트 0~1은 00 03, 2~4는 09 08 07, 5는 63이다. 헤더에서 길이 3을 정한 뒤 본문 3바이트만 읽어 반환한다. 다음 바이트 63은 readFrame이 소비하지 않고 다음 input.read가 99로 반환한다.
15실선: 이번 readFrame이 처리한 범위 · 남은 바이트로 가는 소비 간선 없음
원 도표의 설명과 주석
구분내용
설명wire의 바이트 0~1은 00 03, 2~4는 09 08 07, 5는 63이다. 헤더에서 길이 3을 정한 뒤 본문 3바이트만 읽어 반환한다. 다음 바이트 63은 readFrame이 소비하지 않고 다음 input.read가 99로 반환한다.
전체 읽기 정보한 프레임의 소비 범위 한 프레임의 소비 범위 ExactFrameReaderSolution의 wire에서 길이 헤더와 본문만 읽고 다음 바이트를 남깁니다. 프레임 경계 · 헤더와 본문 한 프레임의 소비 범위 길이 헤더 00 03과 본문 09 08 07만 읽고, 다음 바이트 63은 남겨 둡니다. 0~1 · 헤더 값 00 03 (16진수) readFrame의 처리 본문 길이 3으로 조합 2~4 · 본문 값 09 08 07 (16진수) readFrame의 처리 3바이트를 읽어 반환 5 · 다음 데이터 값 63 (16진수) = 99 readFrame의 처리 남겨 둠 · 다음 input.read()가 반환 길이가 1024를 넘으면 본문을 읽기 전에 실패하며, 헤더 또는 본문이 부족하면 EOFException을 던집니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
헤더 소비

wire 0..1의 00 03으로 길이 3을 읽습니다.

  • 길이: 3
  • 검사: 크기 통과
  • 본문 읽기량은 헤더가 정한 3입니다.
본문 반환

readNBytes(3)가 wire 2..4를 읽습니다.

  • 반환: 09 08 07
  • readFrame은 다음 프레임 앞 바이트까지 소비하지 않습니다.
남은 바이트

종료 뒤 별도 input.read가 wire 5를 읽습니다.

  • readFrame 뒤: 63 남음
  • 다음 read: 99 반환
  • 프레임 함수의 소비 범위와 후속 읽기를 구별합니다.

두 헤더 바이트는 read가 반환한 부호 없음 범위의 int이므로 바로 비트 조합할 수 있습니다.

readNBytes 결과 길이를 다시 확인해 조기 EOF를 검출하고, readAllBytes를 쓰지 않아 다음 프레임 바이트를 소비하지 않습니다.

확인 문제

새 입력을 [0, 0, 99]로 주면 반환 본문과 다음 바이트는 무엇인가요? 또 [4, 1, 9, 8]은 어떤 단계에서 실패하며 본문까지 읽나요? 숫자는 모두 바이트의 부호 없는 값입니다.

확인 문제 해설

00 00은 길이 0이므로 빈 본문을 정상 반환하고 다음 input.read는 99를 읽습니다. 04 01은 (4 × 256) + 1 = 1,025바이트를 요구하므로 헤더 두 바이트만 읽은 뒤 크기 상한 검사에서 실패합니다. 본문 9·8은 아직 읽지 않았습니다. 헤더가 00 하나뿐이면 low가 -1이어서 short length header이고, 00 03 뒤 본문이 두 바이트뿐이면 short payload입니다.


스트림 경계를 안전하다고 판단하는 조건

안전한 바이트 입출력은 EOF, 부분 읽기, 크기 제한, 닫기 책임을 코드로 표현합니다.

같은 복사 함수를 메모리와 파일 입력에 적용해 보고 잘린 데이터와 초과 입력까지 실패시키면 스트림 추상화의 편의와 장치 경계의 안전성을 함께 얻을 수 있습니다.