15.6 파일 읽기와 쓰기 완전 정복
파일을 실제로 읽고 쓰는 방법을 처음부터 실무 수준까지 배웁니다. 자바는 텍스트 파일용과 바이너리(이진) 파일용으로 각각 다른 클래스를 제공합니다.
1. 파일 I/O 클래스 전체 지도
┌───────────────────────────────────────────┐
│ 자바 파일 I/O │
├─────────────────────┬─────────────────────┤
│ 텍스트 기반 │ 바이너리 기반 │
│ (Reader/Writer) │ (InputStream/OutputStream) │
├─────────────────────┼─────────────────────┤
│ FileReader │ FileInputStream │
│ FileWriter │ FileOutputStream │
│ BufferedReader │ BufferedInputStream │
│ BufferedWriter │ BufferedOutputStream │
│ PrintWriter │ DataInputStream │
│ InputStreamReader │ DataOutputStream │
└─────────────────────┴─────────────────────┘
↓ Java 7+ 권장 ↓
java.nio.file.Files API
(Files.readString / writeString / lines / ...)
2. 텍스트 파일 읽기
방법 1: BufferedReader + FileReader (기본)
import java.io.*;
public class TextReadBasic {
public static void main(String[] args) {
// try-with-resources: 블록이 끝나면 자동으로 close() 호출
try (BufferedReader br = new BufferedReader(new FileReader("hello.txt"))) {
String line;
int lineNum = 1;
while ((line = br.readLine()) != null) { // null = 파일 끝
System.out.printf("%3d번째 줄: %s%n", lineNum++, line);
}
} catch (FileNotFoundException e) {
System.out.println("❌ 파일 없음: " + e.getMessage());
} catch (IOException e) {
System.out.println("❌ 읽기 오류: " + e.getMessage());
}
}
}
방법 2: 한글 인코딩 지정 (실무 필수)
FileReader는 JVM 기본 인코딩을 사용합니다. 서버/OS마다 인코딩이 달라 한글이 깨질 수 있으므로 항상 UTF-8을 명시하는 것이 좋습니다.
import java.io.*;
import java.nio.charset.StandardCharsets;
public class TextReadEncoding {
public static void main(String[] args) {
// InputStreamReader로 인코딩 명시
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("korean.txt"),
StandardCharsets.UTF_8 // 명시적 UTF-8
))) {
br.lines() // Stream<String>으로 변환
.filter(line -> !line.isBlank()) // 빈 줄 제거
.map(String::trim) // 앞뒤 공백 제거
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}
방법 3: NIO Files (Java 11+, 가장 간결)
import java.nio.file.*;
import java.util.List;
public class TextReadNIO {
public static void main(String[] args) throws IOException {
Path path = Path.of("hello.txt");
// 파일 전체를 String 하나로 (소용량 파일에 적합)
String content = Files.readString(path);
System.out.println(content);
// 모든 줄을 List로 (중용량)
List<String> lines = Files.readAllLines(path);
lines.forEach(System.out::println);
// Stream으로 지연 읽기 (대용량 파일에 적합, 반드시 try-with-resources!)
try (var stream = Files.lines(path)) {
stream
.filter(line -> line.contains("ERROR"))
.forEach(System.out::println);
}
}
}
3. 텍스트 파일 쓰기
방법 1: BufferedWriter + FileWriter
import java.io.*;
public class TextWriteBasic {
public static void main(String[] args) {
// FileWriter(파일명, append여부)
// false(기본) = 덮어쓰기 / true = 이어쓰기
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt", false))) {
bw.write("첫 번째 줄");
bw.newLine(); // OS에 맞는 줄바꿈 (\n 또는 \r\n)
bw.write("두 번째 줄");
bw.newLine();
bw.write(String.format("세 번째 줄: 숫자=%d, 실수=%.2f", 42, 3.14));
System.out.println("✅ 파일 쓰기 완료");
} catch (IOException e) {
System.out.println("❌ 쓰기 오류: " + e.getMessage());
}
}
}
방법 2: PrintWriter (printf/println 지원)
import java.io.*;
public class TextWritePrintWriter {
public static void main(String[] args) throws IOException {
// PrintWriter: System.out.println처럼 편리하게 쓸 수 있음
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("report.txt")))) {
pw.println("=== 보고서 ===");
pw.printf("날짜: %s%n", java.time.LocalDate.now());
pw.printf("항목 수: %d개%n", 5);
pw.println();
pw.println("내용이 여기에...");
// flush 없이도 try-with-resources로 자동 처리
}
}
}
방법 3: NIO Files (Java 11+)
import java.nio.file.*;
// 한 줄로 쓰기 (가장 간결)
Files.writeString(Path.of("output.txt"), "파일 내용 전체");
// 여러 줄
Files.writeString(Path.of("output.txt"),
"첫째 줄\n둘째 줄\n셋째 줄");
// Text Block과 함께 (Java 15+)
String html = """
<html>
<body>
<h1>안녕하세요!</h1>
</body>
</html>
""";
Files.writeString(Path.of("index.html"), html);
// 이어쓰기
Files.writeString(Path.of("log.txt"), "추가 내용\n", StandardOpenOption.APPEND);
// 리스트 쓰기
List<String> lines = List.of("줄 1", "줄 2", "줄 3");
Files.write(Path.of("lines.txt"), lines);
4. 바이너리 파일 읽기/쓰기
이미지, PDF, ZIP, 동영상 등 바이너리 데이터는 바이트 기반 스트림을 사용합니다.
파일 복사
import java.io.*;
public class BinaryFileCopy {
public static void main(String[] args) {
String src = "photo.jpg";
String dst = "photo_backup.jpg";
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dst))
) {
byte[] buffer = new byte[8192]; // 8KB 버퍼 (성능 최적값)
int bytesRead;
long totalBytes = 0;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
}
System.out.printf("✅ 복사 완료: %s → %s (%,d bytes)%n", src, dst, totalBytes);
} catch (FileNotFoundException e) {
System.out.println("❌ 파일 없음: " + e.getMessage());
} catch (IOException e) {
System.out.println("❌ 복사 오류: " + e.getMessage());
}
}
}
더 간단한 방법 (NIO)
// 한 줄로 파일 복사!
Files.copy(Path.of("photo.jpg"), Path.of("photo_backup.jpg"),
StandardCopyOption.REPLACE_EXISTING);
5. 파일 읽기/쓰기 방법 비교표
| 방법 | 장점 | 단점 | 추천 상황 |
|---|---|---|---|
Files.readString() | 가장 간결 | 대용량에 위험 (OOM) | 설정파일, 소용량 텍스트 |
Files.readAllLines() | List로 바로 사용 | 전체 메모리 로드 | 중용량, 줄 단위 처리 |
Files.lines() | 지연 읽기, 스트림 | try-with-resources 필수 | 대용량 로그 분석 |
BufferedReader | 전통적, 세밀한 제어 | 코드가 길어짐 | 다양한 환경 지원 필요 |
BufferedInputStream | 바이너리 데이터 | 텍스트 변환 불편 | 이미지, PDF 등 |
Files.copy() | 한 줄 복사 | 변환 불가 | 단순 파일 복사/이동 |
6. 실전 예제 1: CSV 파일 처리기
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;
public class CsvProcessor {
record Student(String name, int korean, int math, int english) {
int total() { return korean + math + english; }
double avg() { return total() / 3.0; }
String grade() {
return switch ((int) avg() / 10) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> "C";
case 6 -> "D";
default -> "F";
};
}
}
public static void main(String[] args) throws IOException {
// 1. 테스트용 CSV 파일 생성
String csv = """
이름,국어,수학,영어
홍길동,85,90,88
김철수,72,65,80
이영희,95,98,92
박민수,60,70,55
최지수,88,82,91
""";
Files.writeString(Path.of("students.csv"), csv);
System.out.println("📄 students.csv 생성 완료\n");
// 2. CSV 읽어서 파싱
List<Student> students;
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("students.csv"),
java.nio.charset.StandardCharsets.UTF_8))) {
students = br.lines()
.skip(1) // 헤더(첫째 줄) 건너뜀
.filter(line -> !line.isBlank())
.map(line -> {
String[] parts = line.split(",");
return new Student(
parts[0].trim(),
Integer.parseInt(parts[1].trim()),
Integer.parseInt(parts[2].trim()),
Integer.parseInt(parts[3].trim())
);
})
.collect(Collectors.toList());
}
// 3. 통계 분석
DoubleSummaryStatistics stats = students.stream()
.mapToDouble(Student::avg)
.summaryStatistics();
Student top = students.stream()
.max(Comparator.comparingDouble(Student::avg)).orElseThrow();
Student last = students.stream()
.min(Comparator.comparingDouble(Student::avg)).orElseThrow();
Map<String, List<Student>> byGrade = students.stream()
.collect(Collectors.groupingBy(Student::grade));
// 4. 결과를 result.txt에 저장
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new OutputStreamWriter(
new FileOutputStream("result.txt"),
java.nio.charset.StandardCharsets.UTF_8)))) {
pw.println("╔══════════════════════════════╗");
pw.println("║ 성적 분석 결과 ║");
pw.println("╚══════════════════════════════╝");
pw.println();
pw.println("[ 전체 통계 ]");
pw.printf(" 학생 수 : %d명%n", students.size());
pw.printf(" 평균 점수 : %.1f점%n", stats.getAverage());
pw.printf(" 최고 평균 : %s (%.1f점)%n", top.name(), top.avg());
pw.printf(" 최저 평균 : %s (%.1f점)%n", last.name(), last.avg());
pw.println();
pw.println("[ 개인별 성적표 ]");
pw.printf("%-8s %4s %4s %4s %5s %6s %4s%n",
"이름", "국어", "수학", "영어", "합계", "평균", "등급");
pw.println("─".repeat(45));
students.stream()
.sorted(Comparator.comparingDouble(Student::avg).reversed())
.forEach(s -> pw.printf("%-8s %4d %4d %4d %5d %6.1f %4s%n",
s.name(), s.korean(), s.math(), s.english(),
s.total(), s.avg(), s.grade()));
pw.println();
pw.println("[ 등급별 현황 ]");
byGrade.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> {
String names = e.getValue().stream()
.map(Student::name)
.collect(Collectors.joining(", "));
pw.printf(" %s등급: %s%n", e.getKey(), names);
});
}
System.out.println("✅ 분석 결과가 result.txt에 저장되었습니다.\n");
// 5. 저장된 파일 내용 출력
System.out.println(Files.readString(Path.of("result.txt")));
}
}
[result.txt 출력 결과]
╔══════════════════════════════╗
║ 성적 분석 결과 ║
╚══════════════════════════════╝
[ 전체 통계 ]
학생 수 : 5명
평균 점수 : 80.5점
최고 평균 : 이영희 (95.0점)
최저 평균 : 박민수 (61.7점)
[ 개인별 성적표 ]
이름 국어 수학 영어 합계 평균 등급
─────────────────────────────────────────────
이영희 95 98 92 285 95.0 A
최지수 88 82 91 261 87.0 B
홍길동 85 90 88 263 87.7 B
김철수 72 65 80 217 72.3 C
박민수 60 70 55 185 61.7 D
[ 등급별 현황 ]
A등급: 이영희
B등급: 최지수, 홍길동
C등급: 김철수
D등급: 박민수
7. 실전 예제 2: 로그 파일 분석기
대용량 로그 파일에서 특정 패턴을 찾는 실무 패턴입니다.
import java.io.*;
import java.nio.file.*;
import java.time.*;
import java.util.*;
import java.util.stream.*;
import java.util.regex.*;
public class LogAnalyzer {
record LogEntry(String timestamp, String level, String message) {}
// 로그 파일 생성 (테스트용)
static void generateSampleLog(Path path) throws IOException {
List<String> logs = List.of(
"2024-03-15 10:01:23 [INFO] 서버 시작",
"2024-03-15 10:01:25 [INFO] 데이터베이스 연결 성공",
"2024-03-15 10:02:11 [INFO] 사용자 로그인: user001",
"2024-03-15 10:03:45 [WARN] 메모리 사용량 75% 초과",
"2024-03-15 10:04:12 [ERROR] DB 쿼리 타임아웃: SELECT * FROM orders",
"2024-03-15 10:04:13 [INFO] 재시도 중...",
"2024-03-15 10:04:15 [INFO] 쿼리 재시도 성공",
"2024-03-15 10:05:30 [WARN] 메모리 사용량 85% 초과",
"2024-03-15 10:06:00 [ERROR] 외부 API 연결 실패: payment-service",
"2024-03-15 10:06:01 [ERROR] 결제 처리 실패: orderId=12345",
"2024-03-15 10:07:00 [INFO] 사용자 로그아웃: user001",
"2024-03-15 10:08:00 [WARN] 메모리 사용량 90% 초과",
"2024-03-15 10:09:00 [ERROR] OutOfMemoryError 발생"
);
Files.write(path, logs);
}
// 로그 파싱 (정규식 활용)
static Optional<LogEntry> parseLine(String line) {
Pattern p = Pattern.compile(
"(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) \\[(\\w+)\\s*\\] (.+)");
Matcher m = p.matcher(line);
if (m.matches()) {
return Optional.of(new LogEntry(m.group(1), m.group(2).trim(), m.group(3).trim()));
}
return Optional.empty();
}
public static void main(String[] args) throws IOException {
Path logPath = Path.of("app.log");
Path reportPath = Path.of("log_report.txt");
// 샘플 로그 생성
generateSampleLog(logPath);
System.out.println("📄 app.log 생성 완료 (" + Files.size(logPath) + " bytes)\n");
// 대용량 파일을 Stream으로 지연 읽기
List<LogEntry> entries;
try (Stream<String> lines = Files.lines(logPath)) {
entries = lines
.map(LogAnalyzer::parseLine)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
}
// 분석
Map<String, Long> countByLevel = entries.stream()
.collect(Collectors.groupingBy(LogEntry::level, Collectors.counting()));
List<LogEntry> errors = entries.stream()
.filter(e -> "ERROR".equals(e.level()))
.collect(Collectors.toList());
List<LogEntry> warnings = entries.stream()
.filter(e -> "WARN".equals(e.level()))
.collect(Collectors.toList());
// 결과 리포트 저장
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter(reportPath.toFile())))) {
pw.printf("=== 로그 분석 리포트 ===%n");
pw.printf("분석 파일: %s%n", logPath);
pw.printf("전체 라인: %d개%n%n", entries.size());
pw.println("[ 레벨별 통계 ]");
countByLevel.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.forEach(e -> pw.printf(" %-6s: %d건%n", e.getKey(), e.getValue()));
pw.printf("%n[ ERROR 목록 (%d건) ]%n", errors.size());
errors.forEach(e ->
pw.printf(" %s - %s%n", e.timestamp(), e.message()));
pw.printf("%n[ WARN 목록 (%d건) ]%n", warnings.size());
warnings.forEach(e ->
pw.printf(" %s - %s%n", e.timestamp(), e.message()));
}
System.out.println("✅ 리포트 저장 완료: " + reportPath);
System.out.println(Files.readString(reportPath));
}
}
[log_report.txt 출력]
=== 로그 분석 리포트 ===
분석 파일: app.log
전체 라인: 13개
[ 레벨별 통계 ]
INFO : 6건
ERROR : 4건
WARN : 3건
[ ERROR 목록 (4건) ]
2024-03-15 10:04:12 - DB 쿼리 타임아웃: SELECT * FROM orders
2024-03-15 10:06:00 - 외부 API 연결 실패: payment-service
2024-03-15 10:06:01 - 결제 처리 실패: orderId=12345
2024-03-15 10:09:00 - OutOfMemoryError 발생
[ WARN 목록 (3건) ]
2024-03-15 10:03:45 - 메모리 사용량 75% 초과
2024-03-15 10:05:30 - 메모리 사용량 85% 초과
2024-03-15 10:08:00 - 메모리 사용량 90% 초과
8. 실전 예제 3: 설정 파일(Properties) 읽기/쓰기
.properties 파일은 자바 애플리케이션 설정에서 표준으로 사용됩니다.
import java.io.*;
import java.util.Properties;
public class PropertiesExample {
public static void main(String[] args) throws IOException {
// 1. 설정 파일 쓰기
Properties writeProps = new Properties();
writeProps.setProperty("db.host", "localhost");
writeProps.setProperty("db.port", "3306");
writeProps.setProperty("db.name", "myapp");
writeProps.setProperty("db.user", "admin");
writeProps.setProperty("app.version", "1.0.0");
writeProps.setProperty("app.debug", "false");
try (BufferedWriter bw = new BufferedWriter(new FileWriter("config.properties"))) {
writeProps.store(bw, "애플리케이션 설정 파일"); // 주석도 함께 저장
}
System.out.println("📄 config.properties 생성 완료");
// 2. 설정 파일 읽기
Properties props = new Properties();
try (BufferedReader br = new BufferedReader(new FileReader("config.properties"))) {
props.load(br);
}
String host = props.getProperty("db.host");
int port = Integer.parseInt(props.getProperty("db.port"));
String dbName = props.getProperty("db.name");
boolean debug = Boolean.parseBoolean(props.getProperty("app.debug"));
String version = props.getProperty("app.version", "0.0.1"); // 없으면 기본값
System.out.printf("DB: %s:%d/%s%n", host, port, dbName);
System.out.printf("앱 버전: %s, 디버그: %b%n", version, debug);
// 3. 설정값 수정 후 다시 저장
props.setProperty("app.debug", "true");
props.setProperty("app.version", "1.0.1");
try (BufferedWriter bw = new BufferedWriter(new FileWriter("config.properties"))) {
props.store(bw, "업데이트된 설정");
}
System.out.println("✅ 설정 업데이트 완료");
}
}
[config.properties 내용]
#애플리케이션 설정 파일
#Fri Mar 15 10:00:00 KST 2024
db.host=localhost
db.port=3306
db.name=myapp
db.user=admin
app.version=1.0.0
app.debug=false
고수 팁
파일 I/O 실무 체크리스트:
-
항상 try-with-resources:
close()를 빠뜨리면 파일 핸들 누수 → OS에서 파일을 잠금 처리합니다. -
인코딩 항상 명시 (한글 필수):
// ✅ 올바른 방법
new InputStreamReader(new FileInputStream("file.txt"), StandardCharsets.UTF_8)
new OutputStreamWriter(new FileOutputStream("file.txt"), StandardCharsets.UTF_8)
// ❌ 위험 (OS 기본 인코딩 사용 → 서버마다 다름)
new FileReader("file.txt")
new FileWriter("file.txt") -
용량별 전략 선택:
소용량 (수 MB 미만): Files.readString() / writeString()
중용량 (수백 MB): Files.readAllLines() 또는 BufferedReader
대용량 (수 GB): Files.lines() + Stream (지연 읽기) -
바이너리 파일은 NIO 사용:
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING); // 단순 복사
byte[] data = Files.readAllBytes(path); // 바이트 배열로 -
파일 존재 여부 먼저 확인:
Path path = Path.of("data.txt");
if (Files.notExists(path)) {
Files.createFile(path); // 없으면 생성
}
// 또는 FileWriter의 두 번째 인자(append)는 파일 없으면 자동 생성