3주차 Unit 9.2 — BufferedInputStream / BufferedOutputStream

Psj·2026년 5월 20일

F-lab

목록 보기
113/197

Unit 9.2 — BufferedInputStream / BufferedOutputStream

F-LAB JAVA · 3주차 · Phase 9 · I/O 강화


📌 학습 목표

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

  • BufferedInputStream 의 정의와 내부 8KB 버퍼는?
  • BufferedOutputStream 의 정의와 flush 의 정확한 동작은?
  • Decorator 패턴 이 어떻게 적용되었나?
  • 일반 Stream vs Buffered Stream 의 성능 차이는?
  • buf, count, pos, markpos 4가지 내부 상태는?
  • BufferedInputStream 의 mark/reset 동작은?
  • 버퍼링이 동작하는 정확한 메커니즘 은?
  • flush() 의 두 가지 효과 는?
  • 언제 명시적 flush 가 필요한가?
  • BufferedReader/Writer 와의 차이 는?

🎯 핵심 한 문장

BufferedInputStreamBufferedOutputStream 은 내부에 8KB 버퍼 (byte[8192]) 를 가지며, 매 read/write 가 OS 시스템 호출을 하지 않고 메모리 접근으로 처리되어 수십~수백 배 빠르다.
Decorator 패턴 의 대표적 예 — 기존 InputStream/OutputStream 을 감싸 (Wrap) 버퍼링 기능만 추가, 인터페이스는 동일.
read 는 버퍼 비면 OS 에서 8KB 한 번에 채우고, write 는 버퍼 가득 차거나 flush() / close() 시 OS 로 한 번에 전송.
명시적 flush 가 필요한 경우: 진행 중 즉시 출력 (로그/트랜잭션), 비정상 종료 대비, 다른 프로세스가 즉시 읽어야 할 때 — 일반적으론 close 가 자동 flush 하므로 try-with-resources 만 잘 쓰면 충분.
Reader/Writer 의 BufferedReader/BufferedWriter 와 동일한 메커니즘 (8KB), 단위만 byte ↔ char.

비유 — 우편함

일반 OutputStream:
  편지 1통 쓰고 → 바로 우체국 (system call)
  매번 우체국 방문
  매우 느림

BufferedOutputStream:
  편지 1통 쓰고 → 우편함 (버퍼) 에 넣기
  우편함 가득 차면 → 우체국 한 번에
  메모리 접근만 (빠름)
  수십~수백 배 효율

flush():
  우편함 안 가득 차도 우체국 바로
  - 긴급 우편
  - 닫기 전 마지막

close():
  우체국 가고 우편함 닫기
  자동 flush

→ Buffered* = 우편함 + 한 번에 우체국.


🧭 9개 섹션 로드맵

1. BufferedInputStream 의 정의와 구조
2. BufferedInputStream 의 동작 메커니즘
3. BufferedOutputStream 의 정의와 구조
4. BufferedOutputStream 의 flush 정밀
5. Decorator 패턴의 정수
6. 일반 vs Buffered 성능 비교
7. mark/reset 의 동작
8. 실무 활용과 함정
9. 면접 + 자기 점검

1️⃣ BufferedInputStream 의 정의와 구조

1.1 클래스 정의

package java.io;

public class BufferedInputStream extends FilterInputStream {
    
    private static final int DEFAULT_BUFFER_SIZE = 8192;
    
    protected volatile byte[] buf;     // 내부 버퍼
    protected int count;                // 버퍼에 들어있는 유효 바이트 수
    protected int pos;                  // 다음 읽기 위치
    protected int markpos = -1;         // mark 위치
    protected int marklimit;             // mark 의 한계
    
    // 생성자
    public BufferedInputStream(InputStream in);
    public BufferedInputStream(InputStream in, int size);
}

핵심:

  • FilterInputStream 의 자식 (Decorator)
  • 내부 8KB 버퍼 (기본값)
  • 다른 InputStream 을 감쌈
  • Java 1.0 부터

1.2 FilterInputStream 의 위치

InputStream (추상)
  ├── FileInputStream
  ├── ByteArrayInputStream
  ├── FilterInputStream         ← Decorator 부모
  │   ├── BufferedInputStream   ← 여기
  │   ├── DataInputStream
  │   ├── PushbackInputStream
  │   └── 기타
  ├── ObjectInputStream
  └── ...

1.3 생성자

// 기본 8KB
BufferedInputStream bis = new BufferedInputStream(fis);

// 사용자 정의 크기
BufferedInputStream bis2 = new BufferedInputStream(fis, 16384);   // 16KB

// 일반적 사용
try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    // 처리
}

// 또는 한 줄
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("file.txt"))) {
    // 처리
}

1.4 내부 상태의 의미

4가지 내부 상태:

buf: byte[8192]
  - 실제 데이터를 담는 버퍼

count: 버퍼 내 유효 데이터 수
  - 0 ≤ count ≤ buf.length
  - 0: 버퍼 비어있음
  - count: 마지막 유효 바이트 + 1

pos: 다음 읽기 위치
  - 0 ≤ pos ≤ count
  - pos == count: 버퍼 다 읽음

markpos: mark 호출 위치
  - -1: mark 안 됨
  - 그 외: 표시된 위치

marklimit: mark 후 유효 거리
  - 너무 멀리 가면 무효화

1.5 시각화

초기 상태:
  buf  = [_, _, _, _, _, _, _, _, ...]
  count = 0
  pos   = 0
  
첫 read() 호출 시:
  - 버퍼 비어있으므로 OS 에서 한 번에 채움
  - OS 가 8KB 읽어옴 (또는 가능한 만큼)
  
  buf  = [H, e, l, l, o, _, _, _, ...]
  count = 5   (실제로는 8192 까지 가능)
  pos   = 0

read() 반환:
  - buf[pos] 반환
  - pos++
  
  buf  = [H, e, l, l, o, _, _, _, ...]
  count = 5
  pos   = 1   ← H 읽음

5번 더 read 후:
  pos = 5 = count
  → 다음 read 는 다시 OS 호출 (버퍼 채움)

1.6 read() 의 동작

public int read() throws IOException {
    if (pos >= count) {
        fill();   // 버퍼 채움
        if (pos >= count)
            return -1;   // EOF
    }
    return getBufIfOpen()[pos++] & 0xff;
    // pos 의 바이트 반환 + pos 증가
}

private void fill() throws IOException {
    byte[] buffer = getBufIfOpen();
    // ... mark 처리 ...
    pos = 0;
    count = pos;
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    // OS 에서 한 번에 buffer.length 만큼
    if (n > 0)
        count = n + pos;
}

핵심:

  • 버퍼에 데이터 있으면 메모리 접근만
  • 버퍼 비면 OS 에서 한 번에 8KB
  • 결과: system call 횟수 1/8000 ~ 1/65000

1.7 BufferedInputStream 의 주요 메서드

// 상속 (InputStream)
public int read();
public int read(byte[] b, int off, int len);

// 추가
public synchronized int available();
public synchronized long skip(long n);
public synchronized void mark(int readlimit);
public synchronized void reset();
public boolean markSupported();   // true
public void close();

특징:

  • 대부분 synchronized
  • mark/reset 지원 (FileInputStream X)

1.8 ILIC 활용

public class ShipmentDataReader {
    
    // 일반 패턴
    public byte[] readAll(Path path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path.toFile());
             BufferedInputStream bis = new BufferedInputStream(fis);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            
            byte[] buf = new byte[8192];
            int n;
            while ((n = bis.read(buf)) != -1) {
                baos.write(buf, 0, n);
            }
            
            return baos.toByteArray();
        }
    }
    
    // 더 큰 버퍼 (대용량 파일)
    public byte[] readLargeFile(Path path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path.toFile());
             BufferedInputStream bis = new BufferedInputStream(fis, 65536);   // 64KB
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            
            byte[] buf = new byte[8192];
            int n;
            while ((n = bis.read(buf)) != -1) {
                baos.write(buf, 0, n);
            }
            
            return baos.toByteArray();
        }
    }
}

1.9 자기 점검 답변

BufferedInputStream 의 정의와 내부 구조는?

:
1. 정의:

  • FilterInputStream 의 자식
  • Decorator 패턴
  • 다른 InputStream 을 감쌈
  1. 내부 상태 4가지:

    • buf: byte[8192]
    • count: 유효 데이터 수
    • pos: 다음 읽기 위치
    • markpos: mark 위치
  2. 버퍼 크기:

    • 기본 8KB (8192)
    • 생성자로 변경 가능
  3. 효과:

    • 버퍼에서 메모리 접근
    • OS 호출 1/8000
    • 매우 빠름

2️⃣ BufferedInputStream 의 동작 메커니즘

2.1 한 번의 OS 호출

사용자 코드:
  while ((b = bis.read()) != -1) {
      // 1바이트씩 처리
  }

내부 동작:
  
  첫 호출:
    - pos = 0, count = 0
    - 버퍼 비어있음 → fill() 호출
    - OS 에 read(buf, 0, 8192) 시스템 호출
    - 8192 바이트 (또는 가능한 만큼) 가져옴
    - count = 8192, pos = 0
    - buf[pos++] 반환
  
  2 ~ 8192 호출:
    - pos < count → 메모리에서 buf[pos++] 반환
    - system call 없음!
  
  8193 호출:
    - pos == count → fill() 다시
    - OS 호출 2번째
  
1MB 파일:
  - 일반: system call 100만 번
  - Buffered: system call 128 번 (1MB / 8KB)
  - 약 8000배 절감

2.2 read(byte[]) 의 동작

// read(byte[]) 는 더 복잡
public synchronized int read(byte b[], int off, int len) throws IOException {
    
    int avail = count - pos;
    
    if (avail <= 0) {
        // 버퍼 비어있음
        // 큰 요청이면 직접 OS 호출 (버퍼 우회)
        if (len >= getBufIfOpen().length && markpos < 0) {
            return getInIfOpen().read(b, off, len);
            // 직접 OS 에서 b 로
        }
        // 작은 요청이면 버퍼 채우고 복사
        fill();
        avail = count - pos;
        if (avail <= 0) return -1;
    }
    
    // 버퍼에서 복사
    int cnt = (avail < len) ? avail : len;
    System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
    pos += cnt;
    return cnt;
}

핵심:

  • 큰 요청은 버퍼 우회 (성능 ↑)
  • 작은 요청은 버퍼 활용

2.3 read 의 시각화

시나리오: 파일 20KB, BufferedInputStream 8KB 버퍼

read() 1바이트씩:
  call 1:     fill (OS 호출, 8KB), 반환 1
  call 2:     메모리 반환 1
  ...
  call 8192:  메모리 반환 1
  call 8193:  fill (OS 호출 2번째), 반환 1
  ...
  call 16384: 메모리 반환 1
  call 16385: fill (OS 호출 3번째, 마지막 4KB)
  ...
  call 20480: 메모리 반환 1
  call 20481: fill → 0 → -1 (EOF)
  
총: OS 호출 3번 (1MB 파일은 약 128번)
20KB 메모리 접근

read(byte[1024]) 호출:
  call 1: fill (OS 호출), 1024 반환
  call 2 ~ 8:  메모리 1024 반환
  call 9: fill, 1024 반환
  ...
  total: OS 호출 3번, 메모리 복사
  
read(byte[16384]) 호출 (요청 > 버퍼):
  call 1: 버퍼 우회, 직접 OS 호출
  ...
  더 효율적

2.4 버퍼 채우기 시점

버퍼 채워지는 조건:

1. 최초 read 호출 시
   - pos = 0, count = 0
   - fill 호출

2. 버퍼 다 읽었을 때
   - pos == count
   - fill 호출

3. mark/reset 처리 시
   - mark 위치 보존 위해 추가 처리

채워지는 양:
  - 보통 buf.length (8192)
  - 또는 가능한 만큼
  - 네트워크는 패킷 단위

2.5 close 의 동작

public void close() throws IOException {
    byte[] buffer;
    while ((buffer = buf) != null) {
        if (bufUpdater.compareAndSet(this, buffer, null)) {
            InputStream input = in;
            in = null;
            if (input != null) input.close();
            return;
        }
    }
}

// 동작:
// 1. 버퍼 해제 (atomic)
// 2. 감싼 InputStream 도 close
// 3. cascading close

2.6 ILIC 의 활용

public class ShipmentDataProcessor {
    
    // 1바이트씩 처리 — Buffered 가 필수
    public long countBytes(Path path) throws IOException {
        long count = 0;
        try (FileInputStream fis = new FileInputStream(path.toFile());
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            
            while (bis.read() != -1) {
                count++;
                // 매 read 가 메모리 접근만
                // 매우 빠름
            }
        }
        return count;
    }
    
    // 작은 청크 — 버퍼 활용
    public List<ShipmentRecord> readRecords(Path path) throws IOException {
        List<ShipmentRecord> records = new ArrayList<>();
        
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(path.toFile()))) {
            
            // 각 레코드 헤더 32바이트
            byte[] header = new byte[32];
            while (bis.read(header) == 32) {
                int bodyLen = parseLength(header);
                byte[] body = new byte[bodyLen];
                if (bis.read(body) == bodyLen) {
                    records.add(parseRecord(header, body));
                }
            }
        }
        
        return records;
    }
}

2.7 자기 점검 답변

BufferedInputStream 의 동작 메커니즘은?

:
1. 첫 read:

  • 버퍼 비어있음
  • OS 에서 8KB 한 번에 (fill)
  • buf 채움
  1. 이후 read:

    • pos < count → 메모리 접근
    • pos == count → 다시 fill
  2. OS 호출 횟수:

    • 1MB 파일
    • 일반: 100만 번
    • Buffered: 128번 (8KB ÷ 1MB)
    • 약 8000배
  3. read(byte[]) 최적화:

    • 큰 요청 (≥ buf.length) → 직접 OS
    • 작은 요청 → 버퍼 활용
  4. close:

    • 버퍼 해제
    • 감싼 InputStream 도 close

3️⃣ BufferedOutputStream 의 정의와 구조

3.1 클래스 정의

package java.io;

public class BufferedOutputStream extends FilterOutputStream {
    
    private static final int DEFAULT_BUFFER_SIZE = 8192;
    
    protected byte[] buf;       // 내부 버퍼
    protected int count;         // 현재까지 쓴 바이트 수
    
    // 생성자
    public BufferedOutputStream(OutputStream out);
    public BufferedOutputStream(OutputStream out, int size);
}

핵심:

  • FilterOutputStream 의 자식
  • 내부 8KB 버퍼
  • 다른 OutputStream 을 감쌈
  • Java 1.0 부터

3.2 내부 상태

2가지 상태:

buf: byte[8192]
  - 데이터를 임시 보관

count: 현재까지 쓴 바이트 수
  - 0 ≤ count ≤ buf.length
  - count == buf.length → 가득, flush
  - count == 0 → 비어있음

3.3 write 의 동작

public synchronized void write(int b) throws IOException {
    if (count >= buf.length) {
        flushBuffer();   // 버퍼 가득 → flush
    }
    buf[count++] = (byte) b;   // 버퍼에 쓰기
}

public synchronized void write(byte b[], int off, int len) throws IOException {
    if (len >= buf.length) {
        // 큰 데이터 — 버퍼 우회
        flushBuffer();
        out.write(b, off, len);
        return;
    }
    if (len > buf.length - count) {
        // 버퍼에 안 들어감
        flushBuffer();
    }
    System.arraycopy(b, off, buf, count, len);
    count += len;
}

private void flushBuffer() throws IOException {
    if (count > 0) {
        out.write(buf, 0, count);
        count = 0;
    }
}

3.4 시각화

시나리오: 5바이트 쓰기

초기:
  buf   = [_, _, _, _, _, _, _, _, ...]
  count = 0

write('H'):
  buf   = [H, _, _, _, _, _, _, _, ...]
  count = 1
  
write('e'):
  buf   = [H, e, _, _, _, _, _, _, ...]
  count = 2

write('l'), write('l'), write('o'):
  buf   = [H, e, l, l, o, _, _, _, ...]
  count = 5

★ 아직 디스크 X
  - 버퍼에만 있음
  - OS/디스크 아직 모름

flush() 또는 close():
  - flushBuffer 호출
  - out.write(buf, 0, count)
  - OS 로 5바이트
  - count = 0

3.5 버퍼 가득 차는 시점

시나리오: 10KB 쓰기 (버퍼 8KB)

write(byte[10240]):
  - len (10240) > buf.length (8192)
  - 큰 요청 → 버퍼 우회
  - flushBuffer (현재 버퍼 비움)
  - out.write(b, off, len) 직접

write(byte[4096]) × 3:
  call 1: 버퍼에 4KB, count = 4096
  call 2: 4KB 더 → count = 8192 (가득)
  call 3: 새로 쓰기 전 flushBuffer
        → 다시 4KB, count = 4096

write(byte[10000]):
  - 큰 요청, 버퍼 우회
  - 직접 OS 로 10KB

3.6 close 의 자동 flush

public void close() throws IOException {
    try {
        flush();
        // ★ 자동 flush
    } finally {
        out.close();
        // cascading close
    }
}

// 효과:
// try-with-resources 만 잘 쓰면
// 명시적 flush 불필요
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    bos.write(data);
    // try 종료 → close → 자동 flush
}
// data 가 디스크에 안전하게

3.7 ILIC 활용

public class ShipmentDataWriter {
    
    // 일반 쓰기
    public void writeData(Path path, byte[] data) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(path.toFile());
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            bos.write(data);
            // close 시 자동 flush
        }
    }
    
    // 점진적 쓰기 (많은 작은 write)
    public void writeRecords(Path path, List<byte[]> records) throws IOException {
        try (BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream(path.toFile()))) {
            for (byte[] record : records) {
                bos.write(record);
                // 작은 write 들이 버퍼에 누적
                // 버퍼 가득 시 OS 로
            }
        }
        // 자동 flush
    }
    
    // 큰 버퍼 (대용량)
    public void writeLargeFile(Path path, byte[] data) throws IOException {
        try (BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream(path.toFile()), 65536)) {   // 64KB
            bos.write(data);
        }
    }
}

3.8 자기 점검 답변

BufferedOutputStream 의 정의와 구조는?

:
1. 정의:

  • FilterOutputStream 의 자식
  • Decorator
  • 8KB 버퍼
  1. 내부 상태 2가지:

    • buf: byte[8192]
    • count: 쓴 바이트 수
  2. write 동작:

    • 버퍼에 누적
    • 가득 시 자동 flush
    • 큰 요청은 버퍼 우회
  3. close:

    • 자동 flush
    • try-with-resources 권장

4️⃣ BufferedOutputStream 의 flush 정밀

4.1 flush 의 정의

public synchronized void flush() throws IOException {
    flushBuffer();
    out.flush();
}

private void flushBuffer() throws IOException {
    if (count > 0) {
        out.write(buf, 0, count);
        count = 0;
    }
}

핵심:

  • 버퍼 → OS 로 쓰기
  • 감싼 OutputStream 도 flush
  • count = 0 으로 리셋

4.2 flush 의 두 가지 효과

효과 1: 버퍼 → OS
  - BufferedOutputStream 의 buf 안의 데이터
  - 감싼 FileOutputStream 으로
  - OS 의 page cache 까지

효과 2: 감싼 stream 도 flush
  - 체인된 stream 의 모든 버퍼 비움
  - 예: GZIPOutputStream → BufferedOutputStream → FileOutputStream
  - 모두 비움

주의:
  - flush 는 OS 까지만
  - OS → 디스크는 FileChannel.force 필요

4.3 flush 안 하면?

// ❌ flush 누락
BufferedOutputStream bos = new BufferedOutputStream(
    new FileOutputStream("file.txt"));
bos.write(importantData);
// 아직 디스크에 X (8KB 버퍼에만)

System.exit(0);   // ★ 강제 종료
// 데이터 손실!

// ✓ try-with-resources
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    bos.write(importantData);
}
// close → 자동 flush
// 안전

// ✓ 명시적 flush
BufferedOutputStream bos = new BufferedOutputStream(...);
bos.write(data);
bos.flush();   // 즉시 OS 로

4.4 flush 가 필요한 시점

명시적 flush 가 필요한 경우:

1. 진행 중 즉시 출력
   - 실시간 로그
   - 진행률 표시
   - 디버깅

2. 비정상 종료 대비
   - 트랜잭션 로그
   - 중요 데이터

3. 다른 프로세스가 읽어야 할 때
   - 파일 모니터링
   - 다른 도구가 쓰는 동시에 읽기

4. 네트워크 (스트림)
   - 응답 즉시 전송
   - 클라이언트가 대기 중

5. 인터랙티브 도구
   - 사용자 입력 후 응답
   - 즉시 보여야

4.5 자동 flush vs 명시적 flush

// 자동 (close → flush)
try (BufferedOutputStream bos = ...) {
    bos.write(data);
}
// 일반적 — 가장 권장

// 명시적 — 진행 중
try (BufferedOutputStream bos = ...) {
    for (int i = 0; i < 1000; i++) {
        bos.write(data[i]);
        if (i % 100 == 0) {
            bos.flush();   // 100마다
        }
    }
}

// 명시적 — 비정상 종료 대비
try (BufferedOutputStream bos = ...) {
    bos.write(criticalData);
    bos.flush();
    // 이후 작업 중 예외 가능
    doMore();   // 예외 가능
}
// 마지막 close 가 추가 flush

4.6 flush vs force (FileChannel)

flush: 자바 → OS (page cache)
  - 데이터가 OS 의 캐시에
  - 시스템 충돌 시 손실 가능
  
force: OS → 디스크 (영구 저장)
  - fsync 시스템 호출
  - 디스크에 확실히 쓰임
  - 시스템 충돌 안전

// flush
bos.flush();
// 데이터가 OS 까지

// force (FileChannel)
FileChannel ch = ...;
ch.force(true);    // 메타데이터 포함
ch.force(false);   // 데이터만

// 완전 안전:
try (FileOutputStream fos = new FileOutputStream("file.txt");
     BufferedOutputStream bos = new BufferedOutputStream(fos)) {
    bos.write(data);
    bos.flush();
    fos.getChannel().force(false);   // 디스크까지
}

4.7 PrintWriter 의 autoFlush 와의 차이

// BufferedOutputStream — autoFlush 없음
BufferedOutputStream bos = ...;
bos.write(data);
// flush 안 됨

// PrintWriter — autoFlush 옵션
PrintWriter pw1 = new PrintWriter(out);             // autoFlush=false
PrintWriter pw2 = new PrintWriter(out, true);       // autoFlush=true

pw2.println("Hello");
// 자동 flush (println 후)

// 차이:
// - BufferedOutputStream: 항상 수동 flush
// - PrintWriter (autoFlush=true): println 후 자동

4.8 ILIC 활용

public class TransactionLogger {
    
    private final BufferedOutputStream bos;
    
    public TransactionLogger(Path path) throws IOException {
        this.bos = new BufferedOutputStream(
            new FileOutputStream(path.toFile(), true));   // append
    }
    
    public void logTransaction(Transaction tx) throws IOException {
        bos.write(tx.toBytes());
        bos.flush();   // ★ 즉시 디스크
        // 비정상 종료 시 손실 방지
    }
    
    public void close() throws IOException {
        bos.close();   // 자동 flush
    }
}

// 더 강력 (디스크까지)
public class CriticalLogger {
    
    private final FileOutputStream fos;
    private final BufferedOutputStream bos;
    
    public CriticalLogger(Path path) throws IOException {
        this.fos = new FileOutputStream(path.toFile(), true);
        this.bos = new BufferedOutputStream(fos);
    }
    
    public void logCritical(byte[] data) throws IOException {
        bos.write(data);
        bos.flush();                       // 자바 → OS
        fos.getChannel().force(false);     // OS → 디스크
        // 완전 안전
    }
}

4.9 자기 점검 답변

flush 의 정확한 동작과 필요 시점은?

:
1. flush 의 동작:

  • 버퍼 → 감싼 OutputStream (OS)
  • 감싼 stream 도 flush
  • count = 0 리셋
  1. 자동 flush:

    • close 시 자동
    • try-with-resources 권장
  2. 명시적 flush 필요 시점:

    • 진행 중 즉시 출력
    • 비정상 종료 대비
    • 다른 프로세스가 읽음
    • 네트워크 응답
    • 인터랙티브
  3. flush vs force:

    • flush: 자바 → OS
    • force: OS → 디스크 (fsync)
  4. PrintWriter 의 autoFlush:

    • BufferedOutputStream 엔 없음
    • PrintWriter 전용

5️⃣ Decorator 패턴의 정수

5.1 Decorator 패턴 의 정의

Decorator 패턴:

  객체에 추가 책임을 동적으로 부여하는 패턴.
  서브클래스 만들지 않고 기능 확장.

GoF 패턴 중 하나.

핵심:
  - 같은 인터페이스 (InputStream)
  - 다른 InputStream 을 감쌈
  - 추가 기능 (버퍼링)
  - 무한 중첩 가능

5.2 Stream 의 Decorator 계층

InputStream (Component)
  ├── FileInputStream (ConcreteComponent)
  ├── ByteArrayInputStream (ConcreteComponent)
  ├── FilterInputStream (Decorator)
  │   ├── BufferedInputStream (ConcreteDecorator)
  │   ├── DataInputStream (ConcreteDecorator)
  │   └── PushbackInputStream (ConcreteDecorator)
  └── ObjectInputStream

5.3 중첩 예시

// 4중 중첩
ObjectInputStream ois = new ObjectInputStream(
    new BufferedInputStream(
        new GZIPInputStream(
            new FileInputStream("data.dat.gz"))));

// 각 층의 역할:
// 1. FileInputStream — 파일 (Concrete)
// 2. GZIPInputStream — 압축 해제 (Decorator)
// 3. BufferedInputStream — 버퍼링 (Decorator)
// 4. ObjectInputStream — 객체 복원 (Decorator)

// 모두 InputStream 인터페이스
// 사용자는 ois.read() 만 호출
// 내부 4단계 처리

5.4 데이터 흐름

read() 호출 흐름 (역순):

1. ObjectInputStream.read()
   - 객체 복원 위해 bytes 필요
2. BufferedInputStream.read()
   - 버퍼에서 또는 채움
3. GZIPInputStream.read()
   - 압축 해제
4. FileInputStream.read()
   - OS 호출

리턴 흐름 (정순):
  byte → 압축 해제 → 버퍼 → ObjectInputStream
  
사용자는 1단계만 보지만
내부에선 4단계 처리

5.5 close 의 cascading

ObjectInputStream ois = new ObjectInputStream(
    new BufferedInputStream(
        new GZIPInputStream(
            new FileInputStream("data.dat.gz"))));

ois.close();
// 자동 cascading:
// 1. ObjectInputStream.close()
// 2. → BufferedInputStream.close()
// 3. → GZIPInputStream.close()
// 4. → FileInputStream.close()
// 모두 자동

// 즉, 가장 바깥만 close 해도 OK
// 내부 InputStream 들 모두 close

5.6 자기 정의 Decorator

// 압축 해제 + 카운트
public class CountingInputStream extends FilterInputStream {
    
    private long count = 0;
    
    public CountingInputStream(InputStream in) {
        super(in);
    }
    
    @Override
    public int read() throws IOException {
        int b = super.read();
        if (b != -1) count++;
        return b;
    }
    
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int n = super.read(b, off, len);
        if (n > 0) count += n;
        return n;
    }
    
    public long getCount() { return count; }
}

// 사용
try (CountingInputStream cis = new CountingInputStream(
        new BufferedInputStream(
            new FileInputStream("file.txt")))) {
    cis.transferTo(OutputStream.nullOutputStream());
    System.out.println("Read " + cis.getCount() + " bytes");
}

5.7 Decorator 패턴의 장점

장점:

1. 유연성
   - 무한 조합
   - 새 기능 추가 쉬움

2. 같은 인터페이스
   - 사용자 코드 변경 X
   - 다형성

3. SRP (단일 책임)
   - 각 Decorator 가 한 가지만

4. OCP (개방-폐쇄)
   - 새 Decorator 추가
   - 기존 코드 수정 X

5.8 Decorator 의 단점

단점:

1. 중첩 깊음
   - 가독성 ↓
   - 디버깅 어려움

2. 객체 수 ↑
   - 각 Decorator 가 객체
   - 메모리 약간

3. 성능 약간 ↓
   - 메서드 호출 체인
   - JIT 가 일부 해결

4. 디버깅
   - 스택 트레이스 깊음

5.9 ILIC 활용

public class ShipmentStreamProcessor {
    
    // 일반: BufferedInputStream + FileInputStream
    public byte[] readNormal(Path path) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(path.toFile()))) {
            return bis.readAllBytes();
        }
    }
    
    // 압축 파일 처리
    public byte[] readCompressed(Path path) throws IOException {
        try (InputStream is = new BufferedInputStream(
                new GZIPInputStream(
                    new FileInputStream(path.toFile())))) {
            return is.readAllBytes();
        }
    }
    
    // 직렬화된 객체 + 압축
    @SuppressWarnings("unchecked")
    public List<Shipment> readSerializedCompressed(Path path) 
            throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(
                new BufferedInputStream(
                    new GZIPInputStream(
                        new FileInputStream(path.toFile()))))) {
            return (List<Shipment>) ois.readObject();
        }
    }
    
    // 모니터링 (CountingInputStream)
    public byte[] readWithCount(Path path) throws IOException {
        try (CountingInputStream cis = new CountingInputStream(
                new BufferedInputStream(
                    new FileInputStream(path.toFile())))) {
            byte[] data = cis.readAllBytes();
            log.info("Read {} bytes from {}", cis.getCount(), path);
            return data;
        }
    }
}

5.10 자기 점검 답변

Decorator 패턴과 Stream 의 활용은?

:
1. Decorator 패턴:

  • 동적 기능 추가
  • 같은 인터페이스
  • 무한 조합
  1. Stream 계층:

    • InputStream/OutputStream (Component)
    • FileInputStream 등 (Concrete)
    • FilterInputStream (Decorator)
    • BufferedInputStream 등 (ConcreteDecorator)
  2. 활용:

    • 4중 중첩 가능
    • cascading close
    • 자기 정의 Decorator
  3. 장점:

    • 유연성, SRP, OCP
  4. 단점:

    • 중첩 깊음
    • 디버깅 어려움

6️⃣ 일반 vs Buffered 성능 비교

6.1 시스템 호출의 비용

일반 FileInputStream.read():
  - 매 호출 = 1 system call
  - system call ≈ 1μs (오버헤드)

BufferedInputStream.read():
  - 대부분 메모리 접근 (수 ns)
  - 8192 호출에 1 system call
  - 비율: 1/8192

6.2 벤치마크

public class BufferedBenchmark {
    
    public static void main(String[] args) throws IOException {
        Path file = Path.of("100MB.dat");
        
        // 1. 일반 FileInputStream.read() 1바이트
        long t1 = bench(() -> {
            try (FileInputStream fis = new FileInputStream(file.toFile())) {
                while (fis.read() != -1) { }
            }
        });
        
        // 2. BufferedInputStream.read() 1바이트
        long t2 = bench(() -> {
            try (BufferedInputStream bis = new BufferedInputStream(
                    new FileInputStream(file.toFile()))) {
                while (bis.read() != -1) { }
            }
        });
        
        // 3. FileInputStream.read(byte[8192])
        long t3 = bench(() -> {
            try (FileInputStream fis = new FileInputStream(file.toFile())) {
                byte[] buf = new byte[8192];
                while (fis.read(buf) != -1) { }
            }
        });
        
        // 4. BufferedInputStream + read(byte[8192])
        long t4 = bench(() -> {
            try (BufferedInputStream bis = new BufferedInputStream(
                    new FileInputStream(file.toFile()))) {
                byte[] buf = new byte[8192];
                while (bis.read(buf) != -1) { }
            }
        });
        
        System.out.printf("1바이트 일반:           %d ms%n", t1);
        System.out.printf("1바이트 Buffered:       %d ms%n", t2);
        System.out.printf("byte[8192] 일반:        %d ms%n", t3);
        System.out.printf("byte[8192] Buffered:    %d ms%n", t4);
    }
    
    static long bench(Runnable task) {
        long start = System.nanoTime();
        task.run();
        return (System.nanoTime() - start) / 1_000_000;
    }
}

6.3 예상 결과

100MB 파일 읽기 (대략):

1바이트 일반 FileInputStream:
  ~100,000 ms (100초)
  매 read = system call
  너무 느림

1바이트 BufferedInputStream:
  ~300 ms
  300배 빠름
  대부분 메모리 접근

byte[8192] 일반 FileInputStream:
  ~150 ms
  system call 횟수 = 100MB/8KB = 12,800번

byte[8192] BufferedInputStream:
  ~150 ms (비슷)
  큰 요청은 버퍼 우회 (Buffered 도 직접 OS)

결론:
  - 1바이트씩이면 Buffered 필수
  - 큰 byte[] 이면 차이 작음
  - 일반적으로 Buffered 권장

6.4 시나리오별 권장

1. 1바이트씩 read 필요
   → BufferedInputStream 필수
   → 300배 차이

2. 작은 byte[] (< 1KB) 반복
   → BufferedInputStream 권장
   → 일부 OS 호출 절감

3. 큰 byte[] (8KB+) 반복
   → BufferedInputStream 효과 작음
   → 그래도 권장 (안전)

4. 1만번 + 호출
   → BufferedInputStream 필수
   → 누적 효과

5. 텍스트 (readLine)
   → BufferedReader 사용
   → InputStream + readLine 은 BufferedReader 가 필수

6.5 OutputStream 의 경우

일반 vs Buffered 출력:

1. 1바이트씩 write
   - 일반: 매번 system call
   - Buffered: 버퍼에 누적
   - Buffered 가 매우 유리

2. 작은 write 반복 (로그)
   - 매 로그 = system call (일반)
   - 버퍼 가득 시만 (Buffered)
   - Buffered 가 유리

3. 큰 데이터 한 번에
   - 차이 작음
   - 일반도 OK

4. 명시적 flush 필요 시
   - 일반: 효과 없음
   - Buffered: flush 의 의미

6.6 메모리 사용

일반 FileInputStream:
  - 작은 객체 (수 KB)
  - 버퍼 없음

BufferedInputStream:
  - + 8KB 버퍼
  - 객체당 약 8KB
  - 1만 스트림 = 80MB

대용량 동시 처리:
  - 버퍼 크기 고려
  - 또는 풀링

6.7 ILIC 활용

public class FileProcessor {
    
    // 1. 무조건 Buffered (안전 + 빠름)
    public byte[] readFile(Path path) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(path.toFile()))) {
            return bis.readAllBytes();
        }
    }
    
    // 2. 작은 파일은 Buffered 안 해도 OK
    public byte[] readSmall(Path path) throws IOException {
        if (Files.size(path) < 4096) {
            return Files.readAllBytes(path);
        }
        return readFile(path);
    }
    
    // 3. 큰 청크면 Buffered 효과 작음
    public void processLargeChunks(Path path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path.toFile())) {
            // 큰 buf 사용 시 Buffered 안 해도
            byte[] buf = new byte[65536];   // 64KB
            int n;
            while ((n = fis.read(buf)) != -1) {
                processChunk(buf, 0, n);
            }
        }
    }
}

6.8 자기 점검 답변

일반 vs Buffered 의 성능 차이는?

:
1. 1바이트 read:

  • 일반: 매 호출 system call
  • Buffered: 메모리 접근
  • 300~수천 배 차이
  1. byte[8192] read:

    • 일반: system call/8192 바이트
    • Buffered: 비슷 (큰 요청은 우회)
    • 차이 작음
  2. 출력:

    • 작은 write 반복은 Buffered 유리
    • 큰 데이터는 차이 작음
  3. 권장:

    • 거의 항상 Buffered
    • 단, 큰 청크 + 적은 호출이면 효과 작음
  4. 메모리:

    • 객체당 8KB 추가
    • 대량 동시는 고려

7️⃣ mark/reset 의 동작

7.1 mark/reset 의 정의

public synchronized void mark(int readlimit);
public synchronized void reset() throws IOException;
public boolean markSupported();

핵심:

  • mark: 현재 위치 표시
  • reset: mark 위치로 복귀
  • BufferedInputStream 은 markSupported() = true
  • FileInputStream 은 false

7.2 mark 의 매개변수

public void mark(int readlimit);
// readlimit: mark 후 유효한 최대 read 바이트

// 의미:
// mark 후 readlimit 만큼 read 가능
// 더 멀리 가면 mark 무효화

7.3 사용 예

try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("file.txt"))) {
    
    // 처음 5바이트 읽기
    byte[] buf1 = new byte[5];
    bis.read(buf1);   // "Hello"
    
    bis.mark(1000);   // 현재 위치 표시 (1000 까지 유효)
    
    // 다음 5바이트
    byte[] buf2 = new byte[5];
    bis.read(buf2);   // " Worl"
    
    // mark 위치로 복귀
    bis.reset();
    
    // 다시 같은 위치 읽기
    byte[] buf3 = new byte[5];
    bis.read(buf3);   // " Worl"
    
    System.out.println(new String(buf1));   // "Hello"
    System.out.println(new String(buf2));   // " Worl"
    System.out.println(new String(buf3));   // " Worl"
}

7.4 mark/reset 의 내부

BufferedInputStream 내부:

buf      = [H, e, l, l, o, _, W, o, r, l, d, ...]
count    = 12 (모두 채워짐)
pos      = 5 (Hello 읽음)
markpos  = -1

mark(1000):
  markpos = 5
  marklimit = 1000

read 5번 → pos = 10

reset():
  pos = markpos = 5
  → "World" 부터 다시 읽기

7.5 mark 가 무효화되는 경우

// readlimit 초과
bis.mark(100);
// 100바이트 안에서 reset 가능

byte[] buf = new byte[200];
bis.read(buf);   // 200바이트 읽음 (> 100)
// mark 무효화

bis.reset();   // IOException 가능
// "Resetting to invalid mark"

7.6 markSupported 의 의미

InputStream is = ...;

if (is.markSupported()) {
    is.mark(1024);
    // ... read ...
    is.reset();
}

// 지원 여부:
// BufferedInputStream: true
// FileInputStream: false
// ByteArrayInputStream: true
// PushbackInputStream: 일부

7.7 활용 — 헤더 검사

// 파일 헤더로 형식 판단
public class FileTypeDetector {
    
    public String detectType(InputStream is) throws IOException {
        if (!is.markSupported()) {
            throw new IllegalArgumentException("Mark not supported");
        }
        
        is.mark(16);   // 16바이트 안에서
        
        byte[] header = new byte[16];
        int n = is.read(header);
        
        is.reset();   // 다시 처음으로
        
        // 헤더 분석
        if (startsWith(header, new byte[]{0x50, 0x4B})) {   // PK
            return "ZIP";
        } else if (startsWith(header, new byte[]{(byte)0xFF, (byte)0xD8})) {
            return "JPEG";
        } else if (startsWith(header, new byte[]{(byte)0x89, 0x50, 0x4E, 0x47})) {
            return "PNG";
        }
        return "UNKNOWN";
    }
    
    private boolean startsWith(byte[] data, byte[] prefix) {
        if (data.length < prefix.length) return false;
        for (int i = 0; i < prefix.length; i++) {
            if (data[i] != prefix[i]) return false;
        }
        return true;
    }
}

// 사용
try (InputStream is = new BufferedInputStream(
        new FileInputStream("file.dat"))) {
    String type = detector.detectType(is);
    // 이후 같은 stream 으로 계속 읽기
}

7.8 자기 점검 답변

BufferedInputStream 의 mark/reset 동작은?

:
1. mark(int):

  • 현재 위치 표시
  • readlimit 까지 유효
  1. reset():

    • mark 위치로 복귀
    • IOException (무효 시)
  2. markSupported():

    • BufferedInputStream: true
    • FileInputStream: false
    • ByteArrayInputStream: true
  3. 활용:

    • 헤더 검사 후 다시 읽기
    • 토큰 파싱
    • peek 기능
  4. 내부:

    • markpos, marklimit
    • 버퍼 안에서만 유효

8️⃣ 실무 활용과 함정

8.1 권장 패턴 종합

// 패턴 1: 기본 (가장 자주)
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("file.txt"))) {
    // 처리
}

// 패턴 2: 두 자원 명시
try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    // 처리
}

// 패턴 3: NIO.2 + Buffered (드물게)
try (InputStream is = Files.newInputStream(path);
     BufferedInputStream bis = new BufferedInputStream(is)) {
    // 처리
}

// 패턴 4: 큰 버퍼 (대용량)
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("large.dat"), 65536)) {   // 64KB
    // 처리
}

// 패턴 5: 텍스트 (BufferedReader 권장)
try (BufferedReader br = Files.newBufferedReader(path, UTF_8)) {
    // 한 줄씩 처리
}

8.2 출력 패턴

// 패턴 1: 기본
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    bos.write(data);
}

// 패턴 2: 명시적 flush (진행 중)
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    for (byte[] chunk : chunks) {
        bos.write(chunk);
        bos.flush();   // 매 청크마다
    }
}

// 패턴 3: 텍스트 (BufferedWriter)
try (BufferedWriter bw = Files.newBufferedWriter(path, UTF_8)) {
    bw.write("Hello\n");
}

// 패턴 4: 디스크 동기화까지
try (FileOutputStream fos = new FileOutputStream("file.txt");
     BufferedOutputStream bos = new BufferedOutputStream(fos)) {
    bos.write(criticalData);
    bos.flush();
    fos.getChannel().force(false);   // 디스크까지
}

8.3 흔한 함정 종합

함정 1: flush 누락
  - 데이터가 버퍼에만
  - 비정상 종료 시 손실
  - 해결: try-with-resources (자동 close + flush)

함정 2: 두 번 Buffered
  new BufferedInputStream(new BufferedInputStream(fis))
  - 의미 없음, 비효율
  - 해결: 한 번만

함정 3: 작은 파일에 Buffered
  // 1KB 파일에 8KB 버퍼
  - 메모리 낭비
  - 단, 일관성 위해 사용 가능
  - 또는 Files.readAllBytes 가 더 간단

함정 4: 큰 버퍼 남용
  // 1MB 버퍼
  - 메모리 ↑
  - 효과 미미 (8KB 부터 효과 평탄)
  - 8~64KB 권장

함정 5: 중간만 close
  // BufferedInputStream 만 close 하지 말고
  // try-with-resources 활용

함정 6: mark 후 무효화
  - readlimit 초과
  - reset 시 IOException
  - 해결: 충분한 readlimit

함정 7: BufferedReader 와 혼동
  // 텍스트는 BufferedReader
  // 바이너리는 BufferedInputStream

8.4 BufferedReader/Writer 와의 비교

BufferedInputStream/OutputStream:
  - byte 단위
  - 바이너리 파일
  - 8KB 버퍼

BufferedReader/Writer:
  - char 단위
  - 텍스트 파일
  - 8KB 버퍼 (char)
  - readLine, newLine

선택:
  - 바이너리: BufferedInputStream/OutputStream
  - 텍스트: BufferedReader/Writer
  - 함께 사용 가능 (체인)

8.5 자기 정의 버퍼 크기

// 시나리오별 권장
public class BufferSize {
    
    public static final int SMALL = 1024;       // 1KB — 작은 파일
    public static final int DEFAULT = 8192;      // 8KB — 일반 (표준)
    public static final int MEDIUM = 16384;      // 16KB — 중간
    public static final int LARGE = 65536;       // 64KB — 대용량
    public static final int HUGE = 1048576;      // 1MB — 매우 큰 파일
}

// 활용
new BufferedInputStream(fis, BufferSize.LARGE);

6.6 ILIC 의 종합 활용

@Service
public class ShipmentBufferService {
    
    // 1. 일반 바이너리 처리
    public byte[] readBinary(Path path) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(path.toFile()))) {
            return bis.readAllBytes();
        }
    }
    
    // 2. 큰 파일 복사 (Buffered + transferTo)
    public void copyLargeFile(Path src, Path dest) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(src.toFile()), 65536);
             BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream(dest.toFile()), 65536)) {
            bis.transferTo(bos);
        }
    }
    
    // 3. 점진적 쓰기 + flush
    public void writeProgressive(Path path, List<byte[]> records) throws IOException {
        try (BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream(path.toFile()))) {
            for (int i = 0; i < records.size(); i++) {
                bos.write(records.get(i));
                if (i % 1000 == 0) {
                    bos.flush();
                    log.info("Written {} records", i);
                }
            }
        }
    }
    
    // 4. 파일 형식 판단 (mark/reset)
    public String detectFileType(Path path) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(path.toFile()))) {
            
            if (!bis.markSupported()) {
                return "UNKNOWN";
            }
            
            bis.mark(16);
            byte[] header = new byte[16];
            int n = bis.read(header);
            bis.reset();
            
            return analyzeHeader(header);
        }
    }
    
    // 5. 중요 데이터 — 디스크 동기화
    public void writeCritical(Path path, byte[] data) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(path.toFile(), true);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            bos.write(data);
            bos.flush();                          // 자바 → OS
            fos.getChannel().force(false);        // OS → 디스크
        }
    }
}

8.7 자기 점검 답변

실무 활용과 함정 종합?

:
1. 권장 패턴:

  • try-with-resources
  • 기본 8KB 버퍼
  • 대용량은 64KB
  1. 흔한 함정 7가지:

    • flush 누락
    • 두 번 Buffered
    • 작은 파일 Buffered
    • 큰 버퍼 남용
    • 중간만 close
    • mark 무효화
    • Reader 와 혼동
  2. BufferedReader 와 차이:

    • byte vs char
    • 바이너리 vs 텍스트
  3. flush 시점:

    • 진행 중
    • 비정상 종료 대비
    • 디스크 동기화 필요시 force

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
BufferedInputStream 정의?FilterInputStream 자식, 8KB 버퍼
내부 구조?buf, count, pos, markpos
버퍼링의 효과?system call 절감, 메모리 접근
flush 의 동작?버퍼 → OS
flush 시점?진행 중, 비정상 종료, 네트워크
close 와 flush?close 가 자동 flush
Decorator 패턴?동적 기능 추가, 같은 인터페이스
1바이트 일반 vs Buffered?수백~수천 배 차이
큰 byte[] 차이?작음 (큰 요청은 버퍼 우회)
mark/reset 지원?true
FileInputStream mark?false
readlimit?mark 후 유효 거리
BufferedReader 와 차이?byte vs char

9.2 자기 점검 체크리스트

정의

  • BufferedInputStream 의 위치
  • FilterInputStream 자식
  • 내부 4가지 상태

동작

  • read() 의 메커니즘
  • read(byte[]) 의 우회
  • fill 의 시점
  • OS 호출 횟수

Output

  • BufferedOutputStream 의 구조
  • write 의 누적
  • flush 의 두 가지 효과
  • close 의 자동 flush

Decorator

  • 패턴 의의
  • 4중 중첩 가능
  • cascading close
  • 자기 정의

성능

  • 1바이트 차이
  • 큰 byte[] 차이
  • 시나리오별 권장

mark/reset

  • mark 의 매개변수
  • reset 의 동작
  • markSupported

실무

  • 권장 패턴
  • 흔한 함정 7가지
  • BufferedReader 와 선택

9.3 추가 심화 질문

Q1: 왜 8KB 가 기본?

답:

  • OS 페이지 크기 (4KB) 의 배수
  • 디스크 블록 크기 친화적
  • 메모리/성능 균형
  • 실측에서 sweet spot

Q2: BufferedInputStream 의 동시성?

답:

  • 대부분 메서드 synchronized
  • 다중 스레드 안전
  • 단, 외부 동기화 추가 권장

Q3: flush 가 디스크까지 보장?

답:

  • 아니다
  • flush 는 자바 → OS (page cache)
  • 디스크는 force (FileChannel.force)
  • 또는 SYNC 옵션 (NIO.2)

Q4: BufferedInputStream 의 메모리?

답:

  • 객체당 ~8KB
  • 1만 스트림 = 80MB
  • 풀링 고려
  • 대량 동시는 Direct Buffer

Q5: 일반 InputStream 에 Buffered 안 하면?

답:

  • 작은 read 반복 시 매우 느림
  • system call 폭증
  • 1바이트씩 1MB 파일 = 100만 system call
  • 거의 항상 Buffered 권장

🎯 핵심 요약 — 3줄 정리

1. Buffered* 의 본질

  • 8KB 버퍼로 system call 절감
  • Decorator 패턴
  • 수십~수천 배 빠름

2. flush 의 두 가지

  • 버퍼 → OS (자바 측)
  • 감싼 stream 도 flush
  • close 가 자동 flush

3. 권장

  • 거의 항상 Buffered
  • try-with-resources
  • 디스크 동기화는 FileChannel.force

📚 다음으로...

Unit 9.3 — DataInputStream / DataOutputStream

이번 Unit에서 버퍼링을 봤다면, 다음은 기본 타입 입출력.

  • DataInputStream 의 정의
  • 기본 타입 메서드 (readInt, readLong, readUTF 등)
  • DataOutputStream 의 정의
  • 바이너리 형식의 활용

Phase 9 진행 상황

🚀 Phase 9 — I/O 강화
  ✅ Unit 9.1 try-with-resources
  ✅ Unit 9.2 BufferedInputStream / BufferedOutputStream ← 여기
  ⏭ Unit 9.3 DataInputStream / DataOutputStream
  ⏭ Unit 9.4 Serialization (직렬화)
  ⏭ Unit 9.5 serialVersionUID

3주차 누적 진행

✅ Phase 1 ~ 8 완주 (37 Unit)
🚀 Phase 9 — I/O 강화 (2/5 진행)

총: 39/43 Unit (약 91%)

profile
Software Developer

0개의 댓글