3주차 Unit 8.3 — byte[] 배열로 효율적 읽기

Psj·2026년 5월 20일

F-lab

목록 보기
108/237

Unit 8.3 — byte[] 배열로 효율적 읽기

F-LAB JAVA · 3주차 · Phase 8 · Stream 실전


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • read(byte[]) 의 정확한 동작과 반환값은?
  • read(byte[], off, len) 의 3가지 매개변수의 의미는?
  • 1바이트 읽기 vs byte[] 읽기 의 성능 차이는?
  • 마지막 읽기 함정 (n 만큼만 처리, buf.length 아님) 의 정확한 메커니즘은?
  • for (byte b : buf) 가 왜 위험 한가?
  • 버퍼 크기 선택 의 기준은? (1KB, 4KB, 8KB, 64KB)
  • read 가 항상 buf.length 만큼 채우지 않는 이유 는?
  • readNBytes (Java 9+) 와의 차이는?
  • transferTo (Java 9+) 의 활용은?

🎯 핵심 한 문장

read(byte[] buf) 는 한 번의 호출로 최대 buf.length 만큼 읽지만, 실제 읽은 바이트 수는 반환값 n 만큼이며 buf 의 0~n-1 만 유효하다.
1바이트씩 read 는 매 호출이 system call 이지만 byte[] 활용은 한 번에 여러 바이트를 받아 호출 횟수를 절감 — 1MB 파일 처리 시 수천~수만 배 빨라진다.
가장 흔한 함정은 for (byte b : buf) — 마지막 read 에서 n < buf.length 인 경우 이전 데이터가 남아 잘못된 결과.
해결: for (int k = 0; k < n; k++) 또는 process(buf, 0, n) 으로 n 만큼만 처리.
버퍼 크기는 보통 4~8KB (OS 페이지 크기), 대용량 I/O 에선 64KB 도 좋음.

비유 — 양동이로 물 받기

1바이트씩 (컵 한 잔씩):
  - 매번 수도꼭지 ↔ 컵
  - 수천 번 왕복
  - 매우 느림

byte[] 활용 (양동이로):
  - 한 번에 8KB 양동이 채움
  - 같은 양 → 128번 왕복
  - 빠름

함정 (마지막 양동이):
  - 1MB 파일을 8KB 양동이로
  - 128번째: 8192바이트 (가득)
  - 129번째: 부분만 (1024바이트)
  - "양동이 전체" 로 처리하면 ★ 이전 데이터 남음
  - "n 만큼만" (1024) 처리해야 정확

→ byte[] = 효율 + 정확한 n 처리.


🧭 9개 섹션 로드맵

1. read(byte[]) 의 정확한 동작
2. read(byte[], off, len) 의 매개변수
3. 1바이트 vs byte[] 성능 비교
4. 마지막 읽기 함정 — n 만큼만!
5. for-each 의 위험성
6. 버퍼 크기 선택의 기준
7. read 가 buf.length 안 채우는 이유
8. readNBytes, transferTo (Java 9+)
9. 면접 + 자기 점검

1️⃣ read(byte[]) 의 정확한 동작

1.1 시그니처

public int read(byte[] b) throws IOException;
// 또는 (내부적으로)
public int read(byte[] b) throws IOException {
    return read(b, 0, b.length);
}

핵심:

  • 매개변수: 채울 byte 배열
  • 반환: 실제 읽은 바이트 수
  • 또는 -1 (EOF)

1.2 동작 단계

사용자 코드:
  byte[] buf = new byte[8192];
  int n = fis.read(buf);

JVM 내부:
  1. native 메서드 호출
  2. JNI 통해 OS read 시스템 호출
  3. user → kernel 전환
  4. kernel 이 파일에서 최대 buf.length 바이트 읽음
  5. user buffer 에 복사
  6. kernel → user 전환
  7. 실제 읽은 바이트 수 반환 (n)

n 의 의미:
  - 양수 (1 ~ buf.length): 실제 읽은 수
  - -1: EOF (한 바이트도 못 읽음)
  - 0: 매우 드뭄 (보통 Blocking 에서)

1.3 시각화

파일 내용 (20 바이트):
  [A B C D E F G H I J K L M N O P Q R S T]
   0 1 2 3 4 5 6 7 8 9 10..............19

버퍼 (8 바이트):
  byte[] buf = new byte[8];

read 호출 1:
  int n = fis.read(buf);   // n = 8
  buf: [A, B, C, D, E, F, G, H]   ← 가득
  파일 위치: 8

read 호출 2:
  int n = fis.read(buf);   // n = 8
  buf: [I, J, K, L, M, N, O, P]   ← 가득
  파일 위치: 16

read 호출 3:
  int n = fis.read(buf);   // n = 4 (남은 만큼)
  buf: [Q, R, S, T, M, N, O, P]   ← 0~3 만 유효!
  파일 위치: 20
  
  ★ buf 의 4~7 은 이전 데이터 (M, N, O, P)
  ★ 0 ~ n-1 (0~3) 만 처리해야

read 호출 4:
  int n = fis.read(buf);   // n = -1 (EOF)

1.4 기본 사용 패턴

// 가장 일반적
try (FileInputStream fis = new FileInputStream("file.txt")) {
    byte[] buf = new byte[8192];
    int n;
    
    while ((n = fis.read(buf)) != -1) {
        // 0 ~ n-1 처리 (n 까지가 아님!)
        process(buf, 0, n);
    }
}

// 잘못된 패턴 (다음 섹션에서 정밀)
// for (byte b : buf) { ... }   // ❌

1.5 read(byte[]) 의 보장

보장:
  - n ≥ 0 (음수는 -1 만)
  - n ≤ buf.length
  - 데이터는 buf[0] ~ buf[n-1] 에
  - buf[n] ~ buf[buf.length-1] 는 이전 값

다음 호출:
  - 파일 위치는 자동 증가
  - 다음 read 는 이어서

EOF:
  - n = -1 반환
  - buf 는 변경 없음 (또는 일부만)
  - 보장 X: 정확한 상태

1.6 ILIC 의 활용

public class ShipmentFileReader {
    
    private static final int BUFFER_SIZE = 8192;   // 8KB
    
    public byte[] readAllBytes(Path path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path.toFile());
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = fis.read(buf)) != -1) {
                baos.write(buf, 0, n);   // ★ n 만큼만
            }
            
            return baos.toByteArray();
        }
    }
}

1.7 자기 점검 답변

read(byte[]) 의 정확한 동작은?

:
1. 시그니처:

  • int read(byte[] b)
  • 반환: 실제 읽은 수
  1. 반환값:

    • 1 ~ buf.length: 정상
    • -1: EOF
    • 0: 드뭄
  2. 데이터 위치:

    • buf[0] ~ buf[n-1] 유효
    • 그 이후는 이전 데이터
  3. 호출 결과:

    • 파일 위치 자동 증가
    • 다음 read 는 이어서
  4. 권장 패턴:

    • while ((n = read(buf)) != -1)
    • process(buf, 0, n) — n 만큼만

2️⃣ read(byte[], off, len) 의 매개변수

2.1 시그니처

public int read(byte[] b, int off, int len) throws IOException;

// 매개변수:
// - b: 채울 배열
// - off: 채우기 시작 위치 (offset)
// - len: 최대 채울 바이트 수

// 동작:
// - b[off] ~ b[off+len-1] 채움
// - 실제 읽은 수 반환 (0 ~ len)
// - -1 (EOF)

2.2 활용

// 부분 채우기
byte[] buf = new byte[1024];

// buf 의 100 위치부터 200 바이트 읽기
int n = fis.read(buf, 100, 200);
// buf[100] ~ buf[299] 에 데이터
// buf[0~99] 와 buf[300~1023] 은 변경 X

// 효과:
// - 일부분만 채우기
// - 다른 데이터와 결합
// - 디스플레이 영역 등

2.3 read(byte[]) 와 read(byte[], 0, b.length)

// 같은 동작
fis.read(buf);
fis.read(buf, 0, buf.length);

// 내부적으로 read(byte[]) 가 read(byte[], 0, length) 호출
public int read(byte[] b) throws IOException {
    return read(b, 0, b.length);
}

2.4 검증

// IndexOutOfBoundsException 가능
fis.read(buf, -1, 100);   // off < 0
fis.read(buf, 0, -1);     // len < 0
fis.read(buf, 100, 1000); // off + len > buf.length

// 자바 표준 InputStream 의 검증
// IndexOutOfBoundsException 또는 NullPointerException

2.5 부분 읽기의 패턴

// 시나리오: 헤더 (10바이트) + 본문 분리
public byte[] readWithHeader(InputStream is) throws IOException {
    byte[] result = new byte[1024];
    
    // 1. 헤더 10바이트
    int n = is.read(result, 0, 10);
    if (n != 10) throw new EOFException("Header incomplete");
    
    // 2. 본문 (나머지)
    n = is.read(result, 10, 1014);
    // result 의 10 ~ 1023 위치에 채워짐
    
    return result;
}

2.6 readFully 와 비교 (DataInputStream)

// FileInputStream.read 는 buf.length 안 채울 수 있음
byte[] buf = new byte[100];
int n = fis.read(buf);
// n < 100 가능

// DataInputStream.readFully 는 정확히 채움
DataInputStream dis = new DataInputStream(fis);
dis.readFully(buf);
// 정확히 100 바이트, 부족하면 EOFException

// 사용자 구현
public static void readFully(InputStream is, byte[] buf, int off, int len) 
        throws IOException {
    int totalRead = 0;
    while (totalRead < len) {
        int n = is.read(buf, off + totalRead, len - totalRead);
        if (n == -1) throw new EOFException();
        totalRead += n;
    }
}

// Java 9+ 의 readNBytes
int n = fis.readNBytes(buf, 0, 100);
// 정확히 100 (또는 EOF)

2.7 ILIC 의 활용

public class ShipmentBinaryReader {
    
    // 헤더 분리 처리
    public ShipmentRecord readRecord(InputStream is) throws IOException {
        byte[] buf = new byte[256];
        
        // 1. 헤더 16바이트
        int n = is.read(buf, 0, 16);
        if (n != 16) throw new EOFException("Header incomplete");
        ShipmentHeader header = parseHeader(buf, 0, 16);
        
        // 2. 본문 (헤더의 length 기반)
        int bodyLength = header.getBodyLength();
        n = is.read(buf, 16, bodyLength);
        if (n != bodyLength) throw new EOFException("Body incomplete");
        ShipmentBody body = parseBody(buf, 16, bodyLength);
        
        return new ShipmentRecord(header, body);
    }
    
    // readNBytes 활용 (Java 9+)
    public byte[] readExact(InputStream is, int length) throws IOException {
        byte[] buf = new byte[length];
        int n = is.readNBytes(buf, 0, length);
        if (n != length) {
            throw new EOFException("Expected " + length + " bytes, got " + n);
        }
        return buf;
    }
}

2.8 자기 점검 답변

read(byte[], off, len) 의 매개변수와 활용은?

:
1. 매개변수:

  • b: 채울 배열
  • off: 시작 위치
  • len: 최대 채울 수
  1. 반환:

    • 실제 읽은 수 (0 ~ len)
    • -1 (EOF)
  2. 데이터:

    • b[off] ~ b[off+n-1] 채워짐
    • 그 외 위치 변경 X
  3. 활용:

    • 부분 채우기
    • 헤더 + 본문 분리
    • 다른 데이터와 결합
  4. readFully (DataInputStream):

    • 정확히 채움
    • 부족하면 EOFException
    • Java 9+ readNBytes 도 유사

3️⃣ 1바이트 vs byte[] 성능 비교

3.1 시스템 호출 횟수

파일 크기 1MB (1,048,576 바이트):

1바이트씩 read:
  - 호출 횟수: 1,048,576
  - 각 호출 = 1 system call

byte[] (1KB 버퍼):
  - 호출 횟수: 1024
  - 1000배 절감

byte[] (8KB 버퍼):
  - 호출 횟수: 128
  - 약 8000배 절감

byte[] (64KB 버퍼):
  - 호출 횟수: 16
  - 약 65000배 절감

3.2 실제 시간 측정 (예시)

public class ReadPerformanceBenchmark {
    
    public static void main(String[] args) throws IOException {
        Path file = Path.of("1MB.dat");
        
        // 1바이트씩
        long start = System.nanoTime();
        try (FileInputStream fis = new FileInputStream(file.toFile())) {
            while (fis.read() != -1) { }
        }
        long oneByteTime = System.nanoTime() - start;
        
        // byte[] 1KB
        start = System.nanoTime();
        try (FileInputStream fis = new FileInputStream(file.toFile())) {
            byte[] buf = new byte[1024];
            while (fis.read(buf) != -1) { }
        }
        long oneKBTime = System.nanoTime() - start;
        
        // byte[] 8KB
        start = System.nanoTime();
        try (FileInputStream fis = new FileInputStream(file.toFile())) {
            byte[] buf = new byte[8192];
            while (fis.read(buf) != -1) { }
        }
        long eightKBTime = System.nanoTime() - start;
        
        // BufferedInputStream
        start = System.nanoTime();
        try (FileInputStream fis = new FileInputStream(file.toFile());
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            while (bis.read() != -1) { }
        }
        long bufferedTime = System.nanoTime() - start;
        
        // 결과
        System.out.printf("1 byte: %d ns%n", oneByteTime);
        System.out.printf("1 KB:   %d ns%n", oneKBTime);
        System.out.printf("8 KB:   %d ns%n", eightKBTime);
        System.out.printf("Buffered: %d ns%n", bufferedTime);
    }
}

3.3 예상 결과

1MB 파일 읽기 (대략적 결과):

1바이트씩:        ~1,000,000,000 ns (1 초)
byte[] 1KB:          ~1,500,000 ns (1.5 ms)  — 약 670배
byte[] 8KB:            ~500,000 ns (0.5 ms)  — 약 2000배
byte[] 64KB:           ~300,000 ns (0.3 ms)  — 약 3300배
Buffered (1바이트):    ~3,000,000 ns (3 ms)   — 약 333배

결론:
  - byte[] 가 압도적으로 빠름
  - 8KB 버퍼가 sweet spot
  - 64KB 이상은 큰 차이 없음

3.4 왜 이렇게 빠른가?

이유:

1. 시스템 호출 횟수
   - 매 system call = ~1μs 오버헤드
   - 1바이트: 100만 번 = 1초
   - 8KB: 128번 = 128μs

2. 디스크 I/O 의 단위
   - 디스크는 블록 단위 (4KB)
   - 1바이트 요청도 4KB 읽음
   - 큰 버퍼가 효율

3. CPU 캐시
   - 큰 버퍼 = 캐시 히트율 ↑
   - 1바이트 = 캐시 비효율

4. JIT 최적화
   - 큰 버퍼 처리 = SIMD 등 활용
   - 1바이트 = 최적화 제한

3.5 BufferedInputStream 과의 비교

// 방법 1: byte[]
try (FileInputStream fis = new FileInputStream("file.txt")) {
    byte[] buf = new byte[8192];
    int n;
    while ((n = fis.read(buf)) != -1) {
        process(buf, 0, n);
    }
}

// 방법 2: BufferedInputStream
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("file.txt"))) {
    int b;
    while ((b = bis.read()) != -1) {
        // 사용자는 1바이트씩
        // 내부 8KB 버퍼
        process((byte) b);
    }
}

// 차이:
// - 방법 1: 사용자가 직접 byte[] 다룸
// - 방법 2: 1바이트 API 그대로, 내부 버퍼링
// - 성능: 비슷 (둘 다 효율적)
// - 가독성: 방법 2 가 더 단순

3.6 두 방법의 조합

// BufferedInputStream + byte[] (가장 효율)
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("file.txt"))) {
    byte[] buf = new byte[8192];
    int n;
    while ((n = bis.read(buf)) != -1) {
        process(buf, 0, n);
    }
}

// 효과:
// - BufferedInputStream 의 버퍼링
// - byte[] 의 일괄 처리
// - 사실상 가장 빠름
// - 단, BufferedInputStream + byte[] 큰 차이는 X

3.7 ILIC 의 패턴

public class HighPerformanceFileReader {
    
    private static final int OPTIMAL_BUFFER_SIZE = 8192;   // 8KB
    
    // 표준 패턴 — byte[] 활용
    public long readAndCount(Path path) throws IOException {
        long count = 0;
        
        try (FileInputStream fis = new FileInputStream(path.toFile())) {
            byte[] buf = new byte[OPTIMAL_BUFFER_SIZE];
            int n;
            while ((n = fis.read(buf)) != -1) {
                count += n;
            }
        }
        
        return count;
    }
    
    // BufferedInputStream + byte[]
    public byte[] readAllBuffered(Path path) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(path.toFile()));
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            
            byte[] buf = new byte[OPTIMAL_BUFFER_SIZE];
            int n;
            while ((n = bis.read(buf)) != -1) {
                baos.write(buf, 0, n);
            }
            
            return baos.toByteArray();
        }
    }
}

3.8 자기 점검 답변

1바이트 vs byte[] 의 성능 차이는?

:
1. 시스템 호출 횟수:

  • 1바이트: 1MB = 100만 번
  • byte[8KB]: 128번
  • 약 8000배 절감
  1. 이유:

    • system call 오버헤드 (~1μs)
    • 디스크 블록 단위 I/O (4KB)
    • CPU 캐시 효율
    • JIT 최적화
  2. 권장:

    • byte[] 8KB ~ 64KB
    • 또는 BufferedInputStream
    • 또는 둘 다 결합
  3. 실측:

    • 1MB 파일
    • 1바이트: 1초
    • byte[8KB]: 0.5ms
    • 약 2000배

4️⃣ 마지막 읽기 함정 — n 만큼만!

4.1 마지막 읽기의 시나리오

시나리오:
  파일 크기: 20 바이트
  버퍼 크기: 8 바이트

read 호출 결과:
  1. n = 8 (가득)
  2. n = 8 (가득)
  3. n = 4 (마지막, 부분만)   ← 함정 지점
  4. n = -1 (EOF)

4.2 함정의 발생

// ❌ 함정 — buf.length 사용
try (FileInputStream fis = new FileInputStream("20bytes.dat")) {
    byte[] buf = new byte[8];
    int n;
    while ((n = fis.read(buf)) != -1) {
        // 잘못 — buf 전체 처리
        for (int i = 0; i < buf.length; i++) {
            System.out.print((char) buf[i]);
        }
    }
}

// 동작:
// 1. n = 8 → buf 가득, 0~7 처리 (정상)
// 2. n = 8 → buf 가득, 0~7 처리 (정상, 이전 데이터 덮어씀)
// 3. n = 4 → 0~3 만 새 데이터, 4~7 은 이전 데이터!
//    하지만 buf.length 만큼 처리 → 잘못된 결과

4.3 시각화

파일: [A B C D E F G H I J K L M N O P Q R S T]

읽기 1: buf = [A B C D E F G H]  n=8 ✓
읽기 2: buf = [I J K L M N O P]  n=8 ✓
읽기 3: buf = [Q R S T M N O P]  n=4 ★ 4~7 은 이전 데이터!

잘못된 처리 (buf.length):
  Q R S T M N O P   ← 잘못된 출력 (M N O P 가 두 번 나옴)

올바른 처리 (n 만큼):
  Q R S T            ← 정확한 출력

4.4 올바른 처리

// ✓ 올바름 — n 만큼만
try (FileInputStream fis = new FileInputStream("20bytes.dat")) {
    byte[] buf = new byte[8];
    int n;
    while ((n = fis.read(buf)) != -1) {
        // n 까지만 처리
        for (int i = 0; i < n; i++) {
            System.out.print((char) buf[i]);
        }
    }
}

// 또는 write 에 0, n 전달
try (FileInputStream fis = new FileInputStream("src.dat");
     FileOutputStream fos = new FileOutputStream("dest.dat")) {
    byte[] buf = new byte[8192];
    int n;
    while ((n = fis.read(buf)) != -1) {
        fos.write(buf, 0, n);   // ★ 0, n
    }
}

// 또는 ByteArrayOutputStream
try (FileInputStream fis = new FileInputStream("file.dat");
     ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    byte[] buf = new byte[8192];
    int n;
    while ((n = fis.read(buf)) != -1) {
        baos.write(buf, 0, n);   // ★ 0, n
    }
}

4.5 흔한 실수

// 실수 1: for-each (다음 섹션에서 정밀)
for (byte b : buf) {
    process(b);
    // 항상 buf.length 만큼
    // 마지막 부분에서 이전 데이터 포함
}

// 실수 2: buf.length 직접
for (int i = 0; i < buf.length; i++) {
    process(buf[i]);
}

// 실수 3: 잘못된 write
fos.write(buf);   // buf.length 만큼 쓰기 (이전 데이터 포함)
// 또는
fos.write(buf, 0, buf.length);   // 위와 동일

// ✓ 올바름
fos.write(buf, 0, n);   // n 만큼만

4.6 String 변환 시 함정

// 파일 읽어서 String 으로
byte[] buf = new byte[8192];
int n = fis.read(buf);

// ❌ 잘못
String s = new String(buf, StandardCharsets.UTF_8);
// buf.length 모두 변환
// 끝에 \0 또는 이전 데이터 포함

// ✓ 올바름
String s = new String(buf, 0, n, StandardCharsets.UTF_8);
// 0 ~ n-1 만 변환

4.7 ILIC 의 안전한 패턴

public class SafeFileReader {
    
    // 패턴 1: ByteArrayOutputStream
    public byte[] readAll(InputStream is) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buf = new byte[8192];
            int n;
            while ((n = is.read(buf)) != -1) {
                baos.write(buf, 0, n);   // ✓ n 만큼
            }
            return baos.toByteArray();
        }
    }
    
    // 패턴 2: process(buf, 0, n)
    public void processStream(InputStream is, ByteConsumer consumer) throws IOException {
        byte[] buf = new byte[8192];
        int n;
        while ((n = is.read(buf)) != -1) {
            consumer.consume(buf, 0, n);   // ✓ 0, n 명시
        }
    }
    
    // 패턴 3: String 변환
    public String readAsString(InputStream is) throws IOException {
        StringBuilder sb = new StringBuilder();
        byte[] buf = new byte[8192];
        int n;
        while ((n = is.read(buf)) != -1) {
            sb.append(new String(buf, 0, n, StandardCharsets.UTF_8));
            // ✓ 0, n 만 변환
        }
        return sb.toString();
    }
    
    // 패턴 4: 복사
    public void copy(InputStream src, OutputStream dest) throws IOException {
        byte[] buf = new byte[8192];
        int n;
        while ((n = src.read(buf)) != -1) {
            dest.write(buf, 0, n);   // ✓ 0, n
        }
    }
    
    // 또는 Java 9+
    public void copyEasy(InputStream src, OutputStream dest) throws IOException {
        src.transferTo(dest);   // 알아서 처리
    }
    
    @FunctionalInterface
    interface ByteConsumer {
        void consume(byte[] buf, int off, int len);
    }
}

4.8 자기 점검 답변

마지막 읽기 함정의 메커니즘은?

:
1. 시나리오:

  • 파일 20바이트, 버퍼 8바이트
  • 마지막 read 가 4바이트만 (n=4)
  • buf[4]~buf[7] 은 이전 데이터
  1. 함정:

    • buf.length 사용 → 이전 데이터 포함
    • for-each → buf.length 처리
  2. 해결:

    • buf[0] ~ buf[n-1] 만 유효
    • process(buf, 0, n) 패턴
    • for (int i = 0; i < n; i++)
  3. 흔한 위치:

    • write: fos.write(buf, 0, n)
    • String: new String(buf, 0, n, charset)
    • copy: dest.write(buf, 0, n)

5️⃣ for-each 의 위험성

5.1 for-each 가 왜 위험한가

// ❌ for-each 의 함정
byte[] buf = new byte[8192];
int n = fis.read(buf);

for (byte b : buf) {
    process(b);
    // 항상 buf.length (8192) 만큼 반복
    // 마지막 read 에서 n < 8192 면 잘못된 결과
}

// 이유:
// - for-each 는 배열 전체 순회
// - n 을 무시
// - 이전 데이터 또는 0 (초기값) 처리

5.2 시뮬레이션

// 파일: "HELLO" (5바이트)
// 버퍼: byte[8]

try (FileInputStream fis = new FileInputStream("hello.txt")) {
    byte[] buf = new byte[8];
    int n = fis.read(buf);   // n = 5
    
    // 잘못 — for-each
    for (byte b : buf) {
        System.out.print((char) b);
    }
    // 출력: "HELLO\0\0\0" (\0 = 초기값 0)
    // 5글자 후 0 (null) 3개
}

// 올바름
for (int i = 0; i < n; i++) {
    System.out.print((char) buf[i]);
}
// 출력: "HELLO" ✓

5.3 더 큰 함정 — 재사용

// 버퍼 재사용 시 더 큰 함정
try (FileInputStream fis = new FileInputStream("file.txt")) {
    byte[] buf = new byte[8];
    int n;
    StringBuilder sb = new StringBuilder();
    
    while ((n = fis.read(buf)) != -1) {
        // ❌ for-each
        for (byte b : buf) {
            sb.append((char) b);
        }
        // 마지막 read 에서 이전 데이터 누적
    }
    
    System.out.println(sb.toString());
    // 파일 내용 + 이전 read 의 잔여 데이터
    // 완전히 잘못된 결과
}

5.4 올바른 for-each 활용

// for-each 를 안전하게 쓰려면
byte[] buf = new byte[8192];
int n = fis.read(buf);

// 1. 정확한 길이의 새 배열
byte[] data = Arrays.copyOf(buf, n);   // 새 배열, 정확한 크기
for (byte b : data) {
    process(b);   // 안전
}

// 2. 또는 일반 for
for (int i = 0; i < n; i++) {
    process(buf[i]);
}

// 3. 또는 Stream API
IntStream.range(0, n).forEach(i -> process(buf[i]));

5.5 for-each 가 적절한 경우

// for-each 가 OK 인 경우:

// 1. 배열 전체가 의미 있을 때
byte[] fullData = Files.readAllBytes(path);
for (byte b : fullData) {   // OK — 모든 데이터
    process(b);
}

// 2. 완전히 채워진 배열
byte[] complete = new byte[1024];
fis.readNBytes(complete, 0, 1024);   // 정확히 채움 (Java 9+)
for (byte b : complete) {
    process(b);   // OK
}

// 3. readAllBytes 결과
byte[] all = is.readAllBytes();
for (byte b : all) {
    process(b);   // OK
}

// 즉:
// - read(byte[]) 후엔 for-each 위험
// - readAllBytes 후엔 for-each OK
// - readNBytes 후엔 OK (정확히 채움)

5.6 코드 리뷰 시 체크

체크리스트:

1. read(byte[]) 호출 후 for-each?
   → ❌ 위험

2. read(byte[]) 호출 후 buf.length 사용?
   → ❌ 위험

3. read(byte[]) 후 n 으로 제한?
   → ✓ 안전

4. readAllBytes 후 처리?
   → ✓ 안전

5. write(buf) 또는 write(buf, 0, buf.length)?
   → ❌ 위험 (마지막 read 후)

6. write(buf, 0, n)?
   → ✓ 안전

5.7 ILIC 의 안전한 패턴

public class SafeByteProcessor {
    
    // ❌ 잘못된 예 (코드 리뷰에서 잡아야)
    public void badProcessFile(Path path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path.toFile())) {
            byte[] buf = new byte[1024];
            while (fis.read(buf) != -1) {
                for (byte b : buf) {   // ❌ 위험
                    process(b);
                }
            }
        }
    }
    
    // ✓ 올바른 예 1
    public void goodProcessFile1(Path path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path.toFile())) {
            byte[] buf = new byte[1024];
            int n;
            while ((n = fis.read(buf)) != -1) {
                for (int i = 0; i < n; i++) {
                    process(buf[i]);
                }
            }
        }
    }
    
    // ✓ 올바른 예 2 (Arrays.copyOf)
    public void goodProcessFile2(Path path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path.toFile())) {
            byte[] buf = new byte[1024];
            int n;
            while ((n = fis.read(buf)) != -1) {
                byte[] chunk = Arrays.copyOf(buf, n);
                for (byte b : chunk) {
                    process(b);
                }
            }
        }
    }
    
    // ✓ 가장 권장 (한 번에 모두)
    public void bestProcessFile(Path path) throws IOException {
        byte[] all = Files.readAllBytes(path);
        for (byte b : all) {   // 안전 (전체 데이터)
            process(b);
        }
    }
    
    private void process(byte b) {
        // 처리
    }
}

5.8 자기 점검 답변

for-each 가 read(byte[]) 후 위험한 이유는?

:
1. 이유:

  • for-each 는 배열 전체 순회
  • 항상 buf.length 만큼
  • n 을 무시
  1. 결과:

    • 마지막 read 에서 이전 데이터 처리
    • 빈 부분 (0) 처리
    • 잘못된 결과
  2. 해결:

    • for (int i = 0; i < n; i++)
    • Arrays.copyOf(buf, n) 으로 새 배열
    • process(buf, 0, n) 패턴
  3. for-each 가 OK 한 경우:

    • readAllBytes 결과
    • readNBytes 후 (정확히 채움)
    • Files.readAllBytes 결과

6️⃣ 버퍼 크기 선택의 기준

6.1 일반적 권장 크기

실무 권장:

매우 작은 파일 (< 1KB):     1KB 또는 직접 readAllBytes
일반 텍스트 파일 (< 1MB):    4KB ~ 8KB
일반 바이너리 (1MB ~ 1GB):  8KB ~ 64KB
대용량 파일 (> 1GB):        64KB ~ 1MB
네트워크 I/O:               4KB ~ 64KB

표준 자바 라이브러리:
  BufferedInputStream: 8KB (DEFAULT_BUFFER_SIZE = 8192)
  ByteArrayOutputStream: 32바이트 (초기), 2배씩 증가

6.2 버퍼 크기의 영향

버퍼 크기와 성능:

1KB 버퍼:
  - 시스템 호출 ↑↑
  - 메모리 ↓
  - 작은 파일에 OK

8KB 버퍼:
  - 시스템 호출 ↓
  - 메모리 적당
  - 가장 일반적 권장 (Sweet spot)

64KB 버퍼:
  - 시스템 호출 ↓↓
  - 메모리 좀 더
  - 큰 파일에 효율

1MB 버퍼:
  - 시스템 호출 최소
  - 메모리 큼
  - 매우 큰 파일만
  - L1/L2 캐시 효과 ↓

6.3 OS 페이지 크기와의 관계

OS 의 페이지 크기:
  - Linux/Mac: 4KB (또는 16KB on M1/M2)
  - Windows: 4KB

디스크 블록 크기:
  - SSD: 4KB 또는 8KB
  - HDD: 512바이트 또는 4KB

권장:
  - 버퍼 크기 = 페이지 크기의 배수
  - 4KB, 8KB, 16KB, 32KB, 64KB
  - 페이지 정렬 시 효율 ↑

6.4 실측 비교

public class BufferSizeBenchmark {
    
    public static void main(String[] args) throws IOException {
        Path file = Path.of("100MB.dat");
        int[] sizes = {1, 1024, 4096, 8192, 16384, 65536, 1048576};
        
        for (int size : sizes) {
            long start = System.nanoTime();
            try (FileInputStream fis = new FileInputStream(file.toFile())) {
                byte[] buf = new byte[size];
                while (fis.read(buf) != -1) { }
            }
            long elapsed = System.nanoTime() - start;
            System.out.printf("Buffer %d B: %d ms%n", size, elapsed / 1_000_000);
        }
    }
}

// 예상 결과 (100MB 파일):
// Buffer 1 B:        ~100,000 ms   (1바이트씩, 매우 느림)
// Buffer 1024 B:     ~200 ms       (1KB)
// Buffer 4096 B:     ~80 ms        (4KB)
// Buffer 8192 B:     ~50 ms        (8KB) ★
// Buffer 16384 B:    ~45 ms        (16KB)
// Buffer 65536 B:    ~40 ms        (64KB)
// Buffer 1048576 B:  ~40 ms        (1MB)
//
// → 8KB 부터 큰 차이 X
// → 8KB ~ 64KB 가 sweet spot

6.5 시나리오별 권장

// 1. 작은 파일 (< 100KB) — 한 번에
byte[] all = Files.readAllBytes(path);

// 2. 일반 텍스트 — 8KB
try (BufferedReader reader = Files.newBufferedReader(path)) {
    // BufferedReader 기본 8KB
}

// 3. 일반 바이너리 — 8KB
try (FileInputStream fis = new FileInputStream(file)) {
    byte[] buf = new byte[8192];
    // ...
}

// 4. 대용량 (영상, ISO 등) — 64KB
try (FileInputStream fis = new FileInputStream(file)) {
    byte[] buf = new byte[65536];
    // ...
}

// 5. 매우 큰 파일 (수 GB) — Memory-mapped
try (FileChannel ch = FileChannel.open(path, READ)) {
    MappedByteBuffer buf = ch.map(MapMode.READ_ONLY, 0, ch.size());
    // ...
}

6.6 사용자 정의 버퍼 풀

// Direct Buffer 풀
public class BufferPool {
    
    private final Queue<ByteBuffer> pool = new ConcurrentLinkedQueue<>();
    private final int bufferSize;
    private final int maxPoolSize;
    
    public BufferPool(int bufferSize, int maxPoolSize) {
        this.bufferSize = bufferSize;
        this.maxPoolSize = maxPoolSize;
    }
    
    public ByteBuffer acquire() {
        ByteBuffer buf = pool.poll();
        return (buf != null) ? buf : ByteBuffer.allocateDirect(bufferSize);
    }
    
    public void release(ByteBuffer buf) {
        buf.clear();
        if (pool.size() < maxPoolSize) {
            pool.offer(buf);
        }
    }
}

// 활용
BufferPool pool = new BufferPool(8192, 100);

ByteBuffer buf = pool.acquire();
try {
    // 사용
} finally {
    pool.release(buf);
}

6.7 자바 표준의 기본값

// BufferedInputStream
public BufferedInputStream(InputStream in) {
    this(in, 8192);   // 8KB 기본
}

// BufferedReader
public BufferedReader(Reader in) {
    this(in, 8192);   // 8KB
}

// PrintWriter
// 내부 BufferedWriter 도 8KB

// Files.newBufferedReader
// 8KB 기본

// 결론: 표준의 sweet spot = 8KB

6.8 ILIC 의 버퍼 크기 설정

public class ShipmentIoConstants {
    
    // 일반 텍스트 파일
    public static final int TEXT_BUFFER_SIZE = 8192;   // 8KB
    
    // 대용량 바이너리 (이미지, PDF 등)
    public static final int BINARY_BUFFER_SIZE = 65536;   // 64KB
    
    // 네트워크 I/O
    public static final int NETWORK_BUFFER_SIZE = 16384;   // 16KB
    
    // 작은 파일
    public static final long SMALL_FILE_THRESHOLD = 100_000;   // 100KB
}

@Service
public class ShipmentFileService {
    
    public byte[] readFile(Path path) throws IOException {
        long size = Files.size(path);
        
        if (size < ShipmentIoConstants.SMALL_FILE_THRESHOLD) {
            // 작은 파일 — 한 번에
            return Files.readAllBytes(path);
        } else {
            // 큰 파일 — 청크 단위
            try (InputStream is = Files.newInputStream(path);
                 ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                
                int bufferSize = (size > 100_000_000) 
                    ? ShipmentIoConstants.BINARY_BUFFER_SIZE 
                    : ShipmentIoConstants.TEXT_BUFFER_SIZE;
                
                byte[] buf = new byte[bufferSize];
                int n;
                while ((n = is.read(buf)) != -1) {
                    baos.write(buf, 0, n);
                }
                
                return baos.toByteArray();
            }
        }
    }
}

6.9 자기 점검 답변

버퍼 크기 선택의 기준은?

:
1. 일반 권장:

  • 작은 파일 (< 100KB): readAllBytes
  • 텍스트: 4~8KB
  • 바이너리: 8~64KB
  • 대용량 (> 1GB): 64KB ~ 1MB
  1. OS 페이지 크기:

    • 4KB 의 배수 권장
    • 페이지 정렬 효율
  2. 자바 표준 기본:

    • BufferedInputStream: 8KB
    • BufferedReader: 8KB
    • 8KB = sweet spot
  3. 실측:

    • 1KB 부터 큰 개선
    • 8KB 부터 효과 평탄화
    • 64KB 이상은 미미
  4. 선택 기준:

    • 파일 크기
    • 메모리 여유
    • 자주 호출되는지

7️⃣ read 가 buf.length 안 채우는 이유

7.1 일반적인 오해

오해:
  "read(buf) 는 buf.length 만큼 읽는다"

실제:
  "read(buf) 는 0 ~ buf.length 만큼 읽을 수 있다"
  
즉:
  - 최대 buf.length
  - 더 작을 수도
  - 정확히 buf.length 아닐 수 있음

7.2 buf.length 안 채우는 4가지 이유

1. 파일 끝 근접
   - 남은 데이터가 buf.length 보다 적음
   - 마지막 읽기에서 발생

2. 네트워크 패킷
   - TCP/IP 는 패킷 단위
   - 한 패킷 도착 후 즉시 리턴
   - 더 기다리지 않음

3. 디스크 블록
   - 한 번에 한 블록만 처리
   - 사용자 요청 = 가능한 만큼만

4. 사용자 interrupt
   - 인터럽트 신호로 중단 가능
   - InterruptedIOException

7.3 시나리오별 동작

// 시나리오 1: 파일 끝
// 파일 크기 100바이트, 버퍼 200바이트
try (FileInputStream fis = new FileInputStream("100bytes.dat")) {
    byte[] buf = new byte[200];
    int n = fis.read(buf);
    // n = 100 (파일 끝)
}

// 시나리오 2: 네트워크
try (Socket socket = new Socket("example.com", 80);
     InputStream is = socket.getInputStream()) {
    byte[] buf = new byte[8192];
    int n = is.read(buf);
    // n = 패킷 크기 (보통 1500 이하)
    // 또는 일부만 도착한 만큼
}

// 시나리오 3: 표준 입력
byte[] buf = new byte[1024];
int n = System.in.read(buf);
// n = 사용자가 Enter 누른 줄의 길이

7.4 정확히 채우려면

// 정확히 buf.length 만큼 읽기

// 방법 1: 직접 loop
public static int readFully(InputStream is, byte[] buf, int off, int len) 
        throws IOException {
    int totalRead = 0;
    while (totalRead < len) {
        int n = is.read(buf, off + totalRead, len - totalRead);
        if (n == -1) return totalRead;   // EOF
        totalRead += n;
    }
    return totalRead;
}

// 방법 2: DataInputStream.readFully
DataInputStream dis = new DataInputStream(fis);
byte[] buf = new byte[100];
dis.readFully(buf);
// 정확히 100, 부족하면 EOFException

// 방법 3: Java 9+ readNBytes
int n = is.readNBytes(buf, 0, 100);
// 정확히 100 (또는 EOF 까지)

7.5 부분 읽기 처리

// 정확한 크기가 중요한 경우 — 헤더 등

// ❌ 단순 read
int n = is.read(headerBuf);
// n < headerBuf.length 가능 → 헤더 불완전

// ✓ readFully
DataInputStream dis = new DataInputStream(is);
byte[] headerBuf = new byte[16];
dis.readFully(headerBuf);
// 정확히 16바이트 보장

// ✓ readNBytes (Java 9+)
byte[] headerBuf = is.readNBytes(16);
// 정확히 16바이트 (또는 EOF 면 더 작음)

7.6 부분 읽기를 활용한 패턴

// 데이터를 점진적으로 읽기 — 좋은 패턴
public byte[] readWithProgress(InputStream is, long totalSize, ProgressListener listener) 
        throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buf = new byte[8192];
    long readBytes = 0;
    int n;
    
    while ((n = is.read(buf)) != -1) {
        baos.write(buf, 0, n);
        readBytes += n;
        listener.onProgress(readBytes, totalSize);   // 진행률 통보
    }
    
    return baos.toByteArray();
}

// 네트워크에서 부분 읽기 활용
// - 데이터 일부 도착 → 즉시 처리
// - 모든 데이터 기다리지 않음
// - 응답성 ↑

7.7 ILIC 의 활용

public class ShipmentDataReader {
    
    // 헤더 정확히 읽기
    public ShipmentHeader readHeader(InputStream is) throws IOException {
        byte[] buf = new byte[ShipmentHeader.HEADER_SIZE];
        int n = is.readNBytes(buf, 0, buf.length);   // Java 9+
        
        if (n != ShipmentHeader.HEADER_SIZE) {
            throw new EOFException("Header incomplete: " + n);
        }
        
        return ShipmentHeader.parse(buf);
    }
    
    // 진행률 표시 다운로드
    public byte[] downloadWithProgress(URL url) throws IOException {
        try (InputStream is = url.openStream();
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            
            byte[] buf = new byte[8192];
            int n;
            long total = 0;
            
            while ((n = is.read(buf)) != -1) {
                baos.write(buf, 0, n);
                total += n;
                
                if (total % (1024 * 1024) < 8192) {
                    log.info("Downloaded: {} MB", total / (1024 * 1024));
                }
            }
            
            return baos.toByteArray();
        }
    }
    
    // 청크 단위 처리 — 메모리 효율
    public void processInChunks(InputStream is, int chunkSize) throws IOException {
        byte[] buf = new byte[chunkSize];
        int n;
        while ((n = is.read(buf)) != -1) {
            processChunk(buf, 0, n);
        }
    }
}

7.8 자기 점검 답변

read 가 buf.length 안 채우는 4가지 이유는?

:
1. 파일 끝 근접: 남은 데이터 < buf.length
2. 네트워크 패킷: 한 패킷 도착 후 즉시 리턴
3. 디스크 블록: 한 번에 한 블록
4. 인터럽트: 사용자 interrupt 신호

정확히 읽으려면:

  • DataInputStream.readFully
  • Java 9+ readNBytes
  • 직접 loop

부분 읽기의 활용:

  • 점진적 처리
  • 진행률 표시
  • 응답성 향상

8️⃣ readNBytes, transferTo (Java 9+)

8.1 readAllBytes — 모두 한 번에

// Java 9+
public byte[] readAllBytes() throws IOException;

// 사용
try (InputStream is = Files.newInputStream(path)) {
    byte[] all = is.readAllBytes();
    // 끝까지 모두
}

// 또는 Files
byte[] all = Files.readAllBytes(path);

// 주의:
// - 작은 파일에만 (메모리 한도)
// - 큰 파일은 OutOfMemoryError 위험
// - Integer.MAX_VALUE 제한 (~2GB)

8.2 readNBytes — 정확한 크기

// Java 9+
public byte[] readNBytes(int len) throws IOException;
public int readNBytes(byte[] b, int off, int len) throws IOException;

// 사용
byte[] data = is.readNBytes(100);
// 정확히 100 바이트 (또는 EOF 까지)
// 부족하면 더 작은 배열

// 또는 기존 배열에
byte[] buf = new byte[100];
int n = is.readNBytes(buf, 0, 100);
// n = 실제 읽은 수 (100 또는 그 이하)

// readFully 와 비교:
// - readFully: 부족하면 EOFException
// - readNBytes: 부족해도 OK, 작은 배열 반환

8.3 transferTo — 다른 OutputStream 으로 전송

// Java 9+
public long transferTo(OutputStream out) throws IOException;

// 사용 — 가장 간단한 복사
try (InputStream is = Files.newInputStream(src);
     OutputStream os = Files.newOutputStream(dest)) {
    long n = is.transferTo(os);
    // n = 복사된 바이트 수
}

// 내부:
// - 적절한 버퍼 사용
// - 자동 최적화
// - 사용자 코드 단순

8.4 transferTo 의 내부

// 기본 구현 (InputStream)
public long transferTo(OutputStream out) throws IOException {
    Objects.requireNonNull(out);
    long transferred = 0;
    byte[] buffer = new byte[8192];   // 8KB
    int read;
    while ((read = this.read(buffer, 0, 8192)) >= 0) {
        out.write(buffer, 0, read);
        transferred += read;
    }
    return transferred;
}

// FileInputStream 의 transferTo 는 더 최적화
// - 같은 파일 시스템: 빠른 복사
// - zero-copy 가능 (OS 지원 시)

8.5 transferTo 활용

// 1. 파일 복사
try (InputStream src = Files.newInputStream(srcPath);
     OutputStream dest = Files.newOutputStream(destPath)) {
    src.transferTo(dest);
}

// 또는 더 간단히 (Files)
Files.copy(srcPath, destPath);

// 2. HTTP 응답 → 파일
try (InputStream is = url.openStream();
     OutputStream os = Files.newOutputStream(path)) {
    is.transferTo(os);
}

// 3. JAR 리소스 → 파일
try (InputStream is = MyClass.class.getResourceAsStream("/data.txt");
     OutputStream os = Files.newOutputStream(extractPath)) {
    is.transferTo(os);
}

// 4. 메모리 → 파일
byte[] data = ...;
try (InputStream is = new ByteArrayInputStream(data);
     OutputStream os = Files.newOutputStream(path)) {
    is.transferTo(os);
}

8.6 Java 9+ 메서드 종합

메서드동작활용
readAllBytes()모두 한 번에작은 파일
readNBytes(int)n 바이트 (또는 EOF까지)정확한 크기
readNBytes(buf, off, len)기존 배열에 정확히헤더 등
transferTo(OutputStream)다른 스트림으로복사, 전송
skip(long) (개선)더 정확 (Java 12+ skipNBytes)위치 이동

8.7 ILIC 의 활용

public class ModernFileService {
    
    // 1. 작은 파일 — readAllBytes
    public byte[] readSmall(Path path) throws IOException {
        if (Files.size(path) > 100_000_000) {
            throw new IOException("File too large for readAllBytes");
        }
        return Files.readAllBytes(path);
    }
    
    // 2. 정확한 크기 — readNBytes
    public byte[] readExact(InputStream is, int size) throws IOException {
        byte[] data = is.readNBytes(size);
        if (data.length != size) {
            throw new EOFException("Expected " + size + " bytes, got " + data.length);
        }
        return data;
    }
    
    // 3. 복사 — transferTo
    public void copy(Path src, Path dest) throws IOException {
        try (InputStream is = Files.newInputStream(src);
             OutputStream os = Files.newOutputStream(dest, 
                 StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            long copied = is.transferTo(os);
            log.info("Copied {} bytes", copied);
        }
    }
    
    // 4. 다운로드
    public void download(String urlStr, Path dest) throws IOException {
        URL url = new URL(urlStr);
        try (InputStream is = url.openStream();
             OutputStream os = Files.newOutputStream(dest)) {
            long n = is.transferTo(os);
            log.info("Downloaded {} bytes from {}", n, urlStr);
        }
    }
    
    // 5. 리소스 추출
    public void extractResource(String resourceName, Path dest) throws IOException {
        try (InputStream is = getClass().getResourceAsStream(resourceName)) {
            if (is == null) throw new FileNotFoundException(resourceName);
            
            Files.createDirectories(dest.getParent());
            try (OutputStream os = Files.newOutputStream(dest)) {
                is.transferTo(os);
            }
        }
    }
}

8.8 자기 점검 답변

Java 9+ 의 새 메서드는?

:
1. readAllBytes():

  • 모두 한 번에
  • 작은 파일만
  • Integer.MAX_VALUE 제한
  1. readNBytes(int):

    • n 바이트 정확히
    • EOF 까지 일부만 OK
  2. readNBytes(buf, off, len):

    • 기존 배열에 정확히
    • 부분 채우기
  3. transferTo(OutputStream):

    • 다른 스트림으로 직접
    • 내부 8KB 버퍼
    • 복사 간소화
  4. 활용:

    • 파일 복사: src.transferTo(dest)
    • 다운로드: url.openStream().transferTo(file)
    • 리소스 추출

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
read(byte[]) 반환?실제 읽은 수 (1~buf.length), -1 EOF
1바이트 vs byte[] 성능?system call 수 차이, 8KB ~ 2000배
마지막 read 함정?n < buf.length, 이전 데이터 남음
for-each 위험?buf.length 전체 순회, n 무시
권장 버퍼 크기?8KB (자바 표준 sweet spot)
OS 페이지 크기?4KB (보통), 8KB 배수 권장
read 가 buf.length 안 채우는 이유?파일 끝/네트워크/블록/인터럽트
readFully?DataInputStream, 정확히 채움
readNBytes (Java 9+)?정확한 크기, EOF 까지
transferTo (Java 9+)?다른 스트림으로 직접 전송
BufferedInputStream vs byte[]?둘 다 효율, 결합 OK

9.2 자기 점검 체크리스트

read(byte[])

  • 시그니처와 반환값
  • read(byte[], off, len)
  • 데이터 위치 (0~n-1)

성능

  • 1바이트 vs byte[] 차이
  • system call 수
  • 실제 시간 측정

함정

  • 마지막 read 의 n < buf.length
  • for-each 의 위험
  • buf.length 사용 X

안전 패턴

  • for (int i = 0; i < n; i++)
  • process(buf, 0, n)
  • new String(buf, 0, n, charset)
  • write(buf, 0, n)

버퍼 크기

  • 시나리오별 권장 (1KB ~ 1MB)
  • OS 페이지 크기
  • 8KB sweet spot

Java 9+

  • readAllBytes
  • readNBytes
  • transferTo

9.3 추가 심화 질문

Q1: read(byte[]) 가 0 을 반환할 수 있나?

답:

  • 기본적으로 X (Blocking 모드)
  • buf.length == 0 일 때만 0
  • Non-blocking 채널에선 가능
  • 사용자 정의 InputStream 은 가능 (드물게)
byte[] buf = new byte[0];
int n = is.read(buf);   // 0 가능

Q2: 매우 큰 버퍼 (1GB 등) 의 문제?

답:

  • 메모리 부담
  • L1/L2 캐시 효율 ↓
  • 한 번에 처리 시간 ↑
  • 진행률 표시 어려움
  • 권장: 64KB 이하 + 반복 호출

Q3: read 의 Blocking 시점?

답:

  • 일반 파일: 거의 즉시
  • 네트워크: 데이터 도착까지
  • 파이프: 다른 쪽에서 쓸 때까지
  • /dev/random: 엔트로피 부족 시
  • Blocking 동안 스레드 정지

Q4: ByteBuffer 와 byte[] 차이?

답:

  • byte[]: java.io 의 기본, 단순
  • ByteBuffer: java.nio, position/limit 관리, Direct Buffer 가능
  • 성능: 비슷
  • 활용: Channel 은 ByteBuffer 필수

Q5: 마지막 read 가 n=0 일 수도?

답:

  • 일반적으로 X
  • read 가 0 반환 = 더 읽을 데이터 없지만 EOF 아님 (Non-blocking)
  • Blocking 모드에선 보통:
    • 데이터 있으면 양수
    • EOF 면 -1
  • 0 은 buf.length=0 또는 Non-blocking 만

🎯 핵심 요약 — 3줄 정리

1. read(byte[]) 의 본질

  • 최대 buf.length, 실제는 반환값 n
  • buf[0] ~ buf[n-1] 만 유효
  • 1바이트 read 보다 수천 배 빠름

2. 마지막 읽기 함정

  • 마지막 read 에서 n < buf.length
  • buf[n] ~ buf[buf.length-1] 은 이전 데이터
  • for-each, buf.length 사용 X
  • process(buf, 0, n) 패턴 필수

3. Java 9+ 권장

  • transferTo: 복사 한 줄
  • readNBytes: 정확한 크기
  • 권장 버퍼 8KB ~ 64KB

📚 다음으로...

Unit 8.4 — FileOutputStream

이번 Unit에서 byte[] 효율 읽기를 봤다면, 다음은 파일 쓰기.

  • FileOutputStream 의 정의
  • write 메서드
  • 이어쓰기 모드 (append)
  • 바이트가 문자로 보이는 이유

Phase 8 진행 상황

🚀 Phase 8 — Stream 실전
  ✅ Unit 8.1 System.in (한글 안 되는 이유)
  ✅ Unit 8.2 FileInputStream
  ✅ Unit 8.3 byte[] 배열로 효율적 읽기 ← 여기
  ⏭ Unit 8.4 FileOutputStream
  ⏭ Unit 8.5 한글 처리 (FileReader, InputStreamReader)
  ⏭ Unit 8.6 FileWriter (한글 쓰기)

3주차 누적 진행

✅ Phase 1 ~ 7 완주 (31 Unit)
🚀 Phase 8 — Stream 실전 (3/6 진행)

총: 34/43 Unit (약 79%)
profile
Software Developer

0개의 댓글