F-LAB JAVA · 3주차 · Phase 9 · I/O 강화
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
BufferedInputStream과BufferedOutputStream은 내부에 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* = 우편함 + 한 번에 우체국.
1. BufferedInputStream 의 정의와 구조
2. BufferedInputStream 의 동작 메커니즘
3. BufferedOutputStream 의 정의와 구조
4. BufferedOutputStream 의 flush 정밀
5. Decorator 패턴의 정수
6. 일반 vs Buffered 성능 비교
7. mark/reset 의 동작
8. 실무 활용과 함정
9. 면접 + 자기 점검
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);
}
핵심:
InputStream (추상)
├── FileInputStream
├── ByteArrayInputStream
├── FilterInputStream ← Decorator 부모
│ ├── BufferedInputStream ← 여기
│ ├── DataInputStream
│ ├── PushbackInputStream
│ └── 기타
├── ObjectInputStream
└── ...
// 기본 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"))) {
// 처리
}
4가지 내부 상태:
buf: byte[8192]
- 실제 데이터를 담는 버퍼
count: 버퍼 내 유효 데이터 수
- 0 ≤ count ≤ buf.length
- 0: 버퍼 비어있음
- count: 마지막 유효 바이트 + 1
pos: 다음 읽기 위치
- 0 ≤ pos ≤ count
- pos == count: 버퍼 다 읽음
markpos: mark 호출 위치
- -1: mark 안 됨
- 그 외: 표시된 위치
marklimit: mark 후 유효 거리
- 너무 멀리 가면 무효화
초기 상태:
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 호출 (버퍼 채움)
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;
}
핵심:
// 상속 (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();
특징:
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();
}
}
}
BufferedInputStream 의 정의와 내부 구조는?
답:
1. 정의:
내부 상태 4가지:
버퍼 크기:
효과:
사용자 코드:
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배 절감
// 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;
}
핵심:
시나리오: 파일 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 호출
...
더 효율적
버퍼 채워지는 조건:
1. 최초 read 호출 시
- pos = 0, count = 0
- fill 호출
2. 버퍼 다 읽었을 때
- pos == count
- fill 호출
3. mark/reset 처리 시
- mark 위치 보존 위해 추가 처리
채워지는 양:
- 보통 buf.length (8192)
- 또는 가능한 만큼
- 네트워크는 패킷 단위
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
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;
}
}
BufferedInputStream 의 동작 메커니즘은?
답:
1. 첫 read:
이후 read:
OS 호출 횟수:
read(byte[]) 최적화:
close:
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);
}
핵심:
2가지 상태:
buf: byte[8192]
- 데이터를 임시 보관
count: 현재까지 쓴 바이트 수
- 0 ≤ count ≤ buf.length
- count == buf.length → 가득, flush
- count == 0 → 비어있음
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;
}
}
시나리오: 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
시나리오: 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
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 가 디스크에 안전하게
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);
}
}
}
BufferedOutputStream 의 정의와 구조는?
답:
1. 정의:
내부 상태 2가지:
write 동작:
close:
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
private void flushBuffer() throws IOException {
if (count > 0) {
out.write(buf, 0, count);
count = 0;
}
}
핵심:
효과 1: 버퍼 → OS
- BufferedOutputStream 의 buf 안의 데이터
- 감싼 FileOutputStream 으로
- OS 의 page cache 까지
효과 2: 감싼 stream 도 flush
- 체인된 stream 의 모든 버퍼 비움
- 예: GZIPOutputStream → BufferedOutputStream → FileOutputStream
- 모두 비움
주의:
- flush 는 OS 까지만
- OS → 디스크는 FileChannel.force 필요
// ❌ 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 로
명시적 flush 가 필요한 경우:
1. 진행 중 즉시 출력
- 실시간 로그
- 진행률 표시
- 디버깅
2. 비정상 종료 대비
- 트랜잭션 로그
- 중요 데이터
3. 다른 프로세스가 읽어야 할 때
- 파일 모니터링
- 다른 도구가 쓰는 동시에 읽기
4. 네트워크 (스트림)
- 응답 즉시 전송
- 클라이언트가 대기 중
5. 인터랙티브 도구
- 사용자 입력 후 응답
- 즉시 보여야
// 자동 (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
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); // 디스크까지
}
// 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 후 자동
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 → 디스크
// 완전 안전
}
}
flush 의 정확한 동작과 필요 시점은?
답:
1. flush 의 동작:
자동 flush:
명시적 flush 필요 시점:
flush vs force:
PrintWriter 의 autoFlush:
Decorator 패턴:
객체에 추가 책임을 동적으로 부여하는 패턴.
서브클래스 만들지 않고 기능 확장.
GoF 패턴 중 하나.
핵심:
- 같은 인터페이스 (InputStream)
- 다른 InputStream 을 감쌈
- 추가 기능 (버퍼링)
- 무한 중첩 가능
InputStream (Component)
├── FileInputStream (ConcreteComponent)
├── ByteArrayInputStream (ConcreteComponent)
├── FilterInputStream (Decorator)
│ ├── BufferedInputStream (ConcreteDecorator)
│ ├── DataInputStream (ConcreteDecorator)
│ └── PushbackInputStream (ConcreteDecorator)
└── ObjectInputStream
// 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단계 처리
read() 호출 흐름 (역순):
1. ObjectInputStream.read()
- 객체 복원 위해 bytes 필요
2. BufferedInputStream.read()
- 버퍼에서 또는 채움
3. GZIPInputStream.read()
- 압축 해제
4. FileInputStream.read()
- OS 호출
리턴 흐름 (정순):
byte → 압축 해제 → 버퍼 → ObjectInputStream
사용자는 1단계만 보지만
내부에선 4단계 처리
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
// 압축 해제 + 카운트
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");
}
장점:
1. 유연성
- 무한 조합
- 새 기능 추가 쉬움
2. 같은 인터페이스
- 사용자 코드 변경 X
- 다형성
3. SRP (단일 책임)
- 각 Decorator 가 한 가지만
4. OCP (개방-폐쇄)
- 새 Decorator 추가
- 기존 코드 수정 X
단점:
1. 중첩 깊음
- 가독성 ↓
- 디버깅 어려움
2. 객체 수 ↑
- 각 Decorator 가 객체
- 메모리 약간
3. 성능 약간 ↓
- 메서드 호출 체인
- JIT 가 일부 해결
4. 디버깅
- 스택 트레이스 깊음
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;
}
}
}
Decorator 패턴과 Stream 의 활용은?
답:
1. Decorator 패턴:
Stream 계층:
활용:
장점:
단점:
일반 FileInputStream.read():
- 매 호출 = 1 system call
- system call ≈ 1μs (오버헤드)
BufferedInputStream.read():
- 대부분 메모리 접근 (수 ns)
- 8192 호출에 1 system call
- 비율: 1/8192
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;
}
}
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 권장
1. 1바이트씩 read 필요
→ BufferedInputStream 필수
→ 300배 차이
2. 작은 byte[] (< 1KB) 반복
→ BufferedInputStream 권장
→ 일부 OS 호출 절감
3. 큰 byte[] (8KB+) 반복
→ BufferedInputStream 효과 작음
→ 그래도 권장 (안전)
4. 1만번 + 호출
→ BufferedInputStream 필수
→ 누적 효과
5. 텍스트 (readLine)
→ BufferedReader 사용
→ InputStream + readLine 은 BufferedReader 가 필수
일반 vs Buffered 출력:
1. 1바이트씩 write
- 일반: 매번 system call
- Buffered: 버퍼에 누적
- Buffered 가 매우 유리
2. 작은 write 반복 (로그)
- 매 로그 = system call (일반)
- 버퍼 가득 시만 (Buffered)
- Buffered 가 유리
3. 큰 데이터 한 번에
- 차이 작음
- 일반도 OK
4. 명시적 flush 필요 시
- 일반: 효과 없음
- Buffered: flush 의 의미
일반 FileInputStream:
- 작은 객체 (수 KB)
- 버퍼 없음
BufferedInputStream:
- + 8KB 버퍼
- 객체당 약 8KB
- 1만 스트림 = 80MB
대용량 동시 처리:
- 버퍼 크기 고려
- 또는 풀링
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);
}
}
}
}
일반 vs Buffered 의 성능 차이는?
답:
1. 1바이트 read:
byte[8192] read:
출력:
권장:
메모리:
public synchronized void mark(int readlimit);
public synchronized void reset() throws IOException;
public boolean markSupported();
핵심:
public void mark(int readlimit);
// readlimit: mark 후 유효한 최대 read 바이트
// 의미:
// mark 후 readlimit 만큼 read 가능
// 더 멀리 가면 mark 무효화
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"
}
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" 부터 다시 읽기
// readlimit 초과
bis.mark(100);
// 100바이트 안에서 reset 가능
byte[] buf = new byte[200];
bis.read(buf); // 200바이트 읽음 (> 100)
// mark 무효화
bis.reset(); // IOException 가능
// "Resetting to invalid mark"
InputStream is = ...;
if (is.markSupported()) {
is.mark(1024);
// ... read ...
is.reset();
}
// 지원 여부:
// BufferedInputStream: true
// FileInputStream: false
// ByteArrayInputStream: true
// PushbackInputStream: 일부
// 파일 헤더로 형식 판단
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 으로 계속 읽기
}
BufferedInputStream 의 mark/reset 동작은?
답:
1. mark(int):
reset():
markSupported():
활용:
내부:
// 패턴 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)) {
// 한 줄씩 처리
}
// 패턴 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); // 디스크까지
}
함정 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
BufferedInputStream/OutputStream:
- byte 단위
- 바이너리 파일
- 8KB 버퍼
BufferedReader/Writer:
- char 단위
- 텍스트 파일
- 8KB 버퍼 (char)
- readLine, newLine
선택:
- 바이너리: BufferedInputStream/OutputStream
- 텍스트: BufferedReader/Writer
- 함께 사용 가능 (체인)
// 시나리오별 권장
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);
@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 → 디스크
}
}
}
실무 활용과 함정 종합?
답:
1. 권장 패턴:
흔한 함정 7가지:
BufferedReader 와 차이:
flush 시점:
| 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 |
답:
답:
답:
답:
답:
1. Buffered* 의 본질
2. flush 의 두 가지
3. 권장
이번 Unit에서 버퍼링을 봤다면, 다음은 기본 타입 입출력.
🚀 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
✅ Phase 1 ~ 8 완주 (37 Unit)
🚀 Phase 9 — I/O 강화 (2/5 진행)
총: 39/43 Unit (약 91%)