본문으로 건너뛰기
Advertisement

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 실무 체크리스트:

  1. 항상 try-with-resources: close()를 빠뜨리면 파일 핸들 누수 → OS에서 파일을 잠금 처리합니다.

  2. 인코딩 항상 명시 (한글 필수):

    // ✅ 올바른 방법
    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")
  3. 용량별 전략 선택:

    소용량 (수 MB 미만): Files.readString() / writeString()
    중용량 (수백 MB): Files.readAllLines() 또는 BufferedReader
    대용량 (수 GB): Files.lines() + Stream (지연 읽기)
  4. 바이너리 파일은 NIO 사용:

    Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING); // 단순 복사
    byte[] data = Files.readAllBytes(path); // 바이트 배열로
  5. 파일 존재 여부 먼저 확인:

    Path path = Path.of("data.txt");
    if (Files.notExists(path)) {
    Files.createFile(path); // 없으면 생성
    }
    // 또는 FileWriter의 두 번째 인자(append)는 파일 없으면 자동 생성
Advertisement