
프로그램이 끝나면 책상 위는 치워집니다. 서랍에 넣어 둔 것만 남습니다.
지난 다섯 편으로 만든 프로그램에는 공통점이 하나 있습니다. 끄면 전부 사라진다는 것입니다. 4편에서 만든 출석부에 스무 명을 넣어도, 프로그램을 다시 켜면 빈 목록입니다. 컬렉션은 메모리에 있고, 메모리는 프로그램이 끝나는 순간 반납되기 때문입니다.
그래서 오늘은 남기는 법을 배웁니다. 파일에 쓰고, 다시 읽어 오는 것. 여기까지 하면 비로소 "어제 입력한 것이 오늘도 있는" 프로그램이 됩니다.
책상과 서랍
메모리와 파일의 관계는 책상과 서랍에 가깝습니다.
메모리는 책상 위입니다. 손이 바로 닿아 빠르지만, 퇴근하면 정리됩니다. 지금까지 쓴 변수·배열·컬렉션이 전부 여기 있었습니다.
파일은 서랍입니다. 꺼내는 데 한 단계가 더 필요해 느리지만, 문을 닫고 나가도 그대로 있습니다. 메모장으로 열리는 .txt, 설정을 담은 .json, 사진 .jpg — 전부 파일입니다.
프로그램이 데이터를 다루는 흐름은 결국 서랍에서 꺼내 책상에서 일하고 다시 서랍에 넣는 반복입니다. 그 꺼내고 넣는 일을 자바에서는 입출력(I/O, Input/Output) 이라고 부릅니다.
파일에 쓰기 — 한 줄이면 됩니다
예전 자바 책에는 파일에 글자 하나 쓰려고 클래스를 세 개씩 겹쳐 쓰는 코드가 나옵니다. 요즘은 Files 클래스가 짧은 길을 마련해 두었습니다.
import java.nio.file.Files;
import java.nio.file.Path;
Path path = Path.of("memo.txt"); // 다룰 파일을 가리킴
Files.writeString(path, "첫 줄입니다.\n"); // 쓰기 (없으면 새로 만듦)
Path는 파일의 주소입니다. 아직 파일을 여는 것도, 만드는 것도 아니고 "이 자리"를 가리키기만 합니다. 실제로 만들고 쓰는 것은 Files.writeString입니다.
주의할 점이 하나 있습니다. writeString은 기본이 덮어쓰기입니다. 같은 파일에 두 번 쓰면 앞의 내용은 사라집니다. 이어서 붙이고 싶다면 옵션을 하나 더 줍니다.
import java.nio.file.StandardOpenOption;
Files.writeString(path, "둘째 줄입니다.\n", StandardOpenOption.APPEND);
APPEND는 "뒤에 이어 붙여라"는 뜻입니다. 로그처럼 계속 쌓아야 하는 파일에 씁니다.
파일에서 읽기
읽는 쪽도 짝이 되는 메서드가 있습니다.
String all = Files.readString(path); // 통째로 한 덩어리
List<String> lines = Files.readAllLines(path); // 줄 단위로 나눠서
둘 중에는 줄 단위가 훨씬 자주 쓰입니다. 4편에서 배운 List로 돌아오니, 반복문으로 한 줄씩 처리하면 됩니다.
for (String line : Files.readAllLines(path)) {
System.out.println("읽음: " + line);
}
여기서 초보가 자주 놓치는 것 하나. 읽으려는 파일이 없으면 예외가 납니다. NoSuchFileException이 터지죠. 1편에서 배운 try-catch가 필요한 대표적인 자리입니다. 아니면 미리 확인할 수도 있습니다.
if (Files.exists(path)) { /* 읽기 */ }
반드시 닫아야 합니다
Files.writeString 같은 짧은 길은 자바가 알아서 열고 닫아 줍니다. 그런데 파일이 커지거나 한 줄씩 다뤄야 하면 직접 열고 닫는 방식을 씁니다. 이때가 중요합니다.

그림 1. 셋째를 빼먹으면 내용이 파일에 남지 않습니다.
BufferedWriter w = Files.newBufferedWriter(path);
w.write("한 줄 씁니다.");
// 닫지 않으면? 파일이 비어 있을 수 있습니다
왜 그럴까요. 성능 때문에 자바는 글자를 쓸 때마다 디스크로 보내지 않고 버퍼라는 임시 공간에 모읍니다. 어느 정도 차면 한 번에 내보내죠. 그런데 닫지 않고 프로그램이 끝나면, 버퍼에 남아 있던 글자는 디스크에 도착하지 못한 채 사라집니다. 코드에 오류가 없는데 파일만 비어 있는 상황이 이렇게 생깁니다.
그래서 닫는 것이 필수인데, 사람은 잊습니다. 자바는 이를 위해 문법을 하나 마련해 두었습니다.
try (BufferedWriter w = Files.newBufferedWriter(path)) {
w.write("한 줄 씁니다.");
w.newLine();
} // 여기서 자동으로 닫힙니다 — 중간에 예외가 나도 닫힙니다
괄호 안에서 자원을 여는 이 문법을 try-with-resources라고 부릅니다. 블록을 벗어나는 순간 자바가 대신 닫아 줍니다. 중간에 예외가 터져 빠져나가도 닫아 준다는 점이 특히 중요합니다. 1편의 finally로 직접 닫던 코드를 이 문법이 대체했다고 보면 됩니다.
단골 함정 — 한글이 깨집니다
파일에 쓴 한글이 ?????나 안녕처럼 보인 적이 있다면, 원인은 대개 인코딩입니다.
컴퓨터는 글자를 숫자로 바꿔 저장하는데, 그 변환표가 여러 개입니다. UTF-8로 적어 놓고 다른 표로 읽으면 엉뚱한 글자가 나옵니다. 한글이 특히 잘 깨지는 이유는 영어와 달리 표마다 값이 다르기 때문입니다.
다행히 Files.writeString과 readString은 기본이 UTF-8입니다. 요즘 표준이라 대부분 그대로 두면 됩니다. 다만 옛 코드에서 쓰는 FileWriter는 운영체제 설정을 따라가서, 윈도우에서는 다른 표가 잡히기도 합니다. 명시하는 습관이 안전합니다.
import java.nio.charset.StandardCharsets;
Files.writeString(path, "안녕하세요", StandardCharsets.UTF_8);
직접 실행해 보기
메모를 파일에 쌓고 다시 읽어 오는 프로그램입니다. 두 번 실행해 보세요. 두 번째 실행에서 앞서 쓴 줄이 그대로 남아 있는지가 오늘의 핵심입니다.
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.BufferedWriter;
import java.io.IOException;
import java.util.List;
public class Main {
public static void main(String[] args) {
Path path = Path.of("memo.txt");
try {
// 1. 처음이면 새로 만들고, 있으면 그대로 둡니다
if (!Files.exists(path)) {
Files.writeString(path, "== 메모장 ==\n");
System.out.println("파일을 새로 만들었습니다.");
}
// 2. 이어쓰기 — APPEND 가 없으면 앞의 내용이 지워집니다
Files.writeString(path, "오늘 배운 것: 파일 입출력\n",
StandardOpenOption.APPEND);
// 3. try-with-resources 로 한 줄 더 (자동으로 닫힘)
try (BufferedWriter w = Files.newBufferedWriter(path,
StandardOpenOption.APPEND)) {
w.write("닫는 것은 자바가 대신 합니다.");
w.newLine();
}
// 4. 줄 단위로 읽어 오기
System.out.println("--- 파일 내용 ---");
List<String> lines = Files.readAllLines(path);
for (int i = 0; i < lines.size(); i++) {
System.out.println((i + 1) + ": " + lines.get(i));
}
System.out.println("총 " + lines.size() + "줄");
} catch (IOException e) {
System.out.println("파일을 다루다 문제가 생겼습니다: " + e.getMessage());
}
}
}📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
catch (IOException e)가 전체를 감싸고 있는 것을 눈여겨보세요. 파일 작업은 디스크가 가득 찼거나, 권한이 없거나, 경로가 틀리는 등 내 코드 밖의 이유로 실패할 수 있습니다. 그래서 자바는 파일 관련 메서드에 예외 처리를 강제합니다. 2편에서 배운 throws가 왜 필요한지 여기서 실감하게 됩니다.
온라인 실행기에 따라 파일 만들기를 막아 두는 경우가 있습니다.
catch쪽 메시지가 보인다면 코드가 틀린 것이 아니라 그 환경의 제약이니, 내 컴퓨터에서 다시 돌려 보시기 바랍니다.
한 장 정리
| 하고 싶은 일 | 코드 |
|---|---|
| 파일 가리키기 | Path.of("memo.txt") |
| 통째로 쓰기 (덮어씀) | Files.writeString(path, 글자) |
| 이어서 쓰기 | Files.writeString(path, 글자, StandardOpenOption.APPEND) |
| 통째로 읽기 | Files.readString(path) |
| 줄 단위로 읽기 | Files.readAllLines(path) → List<String> |
| 있는지 확인 | Files.exists(path) |
| 자동으로 닫기 | try (자원 선언) { ... } |
이것만 기억하면 됩니다. 파일 작업은 열고 · 주고받고 · 닫는 세 걸음이고, 셋째를 빼먹으면 내용이 남지 않습니다. try-with-resources가 그 셋째를 대신해 줍니다. 그리고 덮어쓰기가 기본이라, 쌓아야 할 때는 APPEND를 잊지 마세요.
미니 퀴즈
개념을 제대로 잡았는지 직접 풀어 봅니다. 정답은 눌러야 나옵니다.
미니 퀴즈
포켓코딩(JAVA3) 6편 · 다음 편: 스레드 — 여러 일을 동시에 시키기.