1. 파일 전체 읽기
모든 바이트 읽기
때로는 이미지, 아카이브, 기타 바이너리 포맷 등 파일의 모든 내용을 한 번에 그대로 꺼낼 필요가 있습니다. 이를 위해 Java에는 메서드 Files.readAllBytes(path)가 있습니다. 이는 바이트 배열(byte[])을 반환하며, 즉 파일의 원시 데이터를 돌려주므로 이후 원하는 대로 사용할 수 있습니다.
import java.nio.file.*;
public class ReadBytesExample {
public static void main(String[] args) throws Exception {
Path path = Paths.get("example.txt");
byte[] allBytes = Files.readAllBytes(path);
System.out.println("파일 길이(바이트): " + allBytes.length);
}
}
참고: 파일이 매우 크면(기가바이트 단위) 이런 방식은 메모리 문제를 일으킬 수 있습니다 — 파일의 모든 내용을 한 번에 메모리에 올리기 때문입니다.
모든 줄 읽기
텍스트 파일에는 더 사람 친화적인 방법이 있습니다: Files.readAllLines(path). 이는 문자열 목록(List<String>)을 반환하며, 각 원소가 파일의 한 줄입니다(줄 구분자는 자동으로 처리됩니다).
import java.nio.file.*;
import java.util.List;
public class ReadLinesExample {
public static void main(String[] args) throws Exception {
Path path = Paths.get("example.txt");
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
System.out.println(line);
}
}
}
중요: 기본적으로 시스템 인코딩이 사용됩니다. 인코딩을 명시적으로 지정하고 싶다면(예: UTF-8) 오버로드된 버전을 사용하세요:
List<String> lines = Files.readAllLines(path, java.nio.charset.StandardCharsets.UTF_8);
예제: 파일을 읽어 화면에 출력
이것을 우리의 미니 애플리케이션(예: “메모장” 또는 “할 일 목록”)에 추가해 봅시다:
import java.nio.file.*;
import java.util.List;
public class TodoReader {
public static void main(String[] args) throws Exception {
Path todoPath = Paths.get("todo.txt");
if (Files.exists(todoPath)) {
List<String> tasks = Files.readAllLines(todoPath);
System.out.println("오늘의 할 일:");
for (String task : tasks) {
System.out.println("- " + task);
}
} else {
System.out.println("todo.txt 파일을 찾을 수 없습니다. 할 일 목록을 만들어 보세요!");
}
}
}
2. 파일에 쓰기
바이트 배열 쓰기
바이트 배열(예: 직렬화 결과, 이미지, 오디오 등)이 있다면 Files.write(path, bytes)를 사용하세요. 이 메서드는 파일이 없으면 생성하고, 존재하면 덮어씁니다.
import java.nio.file.*;
public class WriteBytesExample {
public static void main(String[] args) throws Exception {
Path path = Paths.get("output.bin");
byte[] data = {1, 2, 3, 4, 5};
Files.write(path, data);
System.out.println("바이트가 output.bin 파일에 기록되었습니다");
}
}
문자열 목록 쓰기
텍스트 파일에는 문자열 목록을 사용하는 편이 더 편리합니다 — 각 문자열이 한 줄로 기록됩니다.
import java.nio.file.*;
import java.util.Arrays;
import java.util.List;
public class WriteLinesExample {
public static void main(String[] args) throws Exception {
Path path = Paths.get("todo.txt");
List<String> tasks = Arrays.asList(
"우유 사기",
"할머니께 전화하기",
"Java 숙제하기"
);
Files.write(path, tasks);
System.out.println("할 일 목록이 todo.txt에 기록되었습니다");
}
}
주의: 기본적으로 파일은 덮어쓰입니다! 이미 존재했다면 이전 작업들이 사라집니다. (이전 내용을 잃지 않는 방법은 다음에 설명합니다.)
문자열을 파일에 쓰기
한 줄만 쓰고 싶다면 한 개의 문자열로 목록을 만들거나 Files.write(path, string.getBytes())를 사용하세요:
import java.nio.file.*;
public class WriteStringExample {
public static void main(String[] args) throws Exception {
Path path = Paths.get("hello.txt");
String message = "안녕, Java!";
Files.write(path, message.getBytes());
System.out.println("문자열이 hello.txt에 기록되었습니다");
}
}
인코딩을 명시적으로 지정하기
텍스트가 깨지는 문제를 피하려면(특히 비 ASCII 텍스트) 항상 인코딩을 지정하세요:
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
public class WriteLinesUtf8Example {
public static void main(String[] args) throws Exception {
Path path = Paths.get("hello.txt");
List<String> lines = List.of("안녕, 세상!", "이것은 Java입니다.");
Files.write(path, lines, StandardCharsets.UTF_8);
}
}
3. 오류 처리
파일 작업 중 Java는 IOException을 던질 수 있습니다. 이는 입출력 단계에서 “뭔가 잘못되었다”는 일반적인 신호입니다. 원인은 다양합니다: 파일이 없거나, 권한이 부족하거나, 디스크가 가득 찼거나 바쁘거나, 심지어 작업 중에 USB 드라이브가 뽑혔을 수도 있습니다.
그래서 파일 읽기/쓰기 메서드는 항상 이런 상황을 처리할 준비를 요구합니다. 이는 try-catch로 처리합니다:
import java.nio.file.*;
import java.util.List;
import java.io.IOException;
public class SafeReadExample {
public static void main(String[] args) {
Path path = Paths.get("todo.txt");
try {
List<String> lines = Files.readAllLines(path);
System.out.println("파일 내용:");
for (String line : lines) {
System.out.println(line);
}
} catch (IOException ex) {
System.out.println("파일 읽기 중 오류: " + ex.getMessage());
}
}
}
왜 이렇게 할까요?
try-catch는 잠재적인 프로그램 크래시를 통제 가능한 상황으로 바꿉니다. 파일이 없으면 — 사용자에게 차분히 알려 줍니다. 시스템이 접근을 허용하지 않으면 — 문제를 지적합니다. 그리고 작업 중 디스크가 사라져도 프로그램이 무시무시한 스택 트레이스와 함께 무너지지 않고, 최소한 정상적인 메시지로 경고합니다. 다시 말해, IOException은 적이 아니라 Java가 우리에게 “이봐, 바깥에서 뭔가 잘못됐어. 다음에 무엇을 할지 정해 봐”라고 알려 주는 방식일 뿐입니다.
4. 실전 예제
파일을 읽어 내용 출력
import java.nio.file.*;
import java.util.List;
import java.io.IOException;
public class PrintFileExample {
public static void main(String[] args) {
Path path = Paths.get("notes.txt");
try {
if (!Files.exists(path)) {
System.out.println("notes.txt 파일을 찾을 수 없습니다.");
return;
}
List<String> lines = Files.readAllLines(path, java.nio.charset.StandardCharsets.UTF_8);
System.out.println("notes.txt 파일 내용:");
for (String line : lines) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("파일 읽기 중 오류: " + e.getMessage());
}
}
}
새 파일에 문자열 쓰기
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
public class WriteFileExample {
public static void main(String[] args) {
Path path = Paths.get("greeting.txt");
String content = "Java IO의 세계에 오신 것을 환영합니다!";
try {
Files.write(path, content.getBytes(StandardCharsets.UTF_8));
System.out.println("문자열이 파일 greeting.txt에 기록되었습니다");
} catch (IOException e) {
System.out.println("파일 쓰기 중 오류: " + e.getMessage());
}
}
}
문자열 목록을 파일에 쓰기
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.io.IOException;
public class WriteListExample {
public static void main(String[] args) {
Path path = Paths.get("shopping.txt");
List<String> items = List.of("빵", "우유", "치즈");
try {
Files.write(path, items, StandardCharsets.UTF_8);
System.out.println("쇼핑 목록이 shopping.txt에 기록되었습니다");
} catch (IOException e) {
System.out.println("파일 쓰기 중 오류: " + e.getMessage());
}
}
}
5. 스트림 간단 소개
Files.readAllBytes, Files.readAllLines, Files.write 메서드는 “모두 한 번에” 접근입니다: 파일 전체를 메모리로 읽거나, 내용을 한 번에 디스크에 씁니다. 작은 파일에는 편리하지만, 큰 파일에는 그다지 좋지 않습니다( OutOfMemoryError가 날 수 있거나 기가바이트 단위 로그를 읽다가 멈출 수 있습니다).
큰 파일이나 줄 단위 처리에는 스트림을 사용합니다:
- 텍스트용: BufferedReader, BufferedWriter
- 바이트용: InputStream, OutputStream
스트림에 대해서는 다음 강의에서 더 자세히 다루겠지만, 간단한 예시를 보겠습니다:
import java.nio.file.*;
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) {
Path path = Paths.get("bigfile.txt");
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
// 줄을 처리합니다
System.out.println(line);
}
} catch (IOException e) {
System.out.println("파일 읽기 중 오류: " + e.getMessage());
}
}
}
6. 유용한 팁
- 파일 덮어쓰기: Files.write 메서드는 기본적으로 파일을 덮어씁니다. 끝에 추가하려면 추가 옵션을 사용하세요(이에 대해서는 다음 강의에서 다룹니다).
- 인코딩: 비 ASCII 텍스트를 다룰 때는 항상 인코딩을 명시적으로 지정하세요. 출력 시 문자 깨짐을 방지합니다. 좋은 선택은 StandardCharsets.UTF_8입니다.
- 경로: 크로스플랫폼을 위해 Paths.get(...)을 사용하세요. 슬래시(/ 또는 \)를 직접 하드코딩하지 마세요.
- 파일 이름: 파일 이름에 금지된 문자(?, *, :, <, >, | 등)를 사용하지 마세요. 특히 Windows에서 코드가 동작해야 한다면 더욱 그렇습니다.
7. 파일 읽기/쓰기에서 흔한 실수
오류 №1: IOException을 처리하지 않음. 초보자는 종종 Files.readAllLines(path)를 try-catch 없이 호출하고, 첫 문제(파일 없음, 권한 없음, 디스크 손상 등)에서 프로그램이 곧바로 종료됩니다. 항상 예외를 처리하세요!
오류 №2: 큰 파일을 readAllBytes/readAllLines로 처리. 파일 크기가 수백 MB 또는 기가바이트라면, 전체를 로드하려는 시도는 프로그램을 무너뜨릴 수 있습니다. 이런 경우에는 스트림(BufferedReader)을 사용하세요.
오류 №3: 텍스트 읽기/쓰기에서 인코딩을 지정하지 않음. 인코딩을 지정하지 않으면 컴퓨터와 OS에 따라 결과가 달라질 수 있습니다. 특히 키릴 문자에서 문제가 자주 드러납니다. StandardCharsets.UTF_8 또는 필요한 인코딩을 명시적으로 사용하세요.
오류 №4: File/Path가 실제 파일이라고 기대함. File 또는 Path 클래스는 실제 파일이 아니라 파일에 대한 “참조”일 뿐입니다. 객체를 만들었다고 해서 디스크에 파일이 생성되는 것은 아닙니다. 파일을 생성하려면 Files.createFile, Files.write 등의 메서드를 사용하세요.
오류 №5: 스트림을 닫지 않음(스트림을 사용하는 경우). 스트림을 수동으로 사용하는 경우(예: BufferedReader), 반드시 닫으세요(가급적 try-with-resources 사용). 그렇지 않으면 파일이 “잠긴 상태”로 남아 다른 프로그램에서 접근하지 못할 수 있습니다.
GO TO FULL VERSION