1. 파일을 줄 단위로 읽기: BufferedReader 등
이전에 바이트 단위 읽기를 살펴봤습니다. 이는 바이너리 포맷을 다룰 때 편리하지만, 텍스트에는 불편할 수 있습니다. 파일은 인코딩에 따라 달라지는 문자들로 구성되므로, Java는 편리한 추상화를 제공합니다 — FileReader, BufferedReader 같은 클래스가 바이트 스트림을 문자/문자열 스트림으로 변환해 줍니다.
텍스트 파일을 떠올려 보세요 — 프로그램 로그, 사용자 목록, 혹은 방대한 소설 “전쟁과 평화”일 수도 있습니다. 어떤 때는 파일 전체를 빠르게 한 번에 읽어야 하고, 어떤 때는 줄 단위로 순회해야 하며, 또 어떤 때는 특정 한 줄만 뽑아야 합니다.
Java에는 이를 처리하는 여러 방법이 있으며, 선택은 파일 크기와 과제에 달려 있습니다. 한 줄씩 처리해야 한다면(예: 줄 수를 세거나 특정 항목을 찾을 때) 줄 단위 읽기를 사용합니다. 파일이 작다면 메모리에 통째로 적재해 문자열 목록처럼 다루는 것도 가능합니다.
왜 줄 단위 읽기가 좋을까?
큰 파일의 경우 줄 단위 읽기는 메모리 문제를 피하게 해 줍니다. 기가바이트 로그를 메모리에 올리는 것은 좋지 않은 생각입니다: OutOfMemoryError가 날 확률이 큽니다. 반면 파일을 한 줄씩 읽으면 수백 메가바이트여도 큰 부담 없이 처리할 수 있습니다.
Java에서는 어떻게 할까?
가장 고전적인 방법은 BufferedReader(또는 그와 유사한 클래스)와 readLine() 메서드를 사용하는 것입니다.
import java.io.*;
public class ReadLinesDemo {
public static void main(String[] args) {
String fileName = "example.txt";
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
int lineNumber = 1;
while ((line = reader.readLine()) != null) {
System.out.printf("%3d: %s%n", lineNumber, line);
lineNumber++;
}
} catch (IOException e) {
System.out.println("파일을 읽는 중 오류: " + e.getMessage());
}
}
}
여기서는 파일을 열고 readLine()으로 한 줄씩 읽다가 끝(null)에 도달할 때까지 반복합니다. 그런 다음 각 줄을 번호와 함께 출력합니다.
try-with-resources 간단 소개
try (...) { ... } 구문이 보이나요? 이것이 try-with-resources입니다. 읽는 도중 오류가 발생하더라도 파일이 반드시 닫히도록 보장합니다. 직접 close()를 호출할 필요가 없습니다. catch/finally가 있어도 자동으로 닫힙니다.
왜 BufferedReader를 사용할까?
BufferedReader는 문자를 하나씩 읽지 않고 블록 단위(보통 8192 바이트)로 읽어 파일 처리 속도를 높여 줍니다. 또한 줄바꿈 전까지의 문자열을 바로 돌려주는 편리한 readLine() 메서드가 있습니다.
버퍼 크기는 어떻게 고를까?
보통 BufferedReader는 8192 바이트(8 KB) 크기의 버퍼를 사용하며, 이는 대부분의 작업에 충분합니다. 아주 긴 줄(예: 100_000자)을 읽는다면 버퍼를 늘릴 수 있습니다:
BufferedReader reader = new BufferedReader(new FileReader(fileName), 65536); // 64 KB 버퍼
하지만 일반적인 작업에는 기본 크기로 충분합니다.
2. 파일을 통째로 읽기: Files.readAllLines와 Files.readString
파일이 작다면(예: 10–20 MB 이하) 통째로 적재하는 것이 편리합니다. 예컨대 모든 줄 목록을 빠르게 얻거나, 텍스트를 분석하거나, 네트워크로 전송해야 할 때 유용합니다.
현대적인 방법: Files.readAllLines
Java 7부터 유용한 메서드가 많은 Files 클래스가 추가되었습니다.
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
public class ReadAllLinesDemo {
public static void main(String[] args) {
Path path = Path.of("example.txt");
try {
List<String> lines = Files.readAllLines(path);
for (int i = 0; i < lines.size(); i++) {
System.out.printf("%3d: %s%n", i + 1, lines.get(i));
}
} catch (IOException e) {
System.out.println("파일을 읽는 중 오류: " + e.getMessage());
}
}
}
여기서 Files.readAllLines(path)는 문자열 목록(List<String>)을 반환합니다. 일반 컬렉션처럼 검색, 정렬, 필터링을 할 수 있습니다.
더 최신: Files.readString (Java 11+)
파일 전체를 하나의 문자열로 얻어야 한다면(예: 부분 문자열 검색이나 JSON 전송) Files.readString을 사용하세요:
import java.nio.file.*;
import java.io.IOException;
public class ReadStringDemo {
public static void main(String[] args) {
Path path = Path.of("example.txt");
try {
String content = Files.readString(path);
System.out.println("파일 내용:");
System.out.println(content);
} catch (IOException e) {
System.out.println("파일을 읽는 중 오류: " + e.getMessage());
}
}
}
인코딩은 어떻게 할까?
기본적으로 Files.readAllLines와 Files.readString은 시스템의 기본 인코딩을 사용합니다. 파일이 다른 인코딩으로 저장되어 있다면(예: Windows-1251) 명시적으로 지정하세요:
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
String content = Files.readString(path, StandardCharsets.UTF_8);
3. 접근 방식 비교: 언제 무엇을 쓸까
| 방법 | 언제 사용 | 장점 | 단점 |
|---|---|---|---|
|
큰 파일, 줄 단위 처리 | 메모리 절약, 유연함 | 줄 단위로만 읽을 수 있음 |
|
작은/중간 크기 파일 | 바로 문자열 목록, 간단함 | 큰 파일에는 — OutOfMemory 위험 |
|
작은 파일, 전체 텍스트가 필요할 때 | 전체 텍스트를 한 번에 | 줄 단위 분할 없음 |
권장 사항:
— 파일이 작다면 Files.readAllLines 또는 Files.readString을 사용하세요.
— 파일이 크거나 크기를 모른다면 BufferedReader.readLine()을 사용하세요.
4. 실전 과제: 예제와 해설
예제 1. 큰 파일의 줄 수 세기
예를 들어, 아주 큰 파일(예: 서버 로그)에 줄이 몇 개인지 알아야 한다고 합시다.
import java.io.*;
public class LineCount {
public static void main(String[] args) {
String fileName = "biglog.txt";
int lineCount = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
while (reader.readLine() != null) {
lineCount++;
}
System.out.println("파일의 총 줄 수: " + lineCount);
} catch (IOException e) {
System.out.println("파일을 읽는 중 오류: " + e.getMessage());
}
}
}
예제 2. 내용으로 줄 찾기
대소문자 구분 없이 “오류”라는 단어가 포함된 모든 줄을 찾습니다:
import java.io.*;
public class FindErrorLines {
public static void main(String[] args) {
String fileName = "biglog.txt";
String keyword = "오류";
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
int lineNumber = 1;
while ((line = reader.readLine()) != null) {
if (line.toLowerCase().contains(keyword)) {
System.out.printf("%3d: %s%n", lineNumber, line);
}
lineNumber++;
}
} catch (IOException e) {
System.out.println("파일을 읽는 중 오류: " + e.getMessage());
}
}
}
예제 3. 작은 파일에서 구성 로드
파일 config.txt:
host=localhost
port=8080
mode=dev
파일을 통째로 읽어 키-값 쌍으로 파싱합니다:
import java.nio.file.*;
import java.util.*;
public class ConfigLoader {
public static void main(String[] args) throws Exception {
Path path = Path.of("config.txt");
List<String> lines = Files.readAllLines(path);
Map<String, String> config = new HashMap<>();
for (String line : lines) {
if (line.trim().isEmpty() || line.startsWith("#")) continue; // 빈 줄과 주석은 건너뜀
String[] parts = line.split("=", 2);
if (parts.length == 2) {
config.put(parts[0].trim(), parts[1].trim());
}
}
System.out.println("로드된 구성: " + config);
}
}
5. 텍스트 파일을 읽을 때 흔한 실수
오류 №1: 이진 파일을 텍스트로 읽으려는 시도. 이미지나 압축 파일을 BufferedReader나 Files.readAllLines로 열면 의미 없는 문자가 나오고 OutOfMemoryError 위험도 있습니다. 이진 파일에는 InputStream을 사용하세요!
오류 №2: 예외를 처리하지 않음. 파일은 삭제되거나, 이동되거나, 잠겨 있을 수 있습니다. 항상 읽기 코드를 try-catch로 감싸고 사용자에게 문제를 알려 주세요.
오류 №3: 인코딩을 무시함. 러시아어 텍스트가 있는데 인코딩을 지정하지 않고 읽으면 “?????” 같은 결과가 나올 수 있습니다. StandardCharsets.UTF_8 또는 필요한 인코딩을 사용하세요.
오류 №4: 스트림을 닫지 않음. 파일을 닫지 않으면 프로그램이 종료될 때까지 잠긴 상태로 남을 수 있습니다. 항상 try-with-resources를 사용하세요.
오류 №5: 텍스트 파일에 read()를 사용함. read() 메서드는 문자를 하나씩 읽어, 문자열 처리에 느리고 불편합니다. 텍스트에는 readLine()이나 Files의 메서드를 사용하세요.
GO TO FULL VERSION