3주차 Unit 7.5 — 오버헤드와 File 객체

Psj·2026년 5월 19일

F-lab

목록 보기
105/240

Unit 7.5 — 오버헤드와 File 객체

F-LAB JAVA · 3주차 · Phase 7 · I/O 시스템 큰 그림
🏆 Phase 7 완주 — I/O 시스템 마스터


📌 학습 목표

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

  • 오버헤드 (Overhead) 의 정의와 종류는?
  • I/O 의 오버헤드 가 발생하는 4가지 지점은?
  • File 객체 의 정의와 주요 메서드는?
  • createNewFile, getAbsolutePath, getCanonicalPath, getName, getParent 의 정확한 차이는?
  • "디렉토리는 자동 생성되지 않는다" 가 어떤 버그를 만드나?
  • mkdir vs mkdirs 의 차이는?
  • File 의 한계 가 NIO.2 의 Path/Files 에서 어떻게 해결되나?
  • File → Path 마이그레이션 전략은?
  • Phase 7 의 모든 학습 종합은?

🎯 핵심 한 문장

오버헤드 (Overhead) 는 "본 작업 외에 추가로 드는 간접 비용" 이며, I/O 는 시스템 호출, 버퍼링, 직렬화, 보조 스트림 중첩 등 곳곳에서 오버헤드가 발생한다.
File 객체 는 Java 1.0 의 파일 추상화로 createNewFile, getAbsolutePath, getCanonicalPath, getName, getParent 등 메서드를 제공하지만,
boolean 반환의 진단 불가, 디렉토리 자동 생성 안 함, 심볼릭 링크 처리 X, 풍부한 속성 X 등 한계로 NIO.2 의 Path + Files 가 권장된다.
특히 file.createNewFile() 이 실패하는 가장 흔한 이유는 부모 디렉토리가 없어서mkdirs() 를 먼저 호출해야.
Phase 7 완주: I/O 의 정의 → IO/NIO/NIO.2 진화 → Stream/Channel 정밀 → 동시성 모델 → 실무 함정 의 5단계로 자바 I/O 의 큰 그림 완성.

비유 — 음식 배달의 오버헤드

본 작업: 음식 만들기 (5분)

오버헤드:
  - 주문 받기 (2분)
  - 포장 (1분)
  - 배달원 호출 (1분)
  - 배달 (15분)
  - 결제 처리 (1분)
  
  총: 25분
  본 작업: 5분 (20%)
  오버헤드: 20분 (80%)

I/O 의 오버헤드:
  본 작업: 데이터 읽기 (수 마이크로초)
  오버헤드:
  - 시스템 호출 (수 마이크로초)
  - 컨텍스트 스위칭
  - 버퍼 복사
  - 보조 스트림 중첩
  
  → I/O 자체가 오버헤드의 결정체

→ 오버헤드 = 본 작업 외의 모든 간접 비용.


🧭 9개 섹션 로드맵

1. 오버헤드의 정의와 종류
2. I/O 의 오버헤드가 발생하는 지점
3. File 객체의 정의와 구조
4. File 의 주요 메서드 정밀
5. "디렉토리는 자동 생성 안 됨" 함정
6. mkdir vs mkdirs, 그 외 함정들
7. File 의 한계 종합 + NIO.2 해결
8. Phase 7 완주 정리 + Phase 8 예고
9. 면접 + 자기 점검

1️⃣ 오버헤드의 정의와 종류

1.1 오버헤드의 정의

오버헤드 (Overhead):

  본 작업을 수행하는 데 필요하지만,
  본 작업 자체가 아닌 추가 비용.

종류:
  - 시간 (CPU 사이클)
  - 메모리 (버퍼, 객체)
  - 자원 (스레드, 소켓, 파일 핸들)
  - 네트워크 (대역폭)

예:
  - 10초 작업이 20초 걸림 → 오버헤드 10초
  - 1KB 데이터 처리에 10KB 메모리 → 9KB 오버헤드

1.2 오버헤드의 종류

시간 오버헤드:
  - 작업 시작 전 준비 (객체 생성, 초기화)
  - 작업 중 추가 처리 (변환, 검증)
  - 작업 종료 후 정리 (close, GC)

메모리 오버헤드:
  - 버퍼 크기
  - 객체 헤더
  - 캐시
  - 임시 데이터

자원 오버헤드:
  - 스레드 (메모리 + 컨텍스트 스위칭)
  - 파일 핸들
  - 소켓 연결
  - DB 연결

네트워크 오버헤드:
  - HTTP 헤더
  - TCP 헤더
  - TLS 핸드셰이크
  - 재전송

1.3 좋은 vs 나쁜 오버헤드

좋은 오버헤드 (피할 수 없음):
  - try-with-resources 의 자원 정리
  - 트랜잭션 관리
  - 보안 검증
  - 로깅 (적절히)

나쁜 오버헤드 (피할 수 있음):
  - 불필요한 객체 생성
  - 과도한 직렬화
  - 매번 새 Connection
  - 비효율적 알고리즘

1.4 오버헤드 측정

// 시간 오버헤드 측정
long start = System.nanoTime();
operation();
long elapsed = System.nanoTime() - start;
System.out.println("Took " + elapsed + " ns");

// 메모리 오버헤드 측정
Runtime runtime = Runtime.getRuntime();
long beforeMem = runtime.totalMemory() - runtime.freeMemory();
operation();
long afterMem = runtime.totalMemory() - runtime.freeMemory();
System.out.println("Used " + (afterMem - beforeMem) + " bytes");

// 프로파일링 도구
// - JProfiler, YourKit
// - JFR (Java Flight Recorder)
// - async-profiler

1.5 오버헤드의 합리성

오버헤드는 항상 나쁜가? — 아니다.

좋은 trade-off:
  - 버퍼링: 메모리 ↑, 속도 ↑↑ → 좋음
  - 캐시: 메모리 ↑, 시간 ↓↓ → 좋음
  - try-with-resources: 코드 ↑, 안전성 ↑↑ → 좋음

나쁜 trade-off:
  - 과도한 추상화: 복잡도 ↑, 효과 미미
  - 매번 새 Connection: 자원 ↑, 무의미

판단 기준:
  - 본 작업 대비 합리적 비율?
  - 가치를 더하는가?

1.6 ILIC 의 오버헤드 분석

// 예: ShipmentService.findById

@Transactional
public Shipment findById(Long id) {
    // 1. 트랜잭션 시작 — 오버헤드 (~1ms)
    // 2. JPA 의 EntityManager — 오버헤드 (~0.5ms)
    
    Shipment s = repository.findById(id).orElseThrow();
    // 3. SQL 실행 + 결과 매핑 — 본 작업 (~10ms)
    
    return s;
    // 4. 트랜잭션 종료 — 오버헤드 (~0.5ms)
}

// 분석:
// 본 작업: 10ms (DB 쿼리)
// 오버헤드: 2ms (트랜잭션, JPA)
// 비율: 오버헤드 20%
// 합리적

1.7 자기 점검 답변

오버헤드의 정의와 종류는?

:
1. 정의:

  • 본 작업 외의 추가 비용
  • 시간, 메모리, 자원, 네트워크
  1. 종류:

    • 시간: 시스템 호출, 컨텍스트 스위칭
    • 메모리: 버퍼, 객체 헤더
    • 자원: 스레드, Connection
    • 네트워크: 헤더, 핸드셰이크
  2. 합리성:

    • 좋은 오버헤드: 안전성, 성능
    • 나쁜 오버헤드: 불필요한 복잡도
  3. 측정:

    • System.nanoTime
    • 프로파일링 도구
    • JFR

2️⃣ I/O 의 오버헤드가 발생하는 지점

2.1 I/O 오버헤드의 4가지 지점

I/O 오버헤드 발생 지점:

1. 시스템 호출 (System Call)
   - JVM ↔ OS 전환 비용
   - 매 read/write 마다

2. 컨텍스트 스위칭
   - user space ↔ kernel space
   - 한 번에 ~1μs

3. 버퍼 복사
   - kernel buffer → user buffer
   - 또는 그 반대

4. 추상화 비용
   - Decorator 중첩
   - 객체 생성
   - 가상 메서드 호출

2.2 시스템 호출의 오버헤드

// 1바이트씩 읽기 — 매번 시스템 호출
FileInputStream fis = new FileInputStream("file.txt");
int b;
while ((b = fis.read()) != -1) {
    // 매 read() 가 read() 시스템 호출
    // 1MB 파일 = 1,048,576번 호출
    // 각 호출 ~1μs → 총 ~1초
}

// 8KB 단위 — 시스템 호출 ↓
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
    // 1MB / 8KB = 128번 호출
    // ~0.128ms
    // 시스템 호출 오버헤드 ~7,800배 절감
}

2.3 버퍼 복사의 오버헤드

일반 read 의 데이터 흐름:

디스크 → kernel buffer (DMA)
            ↓
       user buffer (CPU 복사)
            ↓
        JVM heap (Heap Buffer 인 경우 한 번 더)

매 복사가 메모리 대역폭 소모.

해결:
  - Direct Buffer: kernel ↔ direct buffer 만
  - zero-copy: kernel 내에서 직접 (transferTo)
  - Memory-mapped: 페이지 캐시 활용

2.4 추상화 비용

// Decorator 중첩의 비용
ObjectInputStream ois = new ObjectInputStream(
    new BufferedInputStream(
        new FileInputStream("data.dat")));

// 매 read():
// 1. ObjectInputStream.read()
// 2. → BufferedInputStream.read()
// 3. → FileInputStream.read()
// 4. → native read system call
// 5. → kernel
// 6. → 데이터 반환 (역순)

// 매 호출마다 4-5번의 메서드 호출
// JIT 가 인라이닝하지만 약간의 오버헤드

2.5 자원 관리의 오버헤드

// Connection 매번 생성 (안 좋음)
public void process() {
    Connection conn = DriverManager.getConnection(url);   // 매번 연결
    // ~100ms 비용
    // ...
    conn.close();
}

// Connection Pool 활용 (좋음)
public void process() {
    try (Connection conn = dataSource.getConnection()) {   // 풀에서 가져옴
        // ~0.1ms 비용
        // ...
    }
}

// 오버헤드 절감: ~1000배

2.6 직렬화의 오버헤드

// 직렬화는 비싼 작업
class Shipment implements Serializable {
    private Long id;
    private String blNo;
    // ...
}

// 오버헤드:
// 1. Reflection 기반 필드 접근 (느림)
// 2. 클래스 메타데이터 저장
// 3. transient 필드 처리
// 4. ObjectStream 자체의 오버헤드

// 측정:
// 작은 객체: ~10μs per 직렬화
// 큰 객체: ~ms

// 대안:
// - JSON (Jackson)
// - Protobuf, Avro
// - 더 효율적

2.7 네트워크의 오버헤드

HTTP 요청의 오버헤드:

1. DNS lookup: ~10ms
2. TCP 핸드셰이크: 1-RTT
3. TLS 핸드셰이크: 2-RTT
4. HTTP 헤더: ~500바이트
5. 응답 헤더: ~500바이트

작은 응답 (1KB):
  - 총 시간: ~100ms
  - 본 데이터: 1KB
  - 오버헤드 헤더: 1KB (100%)

큰 응답 (1MB):
  - 본 데이터: 1MB
  - 오버헤드: 1KB (~0.1%)

→ 큰 요청 1번 > 작은 요청 1000번

2.8 ILIC 의 I/O 오버헤드 최적화

public class ShipmentExportService {
    
    // ❌ 나쁜 패턴 — 매번 새 Connection
    public void exportAllBad() throws Exception {
        for (Shipment s : repository.findAll()) {
            Connection conn = DriverManager.getConnection(url);
            // 매번 ~100ms
            // ...
            conn.close();
        }
    }
    
    // ✓ 좋은 패턴 — Connection Pool + Buffered + Stream
    public void exportAllGood(Path dest) throws IOException {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement ps = conn.prepareStatement("SELECT * FROM shipments");
             ResultSet rs = ps.executeQuery();
             BufferedWriter writer = Files.newBufferedWriter(dest)) {
            
            writer.write("id,blNo,weight\n");
            
            while (rs.next()) {
                writer.write(rs.getLong("id") + ",");
                writer.write(rs.getString("bl_no") + ",");
                writer.write(rs.getBigDecimal("weight") + "\n");
            }
        }
        // 한 Connection, 한 Statement, 한 ResultSet
        // BufferedWriter 로 디스크 I/O ↓
    }
    
    // ✓✓ 더 좋은 패턴 — Streaming + Bulk Insert
    @Transactional
    public void importBulk(List<Shipment> shipments) {
        // 1000개씩 묶어서
        for (List<Shipment> chunk : Lists.partition(shipments, 1000)) {
            repository.saveAll(chunk);
        }
        // saveAll 이 내부적으로 batch insert
    }
}

2.9 자기 점검 답변

I/O 오버헤드가 발생하는 4가지 지점은?

:
1. 시스템 호출:

  • JVM ↔ OS 전환
  • 매 read/write
  • Buffered 로 절감
  1. 컨텍스트 스위칭:

    • user ↔ kernel space
    • ~1μs
  2. 버퍼 복사:

    • kernel ↔ user
    • Direct Buffer 또는 zero-copy 로 절감
  3. 추상화 비용:

    • Decorator 중첩
    • Reflection (직렬화)
    • JIT 가 일부 절감

최적화:

  • Buffered + byte[]
  • Direct Buffer
  • zero-copy (transferTo)
  • Connection Pool
  • Bulk Insert/Stream

3️⃣ File 객체의 정의와 구조

3.1 File 의 정의

java.io.File:

  파일과 디렉토리의 추상적 경로 표현.
  Java 1.0 부터 존재.

특징:
  - 인스턴스 객체
  - 경로 + 약간의 메타데이터
  - 파일이 실제로 존재하지 않아도 객체 생성 가능

3.2 File 의 생성자

// 5가지 생성자

// 1. 경로 문자열
File f1 = new File("file.txt");
File f2 = new File("/home/user/file.txt");
File f3 = new File("C:\\Users\\user\\file.txt");

// 2. 부모 + 자식 (이름)
File f4 = new File("/home/user", "file.txt");
// → /home/user/file.txt

// 3. 부모 File + 자식 (이름)
File parent = new File("/home/user");
File f5 = new File(parent, "file.txt");

// 4. URI
URI uri = URI.create("file:///home/user/file.txt");
File f6 = new File(uri);

// 5. (private 또는 deprecated 생성자 있음)

3.3 File 객체의 본질

// File 은 경로의 추상화 — 실제 파일과 별개
File f = new File("nonexistent.txt");
// 객체 생성 성공
// 실제 파일은 없음

f.exists();           // false
f.length();           // 0
f.canRead();          // false
f.getAbsolutePath();  // 작동 (경로만)

// 즉:
// File 객체 = 경로 정보
// 실제 파일 = OS 의 파일 시스템 자원
// 두 개념 분리

3.4 File 의 주요 메서드 그룹

정보 조회:
  - exists(), length(), lastModified()
  - canRead(), canWrite(), canExecute()
  - isFile(), isDirectory(), isHidden()
  
경로 조회:
  - getName(), getPath(), getParent()
  - getAbsolutePath(), getCanonicalPath()
  - toURI(), toPath()
  
생성/삭제:
  - createNewFile(), delete()
  - mkdir(), mkdirs()
  - renameTo()
  
목록:
  - list(), listFiles()
  - listFiles(FileFilter)
  - listFiles(FilenameFilter)
  
권한:
  - setReadable(), setWritable(), setExecutable()
  - setLastModified()
  - setReadOnly()

3.5 File 의 한계 (개요)

File 의 한계 (다음 섹션에서 정밀):

1. boolean 반환 — 진단 어려움
2. 디렉토리 자동 생성 안 함
3. 심볼릭 링크 처리 X
4. 풍부한 속성 X (POSIX 권한 등)
5. 비효율적 디렉토리 순회 (한 번에 전부)
6. 파일 시스템 추상화 X
7. 비동기 I/O X

3.6 File 의 활용 (여전히 사용되는 곳)

// 1. 레거시 코드
public void legacyMethod(File file) {
    // 기존 API 가 File 받음
}

// 2. 라이브러리 호환
SomeLibrary.process(new File("file.txt"));

// 3. 간단한 작업
File config = new File("config.properties");
if (config.exists()) {
    // 처리
}

// 4. Path 와의 변환
File f = new File("file.txt");
Path p = f.toPath();   // Path 로 변환

Path p2 = Path.of("file.txt");
File f2 = p2.toFile();   // File 로 변환

3.7 자기 점검 답변

File 객체의 본질과 구조는?

:
1. 본질:

  • 경로의 추상적 표현
  • Java 1.0 부터
  • 실제 파일과 별개
  1. 생성:

    • 다양한 생성자 (경로 문자열, 부모+자식, URI)
    • 파일 없어도 객체 생성
  2. 메서드 그룹:

    • 정보 조회 (exists, length, ...)
    • 경로 (getName, getAbsolutePath, ...)
    • 생성/삭제 (createNewFile, delete, ...)
    • 목록 (listFiles, ...)
    • 권한 (setReadable, ...)
  3. 현재 권장:

    • 새 코드: Path + Files
    • File: 레거시 호환만

4️⃣ File 의 주요 메서드 정밀

4.1 createNewFile() — 파일 생성

File f = new File("/path/to/file.txt");

boolean created = f.createNewFile();
// 반환:
// - true: 성공적으로 생성
// - false: 이미 존재 또는 실패

// 예외:
// - IOException: I/O 실패 (디렉토리 없음, 권한 없음 등)

// 함정:
// - 부모 디렉토리가 없으면 IOException
// - "디렉토리는 자동 생성 안 됨"
// 사용 패턴
File file = new File("/var/data/file.txt");

try {
    if (file.createNewFile()) {
        System.out.println("Created: " + file);
    } else {
        System.out.println("Already exists: " + file);
    }
} catch (IOException e) {
    // 부모 디렉토리 없음, 권한 없음 등
    e.printStackTrace();
}

// NIO.2 의 대안
Path path = Path.of("/var/data/file.txt");
try {
    Files.createFile(path);
} catch (FileAlreadyExistsException e) {
    // 이미 존재
} catch (NoSuchFileException e) {
    // 부모 디렉토리 없음 (명확!)
} catch (IOException e) {
    // 기타
}

4.2 getAbsolutePath() — 절대 경로

// 절대 경로 vs 상대 경로

// 상대 경로
File rel = new File("file.txt");
rel.getPath();           // file.txt (입력 그대로)
rel.getAbsolutePath();   // /current/dir/file.txt (현재 작업 디렉토리 기준)

// 절대 경로
File abs = new File("/home/user/file.txt");
abs.getPath();           // /home/user/file.txt
abs.getAbsolutePath();   // /home/user/file.txt (동일)

// getAbsolutePath 의 동작:
// 1. 이미 절대 경로면 그대로
// 2. 상대 경로면 user.dir 시스템 프로퍼티 prefix
//    System.getProperty("user.dir")

// 주의:
// - 심볼릭 링크는 해소 안 함
// - . 과 .. 는 그대로 둠

4.3 getCanonicalPath() — 정규화 경로

// Canonical = 정규화된 (심볼릭 링크 해소 + . / .. 정리)

File f = new File("/home/user/./docs/../file.txt");
f.getAbsolutePath();    // /home/user/./docs/../file.txt
f.getCanonicalPath();   // /home/user/file.txt (정규화)

// 심볼릭 링크
File link = new File("/path/to/symlink");
// 가정: /path/to/symlink → /real/path/file.txt
link.getAbsolutePath();    // /path/to/symlink
link.getCanonicalPath();   // /real/path/file.txt

// 예외:
// - IOException: 파일 시스템 접근 실패

// 활용:
// - 두 경로가 같은 파일을 가리키는지 비교
// - 보안 검사 (Path Traversal 방지)
File requested = new File(userInput);
File baseDir = new File("/var/data");

if (!requested.getCanonicalPath().startsWith(baseDir.getCanonicalPath())) {
    // 안전하지 않음 — userInput 이 baseDir 외부
    throw new SecurityException();
}

4.4 getName() — 파일명만

File f = new File("/home/user/docs/report.pdf");

f.getName();          // report.pdf (파일명 + 확장자)
f.getPath();          // /home/user/docs/report.pdf
f.getParent();        // /home/user/docs

// 확장자 분리 (별도)
String name = f.getName();
int dot = name.lastIndexOf('.');
String base = (dot > 0) ? name.substring(0, dot) : name;
String ext = (dot > 0) ? name.substring(dot + 1) : "";

// NIO.2 의 대안
Path p = Path.of("/home/user/docs/report.pdf");
p.getFileName();   // report.pdf (Path 객체)
p.getFileName().toString();   // "report.pdf"

4.5 getParent() — 부모 디렉토리

File f = new File("/home/user/docs/report.pdf");

f.getParent();        // "/home/user/docs" (String)
f.getParentFile();    // File 객체

// 루트의 경우
File root = new File("/");
root.getParent();     // null
root.getParentFile(); // null

// 상대 경로
File rel = new File("file.txt");
rel.getParent();      // null (부모가 명시 안 됨)
rel.getAbsoluteFile().getParent();   // 현재 작업 디렉토리

// 활용
File f = new File("/var/data/file.txt");
File dir = f.getParentFile();
if (!dir.exists()) {
    dir.mkdirs();   // 부모 디렉토리 생성
}
f.createNewFile();

4.6 length() — 파일 크기

File f = new File("file.txt");
long size = f.length();

// 반환:
// - 파일 크기 (바이트)
// - 파일 없으면 0
// - 디렉토리도 0 (대부분 OS)

// 함정:
// - 0 이 "비어있는 파일" 또는 "없는 파일" 둘 다 가능
// - 구분 필요: f.exists() + f.length()

if (f.exists()) {
    if (f.length() == 0) {
        // 빈 파일
    } else {
        // 데이터 있음
    }
} else {
    // 없는 파일
}

4.7 lastModified() — 마지막 수정 시간

File f = new File("file.txt");
long timestamp = f.lastModified();   // millisecond since epoch

// 변환
Instant instant = Instant.ofEpochMilli(timestamp);
LocalDateTime dateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();

// 변경
f.setLastModified(System.currentTimeMillis());

// NIO.2 의 대안 (더 풍부)
Path p = Path.of("file.txt");
FileTime modifiedTime = Files.getLastModifiedTime(p);
Instant inst = modifiedTime.toInstant();

4.8 listFiles() — 디렉토리 순회

File dir = new File("/var/data");

// 모든 파일 (배열, 한 번에 메모리에)
File[] all = dir.listFiles();

// 필터링
File[] txtFiles = dir.listFiles((d, name) -> name.endsWith(".txt"));
File[] regularFiles = dir.listFiles(File::isFile);

// 이름만
String[] names = dir.list();

// 함정:
// - 대용량 디렉토리에서 OOM 위험
// - null 반환 가능 (디렉토리 아니거나 접근 불가)

if (dir.isDirectory()) {
    File[] files = dir.listFiles();
    if (files != null) {
        for (File f : files) { ... }
    }
}

// NIO.2 의 대안 — Stream (lazy)
try (Stream<Path> paths = Files.list(Path.of("/var/data"))) {
    paths.filter(p -> p.toString().endsWith(".txt"))
        .forEach(System.out::println);
}

4.9 ILIC 의 File 메서드 활용

public class ShipmentFileUtil {
    
    private static final File EXPORT_DIR = new File("/var/shipment/exports");
    
    // 파일 생성 (부모 디렉토리 자동 생성 포함)
    public File createExportFile(String name) throws IOException {
        // 부모 디렉토리 확인 + 생성
        if (!EXPORT_DIR.exists() && !EXPORT_DIR.mkdirs()) {
            throw new IOException("Cannot create directory: " + EXPORT_DIR);
        }
        
        File file = new File(EXPORT_DIR, name);
        if (!file.createNewFile() && !file.exists()) {
            throw new IOException("Cannot create file: " + file);
        }
        
        return file;
    }
    
    // 안전한 경로 확인 (Path Traversal 방지)
    public File getSafeFile(String userInput) throws IOException {
        File requested = new File(EXPORT_DIR, userInput);
        String canonical = requested.getCanonicalPath();
        String baseCanonical = EXPORT_DIR.getCanonicalPath();
        
        if (!canonical.startsWith(baseCanonical)) {
            throw new SecurityException("Path traversal attempt: " + userInput);
        }
        
        return requested;
    }
    
    // 오래된 파일 삭제
    public void deleteOldFiles(int days) {
        long threshold = System.currentTimeMillis() - (long) days * 24 * 3600 * 1000;
        
        File[] files = EXPORT_DIR.listFiles();
        if (files == null) return;
        
        for (File f : files) {
            if (f.isFile() && f.lastModified() < threshold) {
                if (!f.delete()) {
                    log.warn("Failed to delete: {}", f);
                }
            }
        }
    }
}

4.10 자기 점검 답변

File 의 주요 메서드 5가지의 정확한 차이는?

:
1. createNewFile():

  • 파일 생성
  • 부모 디렉토리 없으면 IOException
  • 이미 있으면 false
  1. getAbsolutePath():

    • 절대 경로 (user.dir 기준)
    • 심볼릭 링크 해소 X
    • . / .. 그대로
  2. getCanonicalPath():

    • 정규화 경로
    • 심볼릭 링크 해소
    • . / .. 정리
    • IOException 가능
  3. getName():

    • 파일명만 (디렉토리 제외)
    • 확장자 포함
  4. getParent():

    • 부모 디렉토리 (String)
    • 루트면 null
    • getParentFile() 은 File 반환

5️⃣ "디렉토리는 자동 생성 안 됨" 함정

5.1 가장 흔한 함정

// 가장 흔한 실수
File file = new File("/var/data/2026/05/report.txt");

try {
    file.createNewFile();
    // ❌ IOException: No such file or directory
    // 부모 디렉토리 /var/data/2026/05 가 없음
} catch (IOException e) {
    // 잡혔지만 원인 모름
}

5.2 왜 자동 생성 안 하나?

이유:

1. 안전성
   - 사용자가 의도하지 않은 디렉토리 생성 방지
   - 권한 문제 회피

2. 명시성
   - 디렉토리 생성은 명시적 의도
   - mkdir() 또는 mkdirs() 호출

3. 성능
   - 매번 디렉토리 검사는 비용
   - 호출자가 알아서 보장

설계 결정:
  - createNewFile() 은 단일 책임
  - "파일 생성만"
  - 부모 디렉토리는 별도

5.3 mkdir 의 함정

// mkdir() — 단일 디렉토리만
File dir = new File("/var/data/2026/05/reports");

dir.mkdir();
// ❌ false
// 부모 /var/data/2026/05 가 없음
// mkdir 은 "마지막 하나만" 생성

// mkdirs() — 중간 디렉토리 모두
dir.mkdirs();
// ✓ true
// 필요한 모든 부모 디렉토리 생성
// /var/data/2026/05/reports
// /var/data/2026/05
// /var/data/2026
// /var/data
// 모두 생성 시도

5.4 올바른 패턴

// 안전한 파일 생성 패턴

public File safeCreateFile(File file) throws IOException {
    // 1. 부모 디렉토리 확보
    File parent = file.getParentFile();
    if (parent != null && !parent.exists()) {
        if (!parent.mkdirs()) {
            throw new IOException("Cannot create parent directory: " + parent);
        }
    }
    
    // 2. 파일 생성
    if (!file.createNewFile() && !file.exists()) {
        throw new IOException("Cannot create file: " + file);
    }
    
    return file;
}

// 사용
File f = new File("/var/data/2026/05/report.txt");
safeCreateFile(f);
// 부모 디렉토리들 자동 생성

5.5 NIO.2 의 해결

// NIO.2 의 createDirectories — 명시적, 명확
Path file = Path.of("/var/data/2026/05/report.txt");
Files.createDirectories(file.getParent());   // 부모 디렉토리 모두 생성
Files.createFile(file);                       // 파일 생성

// 또는 한 번에
Path file = Path.of("/var/data/2026/05/report.txt");
Files.createDirectories(file.getParent());
Files.writeString(file, "content");

// 이미 존재해도 OK (createDirectories)
// 첫 시도: 디렉토리 생성
// 두 번째 시도: 이미 존재, 그냥 통과

5.6 실무에서 자주 발생하는 시나리오

// 시나리오 1: 날짜별 디렉토리에 저장
public class DailyExporter {
    
    public void export(List<Shipment> shipments) throws IOException {
        LocalDate today = LocalDate.now();
        
        File dir = new File("/var/exports",
            String.format("%d/%02d/%02d", 
                today.getYear(), 
                today.getMonthValue(), 
                today.getDayOfMonth()));
        
        // ❌ 잊으면 IOException
        // dir.exists() check + mkdirs() 누락
        
        File file = new File(dir, "shipments.csv");
        file.createNewFile();   // 실패!
        
        // ✓ 올바른 패턴
        if (!dir.exists()) {
            dir.mkdirs();
        }
        // 또는 NIO.2
        Files.createDirectories(dir.toPath());
    }
}

// 시나리오 2: 업로드 파일 저장
public class FileUploader {
    
    public void save(MultipartFile file, String userId) throws IOException {
        File targetDir = new File("/var/uploads", userId);
        
        // ❌ 누락
        // targetDir.mkdirs();
        
        File target = new File(targetDir, file.getOriginalFilename());
        file.transferTo(target);   // 실패!
        
        // ✓ NIO.2
        Path targetPath = Path.of("/var/uploads", userId, file.getOriginalFilename());
        Files.createDirectories(targetPath.getParent());
        file.transferTo(targetPath);
    }
}

// 시나리오 3: 로그 파일 회전
public class LogRotator {
    
    public void rotate(String logName) throws IOException {
        File current = new File("/var/log", logName);
        File archive = new File("/var/log/archive",
            logName + "." + System.currentTimeMillis());
        
        // ❌ /var/log/archive 가 없으면 실패
        current.renameTo(archive);
        
        // ✓ 올바른 패턴
        Path archivePath = Path.of("/var/log/archive",
            logName + "." + System.currentTimeMillis());
        Files.createDirectories(archivePath.getParent());
        Files.move(current.toPath(), archivePath);
    }
}

5.7 일반 원칙

파일 작업 전 체크리스트:

1. 부모 디렉토리 존재 확인
   - 없으면 mkdirs() 또는 Files.createDirectories()

2. 권한 확인
   - canWrite() 또는 Files.isWritable()

3. 디스크 공간 확인
   - 큰 파일 작업 시 free space 확인

4. 파일 시스템 한계
   - 파일명 길이
   - 디렉토리 안 파일 수

5. 동시성
   - 다른 프로세스가 동시 작업?
   - Lock 필요?

5.8 ILIC 의 안전한 파일 작업

@Component
public class SafeFileService {
    
    private final Path baseDir = Path.of("/var/shipment");
    
    public Path writeShipmentReport(String content, LocalDate date) throws IOException {
        // 1. 날짜별 경로 생성
        Path file = baseDir
            .resolve("reports")
            .resolve(String.valueOf(date.getYear()))
            .resolve(String.format("%02d", date.getMonthValue()))
            .resolve("shipment_" + date + ".txt");
        
        // 2. 부모 디렉토리 보장
        Files.createDirectories(file.getParent());
        
        // 3. 권한 확인
        if (!Files.isWritable(file.getParent())) {
            throw new IOException("No write permission: " + file.getParent());
        }
        
        // 4. 안전한 쓰기 (이미 있으면 덮어쓰기)
        Files.writeString(file, content,
            StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
        
        return file;
    }
    
    public Path uploadShipmentAttachment(String userId, String filename, byte[] data) 
            throws IOException {
        // 1. Path Traversal 방지
        if (filename.contains("/") || filename.contains("..")) {
            throw new SecurityException("Invalid filename: " + filename);
        }
        
        // 2. 사용자별 디렉토리
        Path userDir = baseDir.resolve("uploads").resolve(userId);
        Files.createDirectories(userDir);
        
        // 3. 안전한 파일명 (timestamp 추가)
        Path file = userDir.resolve(System.currentTimeMillis() + "_" + filename);
        
        // 4. 쓰기
        Files.write(file, data);
        
        return file;
    }
}

5.9 자기 점검 답변

"디렉토리는 자동 생성 안 됨" 함정과 해결은?

:
1. 함정:

  • file.createNewFile() 가 부모 디렉토리 없으면 IOException
  • file.renameTo(dest) 도 dest 의 부모 디렉토리 필요
  • 업로드, 로그 회전 등에서 자주 발생
  1. 이유:

    • 단일 책임 (파일 생성만)
    • 안전성 (의도하지 않은 생성 방지)
  2. 해결:

    • mkdirs() (중간 디렉토리 모두)
    • 또는 NIO.2 Files.createDirectories()
  3. 권장 패턴:

    Files.createDirectories(file.getParent());
    Files.writeString(file, content);
  4. 체크리스트:

    • 부모 디렉토리 존재
    • 권한
    • 디스크 공간
    • 동시성

6️⃣ mkdir vs mkdirs, 그 외 함정들

6.1 mkdir vs mkdirs

mkdir:
  - 마지막 디렉토리만 생성
  - 부모 없으면 실패 (false)

mkdirs:
  - 필요한 모든 부모 디렉토리 생성
  - 이미 있으면 그 부분은 건너뜀
  - 최종 디렉토리도 이미 있으면 false
// 비교
File dir = new File("/var/data/2026/05");

// 가정: /var 만 존재
dir.mkdir();    // false (부모 /var/data 없음)
dir.mkdirs();   // true (모두 생성)

// /var/data/2026/05 모두 생성됨

// 또 호출
dir.mkdir();    // false (이미 존재)
dir.mkdirs();   // false (이미 존재)

// 함정: mkdirs() 가 false 반환해도
// "이미 존재" 일 수도, "실패" 일 수도
// 구분 필요
if (!dir.mkdirs() && !dir.exists()) {
    throw new IOException("Cannot create: " + dir);
}

6.2 delete 의 함정

File dir = new File("/var/data");

// 디렉토리 안에 파일 있으면?
dir.delete();   // ❌ false (디렉토리가 비어있지 않음)

// 디렉토리 삭제 = 비어있을 때만
// 안의 파일/디렉토리 먼저 삭제 필요

// 재귀 삭제 (직접 구현 필요)
public static void deleteRecursively(File f) {
    if (f.isDirectory()) {
        File[] children = f.listFiles();
        if (children != null) {
            for (File child : children) {
                deleteRecursively(child);
            }
        }
    }
    f.delete();
}

// NIO.2 의 대안
Files.walkFileTree(Path.of("/var/data"), new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file);
        return FileVisitResult.CONTINUE;
    }
    
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        Files.delete(dir);
        return FileVisitResult.CONTINUE;
    }
});

// 또는 Apache Commons IO
FileUtils.deleteDirectory(new File("/var/data"));

6.3 renameTo 의 함정

File src = new File("/var/data/file.txt");
File dest = new File("/other/dir/file.txt");

boolean renamed = src.renameTo(dest);

// 함정 1: 부모 디렉토리 없음
// /other/dir 없으면 false

// 함정 2: 다른 파일 시스템
// /var 와 /other 가 다른 마운트 포인트면 실패 가능
// 일부 OS

// 함정 3: 이미 존재
// dest 가 이미 있으면 OS 따라 다름

// 함정 4: 실패 원인 모름
// false 만 반환

// NIO.2 의 대안
Files.move(src.toPath(), dest.toPath());
// 명확한 예외:
// - NoSuchFileException
// - FileAlreadyExistsException
// - AtomicMoveNotSupportedException

// 옵션
Files.move(src.toPath(), dest.toPath(),
    StandardCopyOption.REPLACE_EXISTING,
    StandardCopyOption.ATOMIC_MOVE);

6.4 listFiles 의 함정

File dir = new File("/var/data");

// 함정 1: null 반환
File[] files = dir.listFiles();
// - 디렉토리 아님
// - 접근 권한 없음
// - I/O 에러
// 모두 null

// 안전한 사용
File[] files = dir.listFiles();
if (files != null) {
    for (File f : files) { ... }
}

// 함정 2: 메모리 사용
// 1만 파일 = 1만 File 객체 한 번에 메모리
// 대용량 디렉토리에서 OOM 위험

// NIO.2 의 대안 — Stream
try (Stream<Path> paths = Files.list(Path.of("/var/data"))) {
    paths.forEach(System.out::println);
}
// Lazy, 메모리 효율

6.5 경로 구분자

// OS 별 경로 구분자
// Windows: \
// Unix/Linux/Mac: /

// File 사용 시
File f1 = new File("/home/user/file.txt");    // Unix
File f2 = new File("C:\\Users\\user\\file.txt");  // Windows

// 구분자 가져오기
String sep = File.separator;
// Windows: "\\"
// Unix: "/"

// File 은 OS 자동 변환
File f3 = new File("/home/user");
File f4 = new File(f3, "docs");
f4.getPath();
// Unix: /home/user/docs
// Windows: \home\user\docs

// NIO.2 의 일관된 처리
Path p = Path.of("/home/user/docs");
// 또는 multi-arg
Path p2 = Path.of("/home", "user", "docs");
// OS 자동 처리

6.6 파일 존재 확인의 race condition

// 함정: TOCTOU (Time-Of-Check Time-Of-Use)
File f = new File("file.txt");

if (f.exists()) {
    // ★ 여기서 다른 프로세스가 삭제할 수 있음
    f.delete();   // 실패 가능
}

// 더 안전: 직접 시도 + 예외 처리
try {
    Files.delete(Path.of("file.txt"));
} catch (NoSuchFileException e) {
    // 없음
} catch (IOException e) {
    // 기타
}

6.7 File 의 동시성

// File 자체는 immutable (경로만)
// 하지만 가리키는 파일은 외부 자원

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

// 동시에 여러 스레드가 작업
Thread t1 = new Thread(() -> writeToFile(f));
Thread t2 = new Thread(() -> readFromFile(f));

// 동기화 필요 (외부 자원)
// 1. 파일 락
// 2. 애플리케이션 락
// 3. NIO.2 의 FileLock

// FileLock 활용
try (FileChannel ch = FileChannel.open(f.toPath(), READ, WRITE);
     FileLock lock = ch.lock()) {
    // 배타적 락
    // ...
}

6.8 흔한 실수 종합

File 사용 시 흔한 실수:

1. 부모 디렉토리 안 만들고 createNewFile
2. mkdir vs mkdirs 혼동
3. delete 의 디렉토리 비어있어야 함
4. renameTo 의 부모 디렉토리 누락
5. listFiles 의 null 체크 누락
6. 대용량 디렉토리 listFiles
7. 경로 구분자 하드코딩
8. TOCTOU race condition
9. 동시성 무시
10. 진단 어려움 (boolean 반환)

→ 대부분 NIO.2 의 Path/Files 에서 명시적 예외로 해결

6.9 자기 점검 답변

File 사용 시 함정 5가지는?

:
1. 디렉토리 자동 생성 X: mkdirs() 필수
2. mkdir vs mkdirs: 단일 vs 중간 모두
3. delete 의 디렉토리: 비어있어야
4. renameTo 의 부모 누락: dest 부모 디렉토리 필요
5. listFiles 의 null: 권한/오류 시 null

해결: NIO.2 의 Files API

  • Files.createDirectories
  • Files.delete (NoSuchFileException)
  • Files.move (FileAlreadyExistsException)
  • Files.list (Stream)

7️⃣ File 의 한계 종합 + NIO.2 해결

7.1 File 의 한계 7가지

File 의 7가지 한계:

1. boolean 반환 — 진단 어려움
2. 디렉토리 자동 생성 안 함
3. 심볼릭 링크 처리 X
4. 풍부한 속성 X
5. 비효율적 디렉토리 순회
6. 파일 시스템 추상화 X
7. 비동기 I/O X

7.2 NIO.2 의 해결

한계FileNIO.2
에러 처리boolean구체적 예외
디렉토리mkdirsFiles.createDirectories
심볼릭 링크exists 모호LinkOption.NOFOLLOW_LINKS
속성length, lastModifiedBasicFileAttributes, PosixFileAttributes
순회listFiles (메모리)Files.list (Stream)
파일 시스템로컬만FileSystems (ZIP 등)
비동기XAsynchronousFileChannel

7.3 명확한 예외 계층

NIO.2 의 예외 계층:

IOException
  ├── NoSuchFileException        - 파일 없음
  ├── FileAlreadyExistsException - 이미 존재
  ├── AccessDeniedException       - 권한 없음
  ├── DirectoryNotEmptyException  - 디렉토리 비어있지 않음
  ├── NotDirectoryException       - 디렉토리가 아님
  ├── FileSystemException         - 파일 시스템 일반
  ├── AtomicMoveNotSupportedException - 원자적 이동 불가
  └── ClosedFileSystemException   - 닫힌 FS

활용:
try {
    Files.delete(path);
} catch (NoSuchFileException e) {
    // 명확
} catch (DirectoryNotEmptyException e) {
    // 명확
} catch (AccessDeniedException e) {
    // 명확
}

7.4 풍부한 속성

// File 의 빈약한 속성
File f = new File("file.txt");
f.length();
f.lastModified();
f.canRead();
f.canWrite();
// 끝

// NIO.2 의 풍부한 속성
Path p = Path.of("file.txt");

// 기본 속성
BasicFileAttributes basic = Files.readAttributes(p, BasicFileAttributes.class);
basic.creationTime();
basic.lastAccessTime();
basic.lastModifiedTime();
basic.size();
basic.isRegularFile();
basic.isDirectory();
basic.isSymbolicLink();
basic.fileKey();

// POSIX 속성 (Linux/Mac)
PosixFileAttributes posix = Files.readAttributes(p, PosixFileAttributes.class);
posix.owner();
posix.group();
posix.permissions();
// Set<PosixFilePermission>

// DOS 속성 (Windows)
DosFileAttributes dos = Files.readAttributes(p, DosFileAttributes.class);
dos.isHidden();
dos.isReadOnly();
dos.isSystem();

// 권한 변경
Set<PosixFilePermission> perms = EnumSet.of(
    PosixFilePermission.OWNER_READ,
    PosixFilePermission.OWNER_WRITE);
Files.setPosixFilePermissions(p, perms);

7.5 심볼릭 링크 처리

// NIO.2 의 심볼릭 링크 정밀
Path link = Path.of("/path/to/symlink");

// 링크 자체 검사
boolean linkExists = Files.exists(link, LinkOption.NOFOLLOW_LINKS);

// 원본 검사 (기본)
boolean targetExists = Files.exists(link);

// 링크 vs 원본 구분
Files.isSymbolicLink(link);   // true
Files.isRegularFile(link);    // false (링크니까)
Files.isRegularFile(link, LinkOption.NOFOLLOW_LINKS);   // 옵션

// 링크 따라가서 원본 정보
Path real = link.toRealPath();   // 원본 경로

// 링크 자체 정보
Path target = Files.readSymbolicLink(link);   // 가리키는 경로

// 링크 생성
Files.createSymbolicLink(link, target);
Files.createLink(link, target);   // 하드 링크

7.6 효율적 순회

// File 의 비효율
File dir = new File("/large/dir");
File[] all = dir.listFiles();   // 메모리에 모두

// NIO.2 의 Stream
try (Stream<Path> paths = Files.list(Path.of("/large/dir"))) {
    paths.filter(p -> p.toString().endsWith(".txt"))
        .limit(100)
        .forEach(System.out::println);
}
// Lazy, 메모리 효율적

// 재귀 (walk)
try (Stream<Path> paths = Files.walk(Path.of("/large/dir"))) {
    long count = paths.filter(Files::isRegularFile).count();
}

// 조건 (find)
try (Stream<Path> paths = Files.find(
        Path.of("/large/dir"), 5,   // 깊이 5
        (path, attrs) -> attrs.size() > 1_000_000)) {
    paths.forEach(System.out::println);
}

// FileVisitor (가장 정밀)
Files.walkFileTree(Path.of("/large/dir"), new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        if (attrs.size() > 1_000_000) {
            System.out.println(file);
        }
        return FileVisitResult.CONTINUE;
    }
});

7.7 파일 시스템 추상화

// ZIP 파일을 파일 시스템처럼
try (FileSystem zipFs = FileSystems.newFileSystem(
        Path.of("archive.zip"), Map.of())) {
    
    // ZIP 안의 파일 작업
    Path inZip = zipFs.getPath("/inside/file.txt");
    String content = Files.readString(inZip);
    
    // 새 파일 추가 (실제 ZIP 수정)
    Path newFile = zipFs.getPath("/added.txt");
    Files.writeString(newFile, "new content");
    
    // 디렉토리 순회
    try (Stream<Path> paths = Files.walk(zipFs.getPath("/"))) {
        paths.forEach(System.out::println);
    }
}

// 동일 API 로 다양한 FS 사용
// - 로컬 FS
// - ZIP
// - JAR
// - 메모리 (jimfs 같은 라이브러리)
// - 네트워크 FS

7.8 비동기 I/O

// NIO.2 의 비동기 파일 채널
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("file.txt"), StandardOpenOption.READ);

ByteBuffer buffer = ByteBuffer.allocate(1024);

// 방법 1: Future
Future<Integer> future = channel.read(buffer, 0);
// 즉시 리턴, 백그라운드 진행

while (!future.isDone()) {
    // 다른 일
}
int bytesRead = future.get();

// 방법 2: CompletionHandler (콜백)
channel.read(buffer, 0, "attachment", 
    new CompletionHandler<Integer, String>() {
        @Override
        public void completed(Integer result, String attachment) {
            System.out.println("Read " + result + " bytes");
        }
        
        @Override
        public void failed(Throwable exc, String attachment) {
            exc.printStackTrace();
        }
    });

7.9 ILIC 의 마이그레이션

// 옛 코드 (File 기반)
public class OldFileService {
    
    public void process(String dirPath, String fileName) throws IOException {
        File dir = new File(dirPath);
        if (!dir.exists() && !dir.mkdirs()) {
            throw new IOException("Cannot create directory");
        }
        
        File file = new File(dir, fileName);
        if (!file.createNewFile() && !file.exists()) {
            throw new IOException("Cannot create file");
        }
        
        // 처리
        try (FileWriter writer = new FileWriter(file)) {
            writer.write("data");
        }
    }
}

// 새 코드 (Path + Files)
public class NewFileService {
    
    public void process(String dirPath, String fileName) throws IOException {
        Path file = Path.of(dirPath, fileName);
        
        Files.createDirectories(file.getParent());
        Files.writeString(file, "data");
        // 명확, 간결
    }
}

// 점진적 마이그레이션
// 1. File ↔ Path 변환 활용
// 2. 새 메서드만 Path 로
// 3. 점진적으로 옛 코드 교체

7.10 자기 점검 답변

File 의 한계와 NIO.2 의 해결은?

:
1. 에러 처리: boolean → 구체적 예외
2. 디렉토리: mkdirs → Files.createDirectories
3. 심볼릭 링크: 모호 → LinkOption
4. 속성: 빈약 → BasicFileAttributes, PosixFileAttributes
5. 순회: 메모리 → Stream
6. FS 추상화: 로컬만 → FileSystem (ZIP 등)
7. 비동기: X → AsynchronousFileChannel

권장:

  • 새 코드: Path + Files
  • 레거시: File 유지 또는 점진 마이그레이션
  • File ↔ Path 변환 메서드 활용

8️⃣ Phase 7 완주 정리 + Phase 8 예고

8.1 Phase 7 학습 종합

Phase 7 — I/O 시스템 큰 그림

Unit 7.1 — I/O 란 무엇인가
  - JVM 기준 Input/Output
  - I/O 의 4가지 종류
  - 자바 I/O 진화 (1.0 → 1.4 → 7)
  - I/O 모델 4가지 (Blocking/Non-blocking × Sync/Async)

Unit 7.2 — IO vs NIO (역사적 진화)
  - 3 시대 비교
  - File vs Files
  - Decorator 패턴
  - 채널 + 버퍼

Unit 7.3 — Stream vs Channel
  - Buffer 의 4속성 정밀
  - Heap vs Direct Buffer
  - zero-copy
  - MappedByteBuffer

Unit 7.4 — Blocking vs Non-blocking (★ 마스터)
  - OS 레벨 동작
  - Selector 정밀
  - 동시성 모델 4가지
  - 1만 연결 시나리오
  - Tomcat/Netty/WebFlux/Loom

Unit 7.5 — 오버헤드와 File 객체
  - 오버헤드의 정의
  - I/O 오버헤드 4지점
  - File 메서드 정밀
  - 자주 발생하는 함정

8.2 Phase 7 마스터 후 가능한 일

1. I/O 모델 선택
   - 시나리오 분석
   - Blocking/Non-blocking/Reactive/Loom 선택

2. 효율적 파일 작업
   - Stream 활용
   - Buffered + Direct Buffer
   - zero-copy

3. 대규모 동시 연결
   - Selector, Netty 이해
   - WebFlux 활용
   - Project Loom

4. 함정 회피
   - 디렉토리 자동 생성 X
   - mkdir vs mkdirs
   - listFiles null
   - TOCTOU race condition

5. 마이그레이션
   - File → Path
   - 점진적 전환
   - 변환 메서드 활용

6. 면접 자신감
   - I/O 의 본질
   - JVM 기준 I/O
   - 동시성 모델
   - C10K 문제
   - 자바 I/O 의 진화

8.3 Phase 7 의 큰 그림

I/O 의 본질:
  - JVM 기준 데이터 흐름
  - 외부 ↔ 내부

3 시대 진화:
  IO (1.0) → NIO (1.4) → NIO.2 (7)
  - 단방향 → 양방향
  - Blocking → Non-blocking
  - File → Path + Files

동시성:
  - Thread-per-connection (단순, 한계)
  - Event Loop (효율, 복잡)
  - Reactive (함수형)
  - Virtual Threads (미래)

실무 함정:
  - 디렉토리 자동 생성 X
  - 예외 마스킹
  - 메모리 효율 (Stream vs 배열)
  - 자원 누수

8.4 Phase 8 — Stream 실전

다음 Phase 는 자바 IO Stream 의 실전.

Phase 8 — Stream 실전 (6 Unit)

Unit 8.1 — System.in (한글 안 되는 이유)
  - System.in 의 정체
  - 1바이트씩 vs 한글
  - 인코딩 이슈

Unit 8.2 — FileInputStream
  - 파일 바이트 스트림
  - read() 의 정밀
  - 파일 끝 (-1)

Unit 8.3 — byte[] 배열로 효율적 읽기
  - 버퍼링
  - 마지막 읽기 함정 (n 만큼만)

Unit 8.4 — FileOutputStream
  - 파일 쓰기
  - 이어쓰기 모드 (append)
  - 인코딩 매핑

Unit 8.5 — 한글 처리 (FileReader, InputStreamReader)
  - Reader vs InputStream
  - 인코딩 명시
  - 두 방식 비교

Unit 8.6 — FileWriter (한글 쓰기)
  - Writer 의 정밀
  - 다양한 write 메서드

8.5 Phase 7 → Phase 8 의 연결

Phase 7: I/O 의 큰 그림 (개념)
   ↓
Phase 8: Stream 의 실전 (코드)
   ↓
Phase 9: Stream 의 강화 (Buffered/Data/Serialization)
   ↓
Phase 10: 함수형 (람다, Stream API)

연결:
  - Phase 7 의 개념을 Phase 8 에서 코드로
  - Phase 8 의 기본 위에 Phase 9 의 보조 스트림
  - Phase 10 의 함수형 Stream API (다른 개념)

8.6 3주차 누적 진행

✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주, 3 Unit)
✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주, 6 Unit)
✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주, 4 Unit)
✅ Phase 4 — 추상화의 두 도구 (4.1 ~ 4.4 완주, 4 Unit)
✅ Phase 5 — 제네릭과 와일드카드 (5.1 ~ 5.5 완주, 5 Unit)
✅ Phase 6 — 객체 비교 (6.1 ~ 6.4 완주, 4 Unit)
✅ Phase 7 — I/O 시스템 큰 그림 (7.1 ~ 7.5 완주, 5 Unit) ← 여기
🚀 Phase 8 — Stream 실전 (6 Unit 예정)
⏭ Phase 9 — I/O 강화 (5 Unit)
⏭ Phase 10 — 함수형 프로그래밍 (4 Unit)

총: 31/43 Unit 작성 (Phase 7 완주, 약 72%)

8.7 자기 점검 답변

Phase 7 학습의 종합은?

:
1. 개념:

  • I/O = JVM 기준 데이터 흐름
  • 자바 I/O 의 3 시대
  • Stream vs Channel
  1. 정밀:

    • Buffer 의 4속성
    • Heap vs Direct
    • zero-copy
  2. 동시성:

    • Blocking vs Non-blocking
    • Selector 의 멀티플렉싱
    • 4가지 모델 (Thread/Event/Reactive/Loom)
  3. 실무:

    • 오버헤드 4지점
    • File 의 한계
    • 함정 회피
    • NIO.2 권장
  4. 미래:

    • Project Loom (Virtual Threads)
    • 동기 코드 + 효율
    • 새 표준

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
오버헤드 정의?본 작업 외 간접 비용
I/O 오버헤드 4지점?시스템 호출, 컨텍스트 스위칭, 버퍼 복사, 추상화
File 정의?경로의 추상화, Java 1.0+
createNewFile vs Files.createFile?boolean vs 예외
getAbsolutePath vs getCanonicalPath?절대 경로 vs 정규화 (심볼릭 링크 해소)
디렉토리 자동 생성?X — mkdirs() 또는 Files.createDirectories()
mkdir vs mkdirs?단일 vs 중간 모두
File 의 한계 7가지?boolean/디렉토리/심볼릭/속성/순회/FS/비동기
listFiles null?권한, 오류, 디렉토리 아님
File → Path?toPath(), Path.of
TOCTOU?시간차 race condition

9.2 자기 점검 체크리스트

오버헤드

  • 정의와 4가지 종류
  • I/O 오버헤드 4지점
  • 좋은 vs 나쁜 오버헤드
  • 측정 방법

File 기본

  • File 정의와 본질
  • 5가지 생성자
  • 주요 메서드 그룹
  • File ↔ Path 변환

File 메서드 정밀

  • createNewFile
  • getAbsolutePath vs getCanonicalPath
  • getName, getParent
  • length, lastModified
  • listFiles, list

함정

  • 디렉토리 자동 생성 X
  • mkdir vs mkdirs
  • delete (디렉토리 비어야)
  • renameTo (부모 디렉토리)
  • listFiles null
  • TOCTOU
  • 동시성

한계와 NIO.2

  • File 의 7가지 한계
  • 명확한 예외 계층
  • BasicFileAttributes
  • Files.list/walk
  • FileSystem 추상화
  • 비동기 I/O

9.3 추가 심화 질문

Q1: getCanonicalPath 가 IOException 던지는 이유?

답:

  • 파일 시스템 접근 필요 (심볼릭 링크 해소)
  • 실제 파일 시스템 조회
  • 권한 부족, I/O 에러 가능
  • getAbsolutePath 는 문자열 처리만 (IOException X)

Q2: File.deleteOnExit() 의 동작?

답:

File temp = new File("temp.txt");
temp.deleteOnExit();
// JVM 종료 시 자동 삭제
// 단, 정상 종료만 (kill -9 같은 강제 종료는 X)
  • 임시 파일 정리에 유용
  • 단, 신뢰성 100% 아님

Q3: File.equals 의 동작?

답:

  • 경로 문자열 비교 (정확히 같은지)
  • OS 의 대소문자 구분 따름
  • 심볼릭 링크 해소 X
  • 다른 경로지만 같은 파일이면 equals = false
File f1 = new File("/path/file.txt");
File f2 = new File("/path/./file.txt");
f1.equals(f2);   // false (다른 경로 문자열)

// 동일성 비교는
f1.getCanonicalFile().equals(f2.getCanonicalFile());   // true

Q4: 임시 파일 생성?

답:

// File
File temp = File.createTempFile("prefix", ".txt");
// /tmp/prefix12345.txt 같은
temp.deleteOnExit();

// NIO.2
Path temp = Files.createTempFile("prefix", ".txt");
Path tempDir = Files.createTempDirectory("dirPrefix");

// 사용 후 삭제 권장
try (Stream<...> s = ...) {
    // 처리
} finally {
    Files.deleteIfExists(temp);
}

Q5: File 클래스가 deprecated 되나?

답:

  • 현재 deprecated 아님
  • 하위 호환성을 위해 유지
  • 새 코드는 Path 권장
  • 자바 표준 라이브러리는 둘 다 지원
// FileInputStream 도 Path 받음 (Java 7+)
FileInputStream fis = new FileInputStream("file.txt");
// 그래도 NIO.2 권장
InputStream is = Files.newInputStream(Path.of("file.txt"));

🎯 핵심 요약 — 3줄 정리

1. 오버헤드와 I/O

  • 오버헤드: 본 작업 외 간접 비용
  • I/O 의 4지점: 시스템 호출, 컨텍스트 스위칭, 버퍼 복사, 추상화
  • Buffered, Direct Buffer, zero-copy 로 절감

2. File 의 한계

  • boolean 반환 (진단 어려움)
  • 디렉토리 자동 생성 X
  • 풍부한 속성 X
  • 함정: mkdirs 누락, listFiles null

3. NIO.2 가 해결

  • 구체적 예외 (NoSuchFileException 등)
  • Files.createDirectories
  • BasicFileAttributes
  • Stream 기반 순회

🏆 Phase 7 완주 — I/O 시스템 마스터 달성

🚀 Phase 7 — I/O 시스템 큰 그림
  ✅ Unit 7.1 I/O 란 무엇인가
  ✅ Unit 7.2 IO vs NIO (역사적 진화)
  ✅ Unit 7.3 Stream vs Channel
  ✅ Unit 7.4 Blocking vs Non-blocking (★ 마스터 깊이)
  ✅ Unit 7.5 오버헤드와 File 객체 ← 여기, Phase 7 완주

→ 자바 I/O 시스템 정복
→ JVM 기준 I/O 의 본질
→ 3 시대 진화 (IO/NIO/NIO.2)
→ 동시성 모델 4가지 마스터
→ 실무 함정 회피

📚 다음으로...

Phase 8 — Stream 실전

다음 Phase 는 자바 IO Stream 의 실전.

Phase 8 — Stream 실전 (6 Unit)

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 (한글 쓰기)

Phase 7 와의 연결:

  • Phase 7: I/O 개념
  • Phase 8: Stream 의 실전 코드
  • Reader/Writer 의 정확한 활용
  • 한글 (인코딩) 의 정밀
profile
Software Developer

0개의 댓글