3주차 Unit 8.4 — FileOutputStream

Psj·2026년 5월 20일

F-lab

목록 보기
109/197

Unit 8.4 — FileOutputStream

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


📌 학습 목표

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

  • FileOutputStream 의 정의와 OutputStream 과의 관계는?
  • 5가지 생성자 의 차이는?
  • new FileOutputStream(path, true) 의 의미는? (이어쓰기)
  • write(int) 의 정확한 동작 과 1바이트 한계는?
  • write(byte[]), write(byte[], off, len) 의 차이는?
  • flush() 의 역할 과 언제 호출하나?
  • 바이트가 영어로 보이는 이유 (인코딩 매핑)는?
  • 한글을 write(byte) 로 1바이트씩 쓰면 왜 깨지나?
  • 파일 생성 + 부모 디렉토리 의 함정은?

🎯 핵심 한 문장

FileOutputStreamOutputStream 의 파일 전용 구현으로 디스크에 1바이트씩 쓰는 가장 기본적인 도구다.
new FileOutputStream(path)덮어쓰기 모드 (기본), new FileOutputStream(path, true)이어쓰기 (append) 모드.
write(int b) 가 int 의 하위 8비트 (0~255) 만 사용하고, 1바이트씩 쓰면 한글이 깨진다 — 한글 1글자 = UTF-8 3바이트인데 각 바이트를 따로 쓰면 의미 잃음.
파일에 저장되는 건 숫자 (바이트) 이며, 텍스트 에디터가 인코딩에 따라 문자로 보여줌 — 65 → 'A' 는 ASCII/UTF-8 매핑일 뿐.
FileOutputStream(new File(path), true) 모드와 getCanonicalPath, flush() 의 정확한 의미를 알면 실무 버그의 70%를 회피.

비유 — 노트에 글자 쓰기

FileOutputStream:
  새 노트 펴고 글자 쓰기
  - 1바이트 (1글자) 씩
  - 기본: 새 노트 (덮어쓰기)
  - append: 이어 쓰기

write(int b):
  - int 65 입력
  - 노트에 0x41 (1바이트) 쓰임
  - 텍스트 에디터로 보면 'A' (ASCII 매핑)
  - 사실 노트엔 그냥 숫자 65

한글 "안" (3바이트):
  - 1바이트씩 따로 쓰면 ★ 깨짐
  - 한 번에 byte[] 로 써야

→ FileOutputStream = JVM → 파일의 가장 기본 통로.


🧭 9개 섹션 로드맵

1. FileOutputStream 의 정의
2. 5가지 생성자
3. 덮어쓰기 vs 이어쓰기 (append)
4. write 메서드 종류
5. flush() 의 역할
6. 바이트가 문자로 보이는 이유
7. 한글이 깨지는 메커니즘
8. 파일 생성과 부모 디렉토리 함정
9. 면접 + 자기 점검

1️⃣ FileOutputStream 의 정의

1.1 클래스 정의

package java.io;

public class FileOutputStream extends OutputStream {
    
    private final FileDescriptor fd;
    private final boolean append;
    private final String path;
    
    // 생성자, 메서드, ...
}

핵심:

  • OutputStream 의 자식
  • java.io 패키지
  • Java 1.0 부터
  • 파일 디스크립터 + append 모드 + 경로

1.2 OutputStream 계층

OutputStream 의 계층:

OutputStream (추상)
  ├── FileOutputStream          ← 파일
  ├── ByteArrayOutputStream    ← 메모리
  ├── PipedOutputStream         ← 파이프
  ├── FilterOutputStream
  │   ├── BufferedOutputStream
  │   ├── DataOutputStream
  │   └── PrintStream           ← System.out
  ├── ObjectOutputStream        ← 직렬화
  └── 기타

1.3 FileOutputStream 의 주요 메서드

public class FileOutputStream extends OutputStream {
    
    // 핵심 메서드
    public void write(int b) throws IOException;
    public void write(byte[] b) throws IOException;
    public void write(byte[] b, int off, int len) throws IOException;
    
    // 추가
    public void flush() throws IOException;
    public void close() throws IOException;
    
    // FileOutputStream 만의 메서드
    public FileChannel getChannel();
    public FileDescriptor getFD() throws IOException;
}

1.4 기본 사용

// 간단한 쓰기
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
    fos.write(72);     // 'H'
    fos.write(101);    // 'e'
    fos.write(108);    // 'l'
    fos.write(108);    // 'l'
    fos.write(111);    // 'o'
}

// 파일 내용: "Hello"
// 실제 저장된 것: 0x48 0x65 0x6C 0x6C 0x6F (5바이트)

1.5 OutputStream 의 자식으로서의 다형성

// OutputStream 으로 추상화
public void write(OutputStream os, byte[] data) throws IOException {
    os.write(data);
}

// 다양한 OutputStream 으로 호출
write(new FileOutputStream("file.txt"), data);   // 파일
write(new ByteArrayOutputStream(), data);          // 메모리
write(socket.getOutputStream(), data);             // 네트워크
write(System.out, data);                          // 콘솔

1.6 자원 관리의 중요성

// ❌ 자원 누수
FileOutputStream fos = new FileOutputStream("file.txt");
fos.write(data);
// close 안 함!
// 파일 핸들 누수 + 버퍼 데이터 손실

// ✓ try-with-resources
try (FileOutputStream fos = new FileOutputStream("file.txt")) {
    fos.write(data);
}
// 자동 close
// 버퍼 flush 자동

1.7 ILIC 의 활용

public class ShipmentFileWriter {
    
    // 1. 단순 쓰기
    public void writeBytes(Path path, byte[] data) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
            fos.write(data);
        }
    }
    
    // 2. NIO.2 권장
    public void writeBytesModern(Path path, byte[] data) throws IOException {
        Files.write(path, data);
        // 더 간결, 명확한 예외
    }
}

1.8 자기 점검 답변

FileOutputStream 의 정의와 위치는?

:
1. 정의:

  • OutputStream 의 파일 전용 구현
  • java.io 패키지
  • Java 1.0+
  1. 본질:

    • 파일 디스크립터 + append 모드 + 경로
    • OS 의 파일 핸들 활용
  2. 다형성:

    • OutputStream 추상화
    • 메모리/네트워크 등과 동일 API
  3. 자원:

    • close 필수
    • try-with-resources 권장
    • 안 닫으면 데이터 손실 가능

2️⃣ 5가지 생성자

2.1 5가지 생성자

// 1. 경로 문자열 (덮어쓰기)
public FileOutputStream(String name) throws FileNotFoundException;

// 2. 경로 문자열 + append 모드
public FileOutputStream(String name, boolean append) throws FileNotFoundException;

// 3. File 객체 (덮어쓰기)
public FileOutputStream(File file) throws FileNotFoundException;

// 4. File 객체 + append 모드
public FileOutputStream(File file, boolean append) throws FileNotFoundException;

// 5. FileDescriptor
public FileOutputStream(FileDescriptor fdObj);

2.2 경로 문자열

// 덮어쓰기 (기본)
FileOutputStream fos1 = new FileOutputStream("file.txt");
// 파일이 있으면 내용 삭제
// 파일이 없으면 생성

// 이어쓰기
FileOutputStream fos2 = new FileOutputStream("file.txt", true);
// 파일 끝에 추가
// 파일이 없으면 생성

// 절대 경로
FileOutputStream fos3 = new FileOutputStream("/var/data/file.txt");
// 부모 디렉토리 /var/data 가 있어야!

2.3 File 객체

File file = new File("file.txt");

// 덮어쓰기
FileOutputStream fos1 = new FileOutputStream(file);

// 이어쓰기
FileOutputStream fos2 = new FileOutputStream(file, true);

// 활용
File logFile = new File("/var/log/app.log");
if (!logFile.exists()) {
    logFile.getParentFile().mkdirs();
}
FileOutputStream fos = new FileOutputStream(logFile, true);   // 이어쓰기

2.4 FileDescriptor (드물게)

// 표준 출력으로
FileOutputStream stdout = new FileOutputStream(FileDescriptor.out);
stdout.write("Hello".getBytes());

// 표준 에러로
FileOutputStream stderr = new FileOutputStream(FileDescriptor.err);

// 일반적으로 사용 X
// System.out, System.err 직접 사용 권장

2.5 생성자의 동작

// 생성자가 즉시 파일 처리:

// 1. 파일이 없으면
//    - 새로 생성 (빈 파일)

// 2. 파일이 있고 덮어쓰기 모드
//    - 파일 truncate (크기 0 으로)
//    - 내용 삭제

// 3. 파일이 있고 append 모드
//    - 파일 유지
//    - 쓰기 위치를 끝으로

// 4. 부모 디렉토리 없음
//    - FileNotFoundException

// 5. 권한 없음
//    - FileNotFoundException (Java) 또는 IOException

2.6 NIO.2 의 대안

// java.io
FileOutputStream fos = new FileOutputStream("file.txt");
FileOutputStream append = new FileOutputStream("file.txt", true);

// NIO.2 (Java 7+) — 명시적 옵션
OutputStream os1 = Files.newOutputStream(Path.of("file.txt"));
// 기본: CREATE, TRUNCATE_EXISTING, WRITE

OutputStream os2 = Files.newOutputStream(Path.of("file.txt"),
    StandardOpenOption.CREATE,
    StandardOpenOption.WRITE,
    StandardOpenOption.APPEND);   // ★ append

// 다른 옵션
StandardOpenOption.CREATE             // 없으면 생성
StandardOpenOption.CREATE_NEW         // 없으면 생성, 있으면 실패
StandardOpenOption.WRITE              // 쓰기
StandardOpenOption.APPEND             // 끝에 추가
StandardOpenOption.TRUNCATE_EXISTING  // 있으면 truncate
StandardOpenOption.DELETE_ON_CLOSE    // 닫을 때 삭제
StandardOpenOption.SYNC               // 강제 동기화
StandardOpenOption.DSYNC              // 데이터만 동기화

2.7 ILIC 의 활용

public class ShipmentLogger {
    
    private final Path logFile;
    
    public ShipmentLogger(Path logFile) {
        this.logFile = logFile;
    }
    
    // 1. 덮어쓰기 — 새 로그
    public void initLog() throws IOException {
        try (FileOutputStream fos = new FileOutputStream(logFile.toFile())) {
            fos.write("=== Log Started ===\n".getBytes());
        }
    }
    
    // 2. 이어쓰기 — 누적
    public void appendLog(String message) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(logFile.toFile(), true)) {
            String line = LocalDateTime.now() + " " + message + "\n";
            fos.write(line.getBytes(StandardCharsets.UTF_8));
        }
    }
    
    // 3. NIO.2 권장
    public void appendLogModern(String message) throws IOException {
        String line = LocalDateTime.now() + " " + message + "\n";
        Files.write(logFile, line.getBytes(StandardCharsets.UTF_8),
            StandardOpenOption.CREATE, StandardOpenOption.APPEND);
    }
}

2.8 자기 점검 답변

FileOutputStream 의 5가지 생성자는?

:
1. String: 경로, 덮어쓰기
2. String + boolean: 경로 + append
3. File: 객체, 덮어쓰기
4. File + boolean: 객체 + append
5. FileDescriptor: 저수준

동작:

  • 파일 없으면 생성
  • 있고 덮어쓰기: truncate
  • 있고 append: 끝에 위치

NIO.2 대안:

  • Files.newOutputStream(Path, StandardOpenOption...)
  • 명시적 옵션
  • 더 세밀한 제어

3️⃣ 덮어쓰기 vs 이어쓰기 (append)

3.1 두 모드의 차이

덮어쓰기 (기본, append = false):
  1. 파일 열기
  2. 기존 내용 truncate (크기 0)
  3. write 호출은 처음부터
  4. 기존 데이터 손실

이어쓰기 (append = true):
  1. 파일 열기
  2. 쓰기 위치를 파일 끝으로
  3. write 호출은 끝에 추가
  4. 기존 데이터 보존

3.2 덮어쓰기 시각화

초기 파일: "Hello World"

// 덮어쓰기 모드
try (FileOutputStream fos = new FileOutputStream("file.txt")) {
    fos.write("Bye".getBytes());
}

// 결과 파일: "Bye"
// "Hello World" 완전히 사라짐

3.3 이어쓰기 시각화

초기 파일: "Hello World"

// 이어쓰기 모드
try (FileOutputStream fos = new FileOutputStream("file.txt", true)) {
    fos.write("Bye".getBytes());
}

// 결과 파일: "Hello WorldBye"
// 기존 + 추가

3.4 append 모드의 동작

append 모드의 OS 레벨 동작:

1. 파일 열 때
   - O_APPEND 플래그 전달
   - OS 가 "쓰기 위치 = 파일 끝" 설정

2. 매 write 시
   - 자동으로 파일 끝으로 이동
   - 동시에 다른 프로세스가 써도 안전
   - 원자적 (Atomic) — 작은 쓰기는 보장

특징:
  - 멀티 프로세스 로깅에 안전
  - position 변경 무관 (항상 끝)
  - 큰 쓰기는 원자성 보장 X

3.5 활용 시나리오

// 시나리오 1: 로그 파일
public void log(String message) throws IOException {
    try (FileOutputStream fos = new FileOutputStream("app.log", true)) {
        // ★ append 필수
        // 매 호출마다 누적
        fos.write((message + "\n").getBytes());
    }
}

// 시나리오 2: 설정 파일 (덮어쓰기)
public void saveConfig(Properties props) throws IOException {
    try (FileOutputStream fos = new FileOutputStream("config.properties")) {
        // 덮어쓰기 (최신 설정으로)
        props.store(fos, "Configuration");
    }
}

// 시나리오 3: CSV export (덮어쓰기)
public void exportCsv(List<Shipment> shipments, Path dest) throws IOException {
    try (FileOutputStream fos = new FileOutputStream(dest.toFile())) {
        // 새 export — 덮어쓰기
        fos.write("id,blNo,weight\n".getBytes());
        for (Shipment s : shipments) {
            fos.write(s.toCsvLine().getBytes());
            fos.write("\n".getBytes());
        }
    }
}

// 시나리오 4: 메시지 큐 파일
public void enqueue(String message) throws IOException {
    try (FileOutputStream fos = new FileOutputStream("queue.log", true)) {
        // append — 메시지 누적
        fos.write((message + "\n").getBytes());
    }
}

3.6 append 의 함정

// 함정 1: 모드 잊음
try (FileOutputStream fos = new FileOutputStream("important.log")) {
    // ★ append=true 누락
    fos.write(newLog.getBytes());
}
// 기존 로그 모두 삭제!

// 함정 2: 동시성
// 두 스레드가 같은 파일에 append
Thread t1 = new Thread(() -> log("A"));
Thread t2 = new Thread(() -> log("B"));

// O_APPEND 는 작은 쓰기는 원자적
// 하지만 큰 데이터 (> 4KB) 는 섞일 수 있음
// 해결: FileLock 또는 단일 스레드

// 함정 3: 무한 누적
try (FileOutputStream fos = new FileOutputStream("log.txt", true)) {
    while (true) {
        fos.write(...);
    }
}
// 파일이 무한히 커짐
// 로그 회전 (rotation) 필요

3.7 NIO.2 의 append

// NIO.2 — 명시적
Files.newOutputStream(Path.of("file.txt"),
    StandardOpenOption.CREATE,
    StandardOpenOption.WRITE,
    StandardOpenOption.APPEND);

// 또는 Files.write
Files.write(Path.of("file.txt"), data,
    StandardOpenOption.CREATE, StandardOpenOption.APPEND);

// 더 간결
Files.writeString(Path.of("file.txt"), "Hello\n",
    StandardOpenOption.CREATE, StandardOpenOption.APPEND);

3.8 ILIC 의 로그 회전

@Component
public class ShipmentLogRotator {
    
    private final Path baseDir = Path.of("/var/log/shipment");
    private final long maxSize = 10 * 1024 * 1024;   // 10MB
    
    public void appendLog(String level, String message) throws IOException {
        Path current = getCurrentLogFile();
        
        // 회전 체크
        if (Files.exists(current) && Files.size(current) > maxSize) {
            rotate(current);
        }
        
        // append
        String line = String.format("%s [%s] %s%n",
            LocalDateTime.now(), level, message);
        Files.writeString(current, line,
            StandardCharsets.UTF_8,
            StandardOpenOption.CREATE,
            StandardOpenOption.APPEND);
    }
    
    private Path getCurrentLogFile() {
        return baseDir.resolve("app.log");
    }
    
    private void rotate(Path current) throws IOException {
        Path archive = baseDir.resolve("archive")
            .resolve("app." + System.currentTimeMillis() + ".log");
        Files.createDirectories(archive.getParent());
        Files.move(current, archive);
    }
}

3.9 자기 점검 답변

덮어쓰기 vs 이어쓰기의 차이는?

:
1. 덮어쓰기 (기본):

  • 기존 내용 truncate
  • 처음부터 쓰기
  • new FileOutputStream(path)
  1. 이어쓰기 (append):

    • 기존 유지
    • 끝에 추가
    • new FileOutputStream(path, true)
  2. OS 레벨:

    • append = O_APPEND 플래그
    • 매 write 마다 자동 끝 이동
    • 작은 쓰기는 원자적
  3. 활용:

    • 로그: append
    • 설정/Export: 덮어쓰기
    • 메시지 큐: append
  4. NIO.2:

    • StandardOpenOption.APPEND

4️⃣ write 메서드 종류

4.1 3가지 write 메서드

public abstract class OutputStream {
    
    // 1. 1바이트 쓰기 (추상)
    public abstract void write(int b) throws IOException;
    
    // 2. 배열 전체 쓰기
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }
    
    // 3. 배열 일부 쓰기
    public void write(byte[] b, int off, int len) throws IOException;
}

4.2 write(int b) 의 정밀

// write(int b) 의 동작
public void write(int b) throws IOException {
    // b 의 하위 8비트만 사용
    // 즉, b & 0xFF
    // 0 ~ 255 의 값으로 변환되어 쓰임
}

// 예
fos.write(65);          // 'A' (0x41)
fos.write(72);          // 'H' (0x48)
fos.write('A');         // 'A' (char 가 int 로 자동 변환, 65)
fos.write(0x100);       // ★ 0 (하위 8비트만, 0x100 의 하위 = 0x00)
fos.write(-1);          // ★ 255 (0xFF, 음수도 하위 8비트만)

// 즉:
// int 의 24비트는 무시
// 0~255 범위만 의미 있음

4.3 write(byte[]) — 배열 전체

byte[] data = "Hello".getBytes();
fos.write(data);
// 5바이트 모두 쓰기

// 내부 구현
public void write(byte[] b) throws IOException {
    write(b, 0, b.length);
}

// 효율적 — 한 번에 여러 바이트
// system call 1번 (또는 적게)

4.4 write(byte[], off, len) — 배열 일부

byte[] buf = "Hello World".getBytes();
fos.write(buf, 6, 5);
// buf[6] ~ buf[10] 쓰기
// "World"

// 활용:
// - 큰 버퍼의 일부만
// - 헤더 + 본문 분리
// - 정확한 길이 제어

4.5 read 와 write 의 짝

// 일반적 복사 패턴
try (FileInputStream fis = new FileInputStream("src.txt");
     FileOutputStream fos = new FileOutputStream("dest.txt")) {
    
    byte[] buf = new byte[8192];
    int n;
    while ((n = fis.read(buf)) != -1) {
        fos.write(buf, 0, n);   // ★ n 만큼만! (Unit 8.3)
    }
}

// ❌ 잘못 — buf.length 쓰기
while ((n = fis.read(buf)) != -1) {
    fos.write(buf);   // 항상 buf.length
    // 마지막 read 후 이전 데이터 누적
}

// ❌ 더 잘못
while ((n = fis.read(buf)) != -1) {
    fos.write(buf, 0, buf.length);   // 위와 같음
}

4.6 PrintStream 의 편의

// PrintStream (System.out 의 타입)
// OutputStream + 편의 메서드

PrintStream ps = new PrintStream(new FileOutputStream("file.txt"));
ps.print("Hello");
ps.println(" World");
ps.printf("Value: %d%n", 42);

// 편의:
// - 문자열 자동 인코딩
// - 줄바꿈 자동
// - printf

// 단점:
// - 예외 던지지 않음 (checkError 로 확인)
// - 인코딩 명시 필요

4.7 write 의 Blocking

// 일반 파일: 거의 즉시 (디스크 버퍼)
fos.write(data);   // 빠름

// 네트워크 소켓: 네트워크 버퍼 가득 차면 대기
socket.getOutputStream().write(data);   // 느릴 수 있음

// 큰 데이터:
// - OS 의 페이지 캐시에 일단 저장
// - 디스크 동기화는 백그라운드
// - flush() 또는 force() 로 강제 가능

4.8 ILIC 의 활용

public class ShipmentDataWriter {
    
    // 1. 단일 바이트
    public void writeStatus(Path path, byte status) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
            fos.write(status);
        }
    }
    
    // 2. 배열
    public void writeData(Path path, byte[] data) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
            fos.write(data);
        }
    }
    
    // 3. 부분 쓰기
    public void writeHeader(Path path, byte[] fullData, int headerLen) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
            fos.write(fullData, 0, headerLen);   // 헤더만
        }
    }
    
    // 4. 복사 (read + write)
    public void copy(Path src, Path dest) throws IOException {
        try (FileInputStream fis = new FileInputStream(src.toFile());
             FileOutputStream fos = new FileOutputStream(dest.toFile())) {
            byte[] buf = new byte[8192];
            int n;
            while ((n = fis.read(buf)) != -1) {
                fos.write(buf, 0, n);
            }
        }
    }
    
    // 5. 더 간단 (Java 9+)
    public void copyEasy(Path src, Path dest) throws IOException {
        try (InputStream is = Files.newInputStream(src);
             OutputStream os = Files.newOutputStream(dest)) {
            is.transferTo(os);
        }
    }
}

4.9 자기 점검 답변

write 메서드 3가지의 차이는?

:
1. write(int b):

  • 1바이트 쓰기
  • 하위 8비트만 (b & 0xFF)
  • 0~255 범위
  1. write(byte[] b):

    • 배열 전체
    • 내부적으로 write(b, 0, b.length)
  2. write(byte[] b, int off, int len):

    • 배열 일부
    • off 부터 len 바이트
    • read 와 짝: write(buf, 0, n)

주의:

  • 마지막 read 후 write(buf) 는 위험
  • write(buf, 0, n) 가 안전

5️⃣ flush() 의 역할

5.1 flush 의 정의

public void flush() throws IOException;

핵심:

  • 버퍼링된 데이터를 강제 출력
  • close() 가 자동으로 호출
  • 명시적 호출은 특수한 경우만

5.2 버퍼링이 왜 있나?

버퍼링 (Buffering):

  write 호출마다 OS 호출 하지 않고
  내부 버퍼에 일단 저장.
  버퍼 가득 차면 한 번에 OS 로.

이유:
  - system call 비용 절감
  - 디스크 I/O 효율
  - 작은 write 의 누적 처리

단점:
  - 버퍼 안의 데이터는 디스크에 X
  - 비정상 종료 시 손실
  - 강제 출력 필요 시 flush()

5.3 FileOutputStream 의 버퍼링

FileOutputStream 자체는 버퍼링 X
  - 매 write 가 OS 호출
  - flush() 의 효과 거의 없음

BufferedOutputStream 이 버퍼링
  - 내부 8KB 버퍼
  - 매 write 가 버퍼에 저장
  - 버퍼 가득 또는 flush() 시 OS 호출

PrintStream/PrintWriter 도 보통 버퍼링
  - System.out 은 line buffering (줄 단위)
  - println 마다 자동 flush 가능

5.4 시나리오 비교

// 시나리오 1: FileOutputStream — flush 무의미
try (FileOutputStream fos = new FileOutputStream("file.txt")) {
    fos.write(data);
    fos.flush();   // 효과 거의 없음 (이미 OS 로)
}

// 시나리오 2: BufferedOutputStream — flush 중요
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    bos.write(data);
    // 데이터가 8KB 버퍼에 있음
    // 디스크엔 아직 X
    
    bos.flush();   // ★ 디스크에 쓰기
    // 또는 close() 가 자동
}

// 시나리오 3: 진행 중 강제 출력
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    for (int i = 0; i < 100; i++) {
        bos.write(generateData(i));
        bos.flush();   // 매 데이터마다 디스크
        // 비정상 종료 시 데이터 보존
    }
}

5.5 flush vs force (FileChannel)

// flush — 자바 버퍼 → OS 버퍼
bos.flush();
// OS 의 페이지 캐시에 있을 뿐
// 디스크엔 아직 안 갔을 수도

// force — OS 버퍼 → 디스크
FileChannel ch = fos.getChannel();
ch.force(true);
// 또는 false (메타데이터 제외)

// 즉:
// flush: 자바 → OS
// force: OS → 디스크
// 둘 다 거쳐야 진짜 디스크

5.6 close 와 flush

// close 가 자동 flush
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    bos.write(data);
    // try 블록 끝 → close() 호출
    // close() 가 내부적으로 flush() 호출
}

// 명시적 flush
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    bos.write(data);
    bos.flush();   // 즉시 OS 로
    // 이후도 사용 가능
    bos.write(moreData);
}

5.7 PrintStream 의 autoFlush

// PrintStream 의 autoFlush 옵션
PrintStream ps = new PrintStream(new FileOutputStream("file.txt"), true);
//                                                                    ↑ autoFlush

// autoFlush = true:
// - println 후 자동 flush
// - byte 배열 쓴 후 자동 flush

// 일반:
PrintStream ps2 = new PrintStream(new FileOutputStream("file.txt"));
// autoFlush = false (기본)

// System.out 의 경우:
// - 일반 PrintStream
// - autoFlush 보장 없음
// - 단, 콘솔은 보통 line-buffered (\n 마다 flush)

5.8 ILIC 의 활용

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

// 또는 더 강력하게 (force)
public class CriticalLogger {
    
    public void logCritical(String message, Path path) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(path.toFile(), true)) {
            fos.write(message.getBytes());
            // FileOutputStream 자체는 버퍼링 X
            
            // 강제 디스크 동기화
            fos.getChannel().force(false);   // 데이터만 (메타 제외)
        }
    }
}

5.9 자기 점검 답변

flush() 의 역할과 언제 호출?

:
1. 역할:

  • 자바 버퍼 → OS 로 강제 출력
  • 버퍼링 스트림 (Buffered*) 에서 의미
  1. FileOutputStream:

    • 자체 버퍼링 X
    • flush 효과 거의 없음
  2. BufferedOutputStream:

    • 8KB 버퍼
    • flush 필수일 때
  3. 호출 시점:

    • 진행 중 즉시 출력 (로그, 트랜잭션)
    • 비정상 종료 대비
    • close() 가 자동 flush
  4. flush vs force:

    • flush: 자바 → OS
    • force: OS → 디스크
    • 진짜 디스크는 force 필요

6️⃣ 바이트가 문자로 보이는 이유

6.1 파일에 저장되는 것

사실: 파일은 바이트 (숫자) 의 나열.

저장:
  fos.write(65);
  → 파일에 0x41 (1바이트) 저장

파일 내용 (16진수):
  0x41

해석:
  - ASCII 로: 'A'
  - 16진수: 65
  - 10진수: 65
  - 이진수: 01000001
  
  모두 같은 바이트, 다른 표현

6.2 텍스트 에디터의 역할

텍스트 에디터:
  - 파일의 바이트 읽음
  - 인코딩 매핑 적용
  - 문자로 화면에 표시

예:
  파일 바이트: 0x48 0x65 0x6C 0x6C 0x6F
  
  ASCII/UTF-8 매핑:
  - 0x48 → 'H' (72)
  - 0x65 → 'e' (101)
  - 0x6C → 'l' (108)
  - 0x6C → 'l' (108)
  - 0x6F → 'o' (111)
  
  화면: "Hello"

6.3 영어가 잘 보이는 이유

영어 (ASCII):
  - 모든 알파벳이 1바이트 (0~127)
  - ASCII 표준
  - UTF-8 호환

write(65) → 파일 0x41 → 텍스트 에디터 'A'

영어가 잘 되는 이유:
  - ASCII 가 산업 표준
  - 모든 시스템이 인식
  - 1바이트 매핑 = 단순

6.4 ASCII 코드 표

주요 ASCII 코드:

   65 = 'A'      97 = 'a'
   66 = 'B'      98 = 'b'
   ...           ...
   90 = 'Z'     122 = 'z'

   48 = '0'      32 = ' ' (공백)
   49 = '1'      10 = '\n' (LF)
   ...           13 = '\r' (CR)
   57 = '9'       9 = '\t' (Tab)

특수:
   33 = '!'      63 = '?'
   64 = '@'      35 = '#'

검증:
  fos.write(65);   // 파일에 'A'
  fos.write(97);   // 파일에 'a'
  fos.write(48);   // 파일에 '0'
  fos.write(32);   // 파일에 공백

6.5 시각화

// "Hello, World!" 쓰기
try (FileOutputStream fos = new FileOutputStream("file.txt")) {
    fos.write(72);    // 'H'
    fos.write(101);   // 'e'
    fos.write(108);   // 'l'
    fos.write(108);   // 'l'
    fos.write(111);   // 'o'
    fos.write(44);    // ','
    fos.write(32);    // ' '
    fos.write(87);    // 'W'
    fos.write(111);   // 'o'
    fos.write(114);   // 'r'
    fos.write(108);   // 'l'
    fos.write(100);   // 'd'
    fos.write(33);    // '!'
}

// 파일 (hex dump):
// 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21

// 텍스트 에디터 (UTF-8/ASCII):
// "Hello, World!"

6.6 String.getBytes 의 동작

// String → byte[] 의 매핑
String s = "Hello";
byte[] bytes = s.getBytes();
// 인코딩 사용 (시스템 기본 또는 명시)

// UTF-8
byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
// [72, 101, 108, 108, 111]
// 영어는 UTF-8 도 1바이트씩

// 이를 write:
fos.write(utf8);
// 파일: 0x48 0x65 0x6C 0x6C 0x6F
// 텍스트 에디터: "Hello"

6.7 텍스트 에디터의 인코딩 인식

텍스트 에디터의 동작:

1. 파일 열기
2. 바이트 시퀀스 읽음
3. 인코딩 추정 또는 사용자 선택
4. 매핑 적용
5. 화면에 문자 표시

추정 방법:
  - BOM 확인 (UTF-8 BOM, UTF-16 BOM)
  - 통계적 분석
  - 사용자 설정

잘못 추정하면:
  - 한글이 깨짐
  - 영어는 정상 (ASCII 호환)
  - 사용자가 인코딩 변경

6.8 ILIC 의 활용

public class TextFileGenerator {
    
    // ASCII 안전한 텍스트
    public void writeAsciiText(Path path, String text) throws IOException {
        // ASCII 만 (영어 + 숫자 + 기본 기호)
        try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
            for (char c : text.toCharArray()) {
                if (c < 128) {   // ASCII 범위
                    fos.write(c);
                } else {
                    throw new IOException("Non-ASCII character: " + c);
                }
            }
        }
    }
    
    // 바이트 직접 (영어 텍스트)
    public void writeBytes(Path path, byte[] bytes) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
            fos.write(bytes);
        }
    }
    
    // 검증
    public void verify(Path path) throws IOException {
        byte[] bytes = Files.readAllBytes(path);
        
        System.out.println("File bytes (hex):");
        for (byte b : bytes) {
            System.out.printf("%02X ", b);
        }
        System.out.println();
        
        System.out.println("Interpreted as ASCII:");
        System.out.println(new String(bytes, StandardCharsets.US_ASCII));
    }
}

6.9 자기 점검 답변

바이트가 문자로 보이는 이유는?

:
1. 파일의 본질:

  • 바이트 (숫자) 의 나열
  • 0x48 = 72 = 'H' (ASCII)
  • 같은 바이트, 다른 표현
  1. 텍스트 에디터:

    • 바이트 읽음
    • 인코딩 매핑
    • 문자로 표시
  2. ASCII:

    • 0~127 의 표준 매핑
    • 영어, 숫자, 기본 기호
    • 모든 시스템에서 인식
  3. 영어가 잘 되는 이유:

    • ASCII = UTF-8 의 일부
    • 1바이트 = 1문자
    • 깨짐 X
  4. 한글이 안 되는 이유 (다음 섹션):

    • 1바이트로 표현 불가
    • 여러 바이트 필요
    • write(int) 로는 부족

7️⃣ 한글이 깨지는 메커니즘

7.1 한글 1글자의 크기

한글 1글자의 인코딩별 크기:

UTF-8: 3바이트
  "안" → 0xEC 0x95 0x88
  "녕" → 0xEB 0x85 0x95

UTF-16: 2바이트
  "안" → 0xC5 0x48
  "녕" → 0xB1 0x4D

EUC-KR / CP949: 2바이트
  "안" → 0xBE 0xC8
  "녕" → 0xB3 0xC4

→ 한글은 절대 1바이트 X
→ write(int) 로 한 번에 못 씀

7.2 잘못된 쓰기 시도

// ❌ 한글 1글자를 1바이트씩
try (FileOutputStream fos = new FileOutputStream("file.txt")) {
    fos.write('안');   // ★ 잘못!
}

// 동작:
// 1. '안' = char (16비트)
// 2. fos.write(int) 가 int 의 하위 8비트만
// 3. '안' = 0xC548
// 4. 하위 8비트 = 0x48 ('H' 와 같은 바이트)
// 5. 파일에 0x48 (1바이트) 저장

// 결과: '안' 이 'H' 로 저장
// 데이터 손실 + 깨짐

7.3 올바른 쓰기 — byte[]

// ✓ 한글을 byte[] 로
try (FileOutputStream fos = new FileOutputStream("file.txt")) {
    byte[] utf8 = "안녕".getBytes(StandardCharsets.UTF_8);
    fos.write(utf8);
    // 6바이트 모두 정확히 쓰임
}

// 파일 (hex):
// EC 95 88 EB 85 95

// UTF-8 로 열기:
// "안녕"

// EUC-KR 로 열기:
// 깨짐

7.4 인코딩별 결과

String text = "안녕";

// UTF-8 (현대 권장)
byte[] utf8 = text.getBytes(StandardCharsets.UTF_8);
// [0xEC, 0x95, 0x88, 0xEB, 0x85, 0x95]
// 6바이트

// EUC-KR (옛 한국)
byte[] euckr = text.getBytes(Charset.forName("EUC-KR"));
// [0xBE, 0xC8, 0xB3, 0xC4]
// 4바이트

// UTF-16
byte[] utf16 = text.getBytes(StandardCharsets.UTF_16);
// [0xFE, 0xFF, 0xC5, 0x48, 0xB1, 0x4D] (with BOM)
// 6바이트

7.5 쓰기 ↔ 읽기 인코딩 일치

// 쓰기 (UTF-8)
try (FileOutputStream fos = new FileOutputStream("file.txt")) {
    fos.write("안녕".getBytes(StandardCharsets.UTF_8));
}

// 읽기 — 같은 인코딩
try (FileInputStream fis = new FileInputStream("file.txt")) {
    byte[] bytes = fis.readAllBytes();
    String text = new String(bytes, StandardCharsets.UTF_8);   // ✓
    // text = "안녕"
}

// 읽기 — 다른 인코딩
try (FileInputStream fis = new FileInputStream("file.txt")) {
    byte[] bytes = fis.readAllBytes();
    String text = new String(bytes, Charset.forName("EUC-KR"));   // ❌
    // text = 깨진 문자
}

7.6 권장 패턴 — Writer 사용

// ❌ FileOutputStream 직접 — 매번 인코딩 처리
try (FileOutputStream fos = new FileOutputStream("file.txt")) {
    fos.write("안녕".getBytes(StandardCharsets.UTF_8));
    fos.write("\n".getBytes());
    fos.write("하세요".getBytes(StandardCharsets.UTF_8));
}

// ✓ Writer 활용 — 인코딩 자동
try (Writer writer = new OutputStreamWriter(
        new FileOutputStream("file.txt"),
        StandardCharsets.UTF_8)) {
    writer.write("안녕\n");
    writer.write("하세요");
}

// ✓✓ BufferedWriter — 효율
try (BufferedWriter writer = new BufferedWriter(
        new OutputStreamWriter(
            new FileOutputStream("file.txt"),
            StandardCharsets.UTF_8))) {
    writer.write("안녕");
    writer.newLine();
    writer.write("하세요");
}

// ✓✓✓ NIO.2 (가장 권장)
try (BufferedWriter writer = Files.newBufferedWriter(
        Path.of("file.txt"),
        StandardCharsets.UTF_8)) {
    writer.write("안녕");
    writer.newLine();
    writer.write("하세요");
}

7.7 BufferedReader/Writer 의 한글 처리

// 한글 안전 패턴 (Unit 8.5, 8.6 에서 정밀)
public void writeKoreanSafe(Path path, String text) throws IOException {
    try (BufferedWriter writer = Files.newBufferedWriter(path,
            StandardCharsets.UTF_8)) {
        writer.write(text);
    }
}

public String readKoreanSafe(Path path) throws IOException {
    try (BufferedReader reader = Files.newBufferedReader(path,
            StandardCharsets.UTF_8)) {
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line).append('\n');
        }
        return sb.toString();
    }
}

7.8 ILIC 의 한글 처리

public class ShipmentKoreanWriter {
    
    private static final Charset CHARSET = StandardCharsets.UTF_8;
    
    // CSV 한글 포함
    public void writeCsv(Path path, List<Shipment> shipments) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(path, CHARSET)) {
            // 헤더
            writer.write("ID,선적번호,수하인이름,중량(kg)\n");
            
            // 데이터
            for (Shipment s : shipments) {
                writer.write(s.getId() + ",");
                writer.write(s.getBlNo() + ",");
                writer.write(s.getConsigneeName() + ",");   // 한글 OK
                writer.write(s.getWeight() + "\n");
            }
        }
    }
    
    // JSON 한글
    public void writeJson(Path path, Shipment s) throws IOException {
        String json = String.format(
            "{\"id\":%d,\"수하인\":\"%s\",\"비고\":\"%s\"}",
            s.getId(), s.getConsigneeName(), s.getNotes());
        
        Files.writeString(path, json, CHARSET);
    }
    
    // 로그 (한글 + 영문)
    public void log(Path logFile, String level, String message) throws IOException {
        String line = String.format("[%s] %s %s%n",
            LocalDateTime.now(), level, message);
        Files.writeString(logFile, line, CHARSET,
            StandardOpenOption.CREATE, StandardOpenOption.APPEND);
    }
}

7.9 자기 점검 답변

한글이 깨지는 메커니즘은?

:
1. 한글 = 다바이트:

  • UTF-8: 3바이트
  • EUC-KR: 2바이트
  • 절대 1바이트 X
  1. write(int) 의 한계:

    • 1바이트만
    • 한글 char 의 하위 8비트만 쓰임
    • 데이터 손실
  2. 올바른 방법:

    • byte[] utf8 = text.getBytes(UTF_8)
    • fos.write(utf8)
    • 또는 Writer 사용
  3. 권장:

    • OutputStreamWriter + 인코딩
    • Files.newBufferedWriter
    • 자동 인코딩 처리
  4. 인코딩 일치:

    • 쓰기/읽기 같은 인코딩
    • UTF-8 권장

8️⃣ 파일 생성과 부모 디렉토리 함정

8.1 자동 생성되는 것

// FileOutputStream 이 자동으로:
// 1. 파일 없으면 생성 (빈 파일)
// 2. 파일 있으면 truncate (덮어쓰기 모드) 또는 append

new FileOutputStream("file.txt");
// "file.txt" 가 없으면 생성, 있으면 truncate

8.2 자동 생성 안 되는 것

// ❌ 부모 디렉토리는 자동 생성 X
new FileOutputStream("/var/data/2026/05/file.txt");
// /var/data/2026/05 가 없으면 ★ FileNotFoundException

// 에러 메시지:
// java.io.FileNotFoundException: /var/data/2026/05/file.txt 
//   (No such file or directory)

8.3 함정 시나리오

// 자주 발생하는 함정

// 시나리오 1: 날짜별 디렉토리
public void writeLog(String message) throws IOException {
    LocalDate today = LocalDate.now();
    String path = String.format("/var/log/%d/%02d/%02d/app.log",
        today.getYear(), today.getMonthValue(), today.getDayOfMonth());
    
    // ❌ 부모 디렉토리 누락
    try (FileOutputStream fos = new FileOutputStream(path, true)) {
        fos.write(message.getBytes());
    }
    // FileNotFoundException 가능
}

// 시나리오 2: 업로드 처리
public void saveUpload(String userId, byte[] data) throws IOException {
    String path = "/var/uploads/" + userId + "/file.dat";
    
    // ❌ /var/uploads/<userId> 없으면 실패
    try (FileOutputStream fos = new FileOutputStream(path)) {
        fos.write(data);
    }
}

8.4 올바른 패턴

// ✓ 부모 디렉토리 미리 생성
public void writeLogSafe(String message) throws IOException {
    LocalDate today = LocalDate.now();
    Path path = Path.of("/var/log",
        String.valueOf(today.getYear()),
        String.format("%02d", today.getMonthValue()),
        String.format("%02d", today.getDayOfMonth()),
        "app.log");
    
    // 부모 디렉토리 보장
    Files.createDirectories(path.getParent());
    
    try (FileOutputStream fos = new FileOutputStream(path.toFile(), true)) {
        fos.write(message.getBytes());
    }
}

// ✓ File 활용
public void writeLogFile(String message) throws IOException {
    File file = new File("/var/log/2026/05/19/app.log");
    
    // 부모 디렉토리 생성
    File parent = file.getParentFile();
    if (parent != null && !parent.exists()) {
        if (!parent.mkdirs()) {
            throw new IOException("Cannot create dir: " + parent);
        }
    }
    
    try (FileOutputStream fos = new FileOutputStream(file, true)) {
        fos.write(message.getBytes());
    }
}

// ✓ NIO.2 (가장 권장)
public void writeLogModern(String message) throws IOException {
    Path path = Path.of("/var/log/2026/05/19/app.log");
    
    Files.createDirectories(path.getParent());
    Files.writeString(path, message + "\n",
        StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}

8.5 createNewFile 비교

// File.createNewFile
File file = new File("/var/data/file.txt");
file.createNewFile();   // 부모 없으면 IOException

// FileOutputStream
new FileOutputStream("/var/data/file.txt");
// 부모 없으면 FileNotFoundException

// 둘 다 부모 디렉토리 자동 생성 X
// mkdirs() 또는 createDirectories() 필요

8.6 권한 함정

// 권한 없는 경로
new FileOutputStream("/root/file.txt");
// FileNotFoundException
// "Permission denied"

// 읽기 전용 파일 시스템
new FileOutputStream("/proc/test.txt");
// IOException

// 검증 후 시도
public boolean canWrite(Path path) {
    Path parent = path.getParent();
    return parent != null && Files.isWritable(parent);
}

8.7 ILIC 의 안전한 패턴

@Component
public class SafeFileWriter {
    
    private final Path baseDir = Path.of("/var/shipment");
    
    // 1. 안전한 파일 생성
    public void writeShipmentFile(String relativePath, byte[] data) throws IOException {
        Path file = baseDir.resolve(relativePath).normalize();
        
        // Path Traversal 방어
        if (!file.startsWith(baseDir)) {
            throw new SecurityException("Invalid path: " + relativePath);
        }
        
        // 부모 디렉토리 생성
        Files.createDirectories(file.getParent());
        
        // 권한 확인
        if (!Files.isWritable(file.getParent())) {
            throw new IOException("Cannot write to: " + file.getParent());
        }
        
        // 쓰기
        Files.write(file, data);
    }
    
    // 2. 날짜별 로그
    public void writeDateLog(String level, String message) throws IOException {
        LocalDate today = LocalDate.now();
        Path file = baseDir.resolve("logs")
            .resolve(String.valueOf(today.getYear()))
            .resolve(String.format("%02d", today.getMonthValue()))
            .resolve(String.format("%02d", today.getDayOfMonth()))
            .resolve(level.toLowerCase() + ".log");
        
        Files.createDirectories(file.getParent());
        
        String line = String.format("[%s] %s%n",
            LocalDateTime.now(), message);
        Files.writeString(file, line, StandardCharsets.UTF_8,
            StandardOpenOption.CREATE, StandardOpenOption.APPEND);
    }
    
    // 3. 업로드 안전 처리
    public Path saveUpload(String userId, String filename, byte[] data) 
            throws IOException {
        // 사용자 디렉토리
        Path userDir = baseDir.resolve("uploads").resolve(userId);
        Files.createDirectories(userDir);
        
        // 안전한 파일명 (시간스탬프 + 원본)
        String safeFilename = System.currentTimeMillis() + "_" + sanitize(filename);
        Path target = userDir.resolve(safeFilename);
        
        // 쓰기
        Files.write(target, data,
            StandardOpenOption.CREATE_NEW);   // 새 파일만 (중복 방지)
        
        return target;
    }
    
    private String sanitize(String filename) {
        return filename.replaceAll("[^a-zA-Z0-9.가-힣_-]", "_");
    }
}

8.8 자기 점검 답변

파일 생성과 부모 디렉토리 함정은?

:
1. 자동 생성:

  • 파일: ✓
  • 부모 디렉토리: ✗
  1. 부모 디렉토리 없으면:

    • FileNotFoundException
    • "No such file or directory"
  2. 해결:

    • Files.createDirectories(file.getParent())
    • file.getParentFile().mkdirs()
    • 미리 보장
  3. 자주 발생하는 곳:

    • 날짜별 로그
    • 업로드 처리
    • 사용자별 디렉토리
  4. 추가 안전:

    • Path Traversal 방어
    • 권한 확인
    • CREATE_NEW (중복 방지)

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
FileOutputStream 정의?OutputStream 의 파일 전용
5가지 생성자?String, File, FileDescriptor + append 변형
덮어쓰기 vs append?truncate vs 끝에 추가
new FileOutputStream(path, true)?append 모드
write(int b) 의 동작?하위 8비트만, 0~255
write(byte[], 0, n) 의 이유?마지막 read 의 n < buf.length
flush() 의 역할?버퍼 → OS 강제
FileOutputStream 의 flush?자체 버퍼링 X, 효과 없음
BufferedOutputStream 의 flush?8KB 버퍼 → OS
flush vs force?자바→OS vs OS→디스크
바이트가 영어로?ASCII/UTF-8 매핑
한글 1바이트 쓰면?깨짐 (3바이트 필요)
부모 디렉토리 자동 생성?X, mkdirs 필요

9.2 자기 점검 체크리스트

기본

  • FileOutputStream 의 정의
  • OutputStream 계층
  • 5가지 생성자

append 모드

  • 덮어쓰기 vs append
  • OS 의 O_APPEND
  • NIO.2 의 StandardOpenOption.APPEND

write

  • write(int b) 의 1바이트 한계
  • write(byte[])
  • write(byte[], off, len)
  • write(buf, 0, n) 패턴

flush

  • flush 의 역할
  • FileOutputStream vs BufferedOutputStream
  • flush vs force
  • close 와의 관계

인코딩

  • 바이트가 문자로 보이는 이유 (ASCII)
  • 한글이 깨지는 이유 (다바이트)
  • Writer 사용 권장
  • StandardCharsets.UTF_8

함정

  • 부모 디렉토리 자동 생성 X
  • mkdirs / createDirectories
  • 권한 확인
  • Path Traversal

9.3 추가 심화 질문

Q1: write 후 즉시 다른 프로세스가 읽으면?

답:

  • 데이터가 OS 버퍼에만 있을 수 있음
  • 디스크 동기화 안 됐을 수도
  • 일반적으로 OS 가 일관성 보장 (같은 머신)
  • 다른 머신 (NFS 등) 은 force 필요
fos.write(data);
fos.flush();                  // 자바 → OS
fos.getChannel().force(true); // OS → 디스크

Q2: append 모드의 원자성?

답:

  • O_APPEND 는 작은 쓰기 원자적
  • PIPE_BUF (보통 4KB) 이하
  • 큰 쓰기는 분할 가능
  • 동시 쓰기 시 섞일 수 있음
  • 해결: FileLock 또는 단일 쓰레드

Q3: BufferedOutputStream + FileOutputStream 의 close 순서?

답:

try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    bos.write(data);
}
// close 순서:
// 1. bos.close() — flush + 자기 close
// 2. (내부) fos.close() — 파일 핸들 해제

// 외부 (BufferedOutputStream) 만 close 해도 OK
// 내부 (FileOutputStream) 도 close 됨

Q4: System.out 도 FileOutputStream 인가?

답:

  • System.out 은 PrintStream
  • 내부적으로 FileOutputStream (FileDescriptor.out) 활용
  • PrintStream 은 OutputStream 의 자식

Q5: 파일이 가득 차면 (디스크 풀)?

답:

  • write 가 IOException
  • "No space left on device"
  • 부분만 쓰일 수 있음
  • 처리:
    • try-with-resources 로 자원 정리
    • 사용자 알림
    • 임시 파일 정리

🎯 핵심 요약 — 3줄 정리

1. FileOutputStream

  • OutputStream 의 파일 전용
  • 5가지 생성자 (덮어쓰기/append)
  • write(int) 는 1바이트만 (한글 X)

2. write 와 flush

  • write(buf, 0, n) — read 와 짝
  • FileOutputStream 은 자체 버퍼링 X
  • BufferedOutputStream 의 flush 의미

3. 함정

  • 부모 디렉토리 자동 생성 X
  • 한글 = byte 사용
  • mkdirs 또는 Files.createDirectories 필수

📚 다음으로...

Unit 8.5 — 한글 처리 (FileReader, InputStreamReader)

이번 Unit에서 1바이트 쓰기의 한계를 봤다면, 다음은 한글 읽기.

  • FileReader 의 정의
  • InputStreamReader + 인코딩
  • 두 방식의 비교
  • 실무 권장 패턴

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 실전 (4/6 진행)

총: 35/43 Unit (약 81%)
profile
Software Developer

0개의 댓글