728x90
[Java] 파일 입출력의 진화: 전통 I/O에서 현대적 비동기·리액티브 방식까지
Java는 출시 초기부터 지금까지 다양한 파일 입출력(File I/O) API를 제공해 왔습니다. 각 버전마다 성능, 사용 편의성, 비동기 처리 능력 등을 개선해 왔는데요, 이 글에서는 시간의 흐름에 따라 주요 변화를 짚어보고, 현재 어떤 방식으로 파일을 다루는지 살펴보겠습니다.
아래 코드들은 깃허브에 테스트 코드까지 구현해 놓았습니다. 필요하신분들은 참고하시길 바랍니다.
1. Java 1.x: java.io 전통 스트림 방식
- 시기: Java 1.0~1.4
- 주요 클래스: FileInputStream/FileOutputStream, FileReader/FileWriter, BufferedInputStream/BufferedOutputStream
- 사용법: 바이트·문자 단위로 직접 읽고 쓰며, Buffered* 클래스로 버퍼링
예시코드
// 파일 존재 여부 확인 및 기본 스트림 사용 예시
import java.io.*;
public class LegacyIOExample {
public void basicIO() {
File file = new File("data.txt");
System.out.println("경로: " + file.getAbsolutePath());
try (FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream("out.txt")) {
int b;
while ((b = fis.read()) != -1) {
fos.write(b); // 1바이트씩 복사
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
단점
- 장황한 리소스 해제: finally 블록에서 close() 코드 중복
- 예외 처리 복잡: 예외가 발생 시 리소스 누수 위험
- 문자셋 지정 불편: 기본 플랫폼 인코딩 의존
- 원자적 파일 조작 부재: 복사·이동·삭제 시 일관성 보장 어려움
이러한 리소스 관리 번거로움과 원자적 조작 부재를 보완하기 위해 Java 7에서 NIO.2 (java.nio.file) 가 도입되었습니다.
2. Java 7+: NIO.2 (java.nio.file) - New I/O
- File API 통합: Path, Files, StandardCopyOption
- 간편 메서드: Files.copy, Files.move, Files.delete
- Files.copy, Files.write, Files.move 사용 시 리소스 자동 해제
❗️ Files.newInputStream · Files.newOutputStream를 이용하여
직접 스트림 제어를 하는 경우는 반드시 try-with-resources 를 사용해야 한다
예시코드
public class NioFileExample {
/**
* 파일을 대상 경로로 복사합니다.
*/
public void copyFile(Path file, Path targetFile) throws IOException {
Files.createDirectories(targetFile.getParent());
Files.copy(file, targetFile, StandardCopyOption.REPLACE_EXISTING);
// 아래와 같이 직접 파일 스트림을 사용하는 경우 try-with-resources를 사용해야 한다.
// 직접 연 스트림은 try-with-resources로 관리
// try (InputStream in = Files.newInputStream(file, StandardOpenOption.READ);
// OutputStream out = Files.newOutputStream(targetFile,
// StandardOpenOption.CREATE,
// StandardOpenOption.TRUNCATE_EXISTING)) {
// // JDK 9+ 메서드: in.transferTo(out);
// byte[] buffer = new byte[8 * 1024];
// int read;
// while ((read = in.read(buffer)) != -1) {
// out.write(buffer, 0, read);
// }
// }
}
/**
* 문자열 콘텐츠를 파일에 작성합니다.
*/
public void writeFile(Path path, String content, StandardOpenOption... openOptions)
throws IOException {
Files.createDirectories(path.getParent());
Files.writeString(path, content, openOptions);
}
/**
* 파일을 삭제합니다 (존재하지 않을 경우 예외는 발생하지 않습니다).
*/
public void deleteFile(Path path) throws IOException {
Files.deleteIfExists(path);
}
/**
* 파일을 다른 디렉토리로 이동하거나 이름을 변경합니다.
*/
public void moveFile(Path file, Path targetFile) throws IOException {
Files.createDirectories(targetFile.getParent());
Files.move(file, targetFile, StandardCopyOption.REPLACE_EXISTING);
}
}
단점
- 여전히 블로킹 I/O: 대용량 처리 시 스레드 대기
- 텍스트 vs. 바이너리: 인코딩·바이너리 구분 필요
- 파일 속성 분리 조회: Files.readAttributes 호출 필요
3. Java 8+: Stream API 활용
Java 8의 Stream API 등장으로 Files.lines()로 함수형 스타일 텍스트 처리가 가능해졌습니다.
- Files.lines(path)로 한 줄씩 스트림 처리
- 함수형 연산 (map, filter, collect)
- parallel()로 손쉬운 병렬화
예시코드
public class StreamFileExample {
/**
* 파일의 모든 줄을 읽어 반환합니다.
*/
public List<String> readLines(Path path) throws IOException {
// StandardCharsets default - UTF_8
try (Stream<String> lines = Files.lines(path)) {
return lines.collect(Collectors.toList());
}
}
/**
* 문자열 리스트를 파일에 라인 단위로 씁니다.
*/
public void writeLines(Path path, List<String> lines, StandardOpenOption... openOptions) throws IOException {
Files.createDirectories(path.getParent());
// StandardCharsets default - UTF_8
Files.write(path, lines, openOptions);
}
/**
* 지정된 파일을 삭제합니다.
*/
public void deleteFile(Path path) throws IOException {
Files.deleteIfExists(path);
}
/**
* 파일을 지정된 경로로 이동합니다.
*/
public void moveFile(Path src, Path dest) throws IOException {
Files.createDirectories(dest.getParent());
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
}
}
단점
- 텍스트 전용: 바이너리 처리 불가
- 메모리 과다 사용: 전체 라인 수집 시
- 여전히 블로킹 I/O
4. Java 7+: 비동기 I/O (AsynchronousFileChannel)
대용량·네트워크 마운트 환경에서 논블로킹 I/O를 위해 AsynchronousFileChannel을 활용 할 수 있게 되었습니다.
- 코드 복잡도 증가: Future/CompletionHandler 관리
- 채널 닫기 누수 위험
- 버퍼·스레드풀 직접 관리 필요
예시코드
// StringIOExample.java
public class StringIOExample {
/**
* 파일을 읽어 문자열로 반환
*/
public String readText(Path path) throws IOException {
return Files.readString(path, StandardCharsets.UTF_8);
}
/**
* 문자열을 파일에 기록 (덮어쓰기)
*/
public void writeText(Path path, String content) throws IOException {
Files.createDirectories(path.getParent());
Files.writeString(path, content,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
/**
* 파일 삭제
*/
public void deleteFile(Path path) throws IOException {
Files.deleteIfExists(path);
}
/**
* 파일 이동
*/
public void moveFile(Path src, Path dest) throws IOException {
Files.createDirectories(dest.getParent());
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
}
}
단점
- 텍스트 전용: 바이너리 처리 불가
- 메모리 과다 사용: 전체 라인 수집 시
- 여전히 블로킹 I/O
4. Java 7+: 비동기 I/O (AsynchronousFileChannel)
- Future 또는 CompletionHandler 기반 비동기 읽기/쓰기
- 논블로킹 I/O
- 높은 동시성
예시코드
public class AsyncFileExample {
/**
* 비동기 읽기 → Future<Integer> 반환
*/
public Future<Integer> readAsync(Path path, ByteBuffer buffer) throws IOException {
AsynchronousFileChannel ch = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
return ch.read(buffer, 0);
}
/**
* 비동기 쓰기 → Future<Integer> 반환
*/
public Future<Integer> writeAsync(Path path, ByteBuffer buffer) throws IOException {
AsynchronousFileChannel ch = AsynchronousFileChannel.open(
path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
return ch.write(buffer, 0);
}
/**
* 동기 파일 삭제
*/
public void deleteFile(Path path) throws IOException {
Files.deleteIfExists(path);
}
/**
* 동기 파일 이동
*/
public void moveFile(Path src, Path dest) throws IOException {
Files.createDirectories(dest.getParent());
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
}
}
단점
- 코드 복잡도 증가: Future/CompletionHandler 관리
- 채널 닫기 누수 위험
- 버퍼·스레드풀 직접 관리 필요
5. Java 11+: 편의 메서드 (Files.readString, Files.writeString)
- 편의 메서드 제공 : 텍스트 읽기·쓰기 (readString/writeString)
- 내부 버퍼링·인코딩 지정 지원
- 코드 간결
public class ConvenienceFileExample {
/**
* 파일을 읽어 문자열로 반환
*/
public String readText(Path path) throws IOException {
// default : StandardCharsets.UTF_8
return Files.readString(path);
}
/**
* 문자열을 파일에 기록 (덮어쓰기)
*/
public void writeText(Path path, String content, StandardOpenOption... openOptions) throws IOException {
Files.createDirectories(path.getParent());
Files.writeString(path, content, StandardCharsets.UTF_8, openOptions);
}
/**
* 파일 삭제
*/
public void deleteFile(Path path) throws IOException {
Files.deleteIfExists(path);
}
/**
* 파일 이동
*/
public void moveFile(Path src, Path dest) throws IOException {
Files.createDirectories(dest.getParent());
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
}
}
단점
- 텍스트 전용: 바이너리 처리 불가
- 대용량 비효율: 스트림 제어 불가
- 비동기 미지원
이번 글에서는 Java 버전에 따라 파일을 읽고 쓰기는 전체적인 흐름을 다루었고, 다음 글에서는 위에서 정리한 이론을 바탕으로, 직접 구현한 파일 입출력 코드를 단계별로 작성해 보려고 합니다.
728x90
'Backend > Java' 카테고리의 다른 글
[Java] RestTemplate 대신 WebClient를 선택하는 이유 (3) | 2025.05.25 |
---|---|
[Java] CompletableFuture를 활용한 비동기 프로그래밍 (1) | 2025.05.05 |
[Java] ☕ Executor 프레임 워크 (1) | 2025.04.27 |
[Java] 🚦 자바 메모리 가시성(Java Memory Visibility) (1) | 2025.04.20 |
[Java] ☕ Thread 정리: 메서드, 생명주기, 제어 방법 (0) | 2025.04.07 |