공공부하자개발 · 영어 학습 노트
자바
명령어자주 쓰는 API 참조0/8 완료
  • 01String · StringBuilder · 정규식
  • 02Collections · Arrays · Map
  • 03Stream · Collectors · 함수형 인터페이스
  • 04Files · Path · I/O
  • 05java.time · BigDecimal · 기타 유틸
  • 06Oracle SQL
  • 07예외 · 디버깅
  • 08Spring Boot 설정 키
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 명령어 › 04 / 8

Files · Path · I/O

섹션 13진행 0 / 8

Files · Path · I/O 명령어

java.nio.file(Path, Files)을 중심으로 파일 읽기/쓰기/복사/이동/삭제/순회, 옵션 상수, 인코딩, java.io 스트림 계층, 직렬화, Properties, Zip/GZIP, 임시 파일, WatchService까지 표로 정리했다. Ctrl+F로 메서드명(예: walk, REPLACE_EXISTING, newBufferedReader, createTempFile)을 검색하면 된다. 결과는 JDK 21 / Windows에서 실행한 값이며, 경로 구분자는 Windows 기준(\)이다. Linux/macOS에서는 /로 나온다.

01한눈에 보기
메서드 설명 예제 결과
Path.of(a, b, ...) 경로 생성 Path.of("data", "logs", "app.log") data\logs\app.log
path.resolve(child) 하위 경로 결합 Path.of("data").resolve("app.log") data\app.log
path.getFileName() 파일명 p.getFileName() app.log
path.getParent() 상위 디렉터리 p.getParent() data\logs
Files.exists(p) 존재 여부 Files.exists(f) true
Files.readString(p) 전체를 문자열로 Files.readString(f) "line1\nline2\n한글\n"
Files.readAllLines(p) 줄 리스트 Files.readAllLines(f) [line1, line2, 한글]
Files.lines(p) 줄 스트림 (close 필수) try (var s = Files.lines(f)) { s.count(); } 3
Files.writeString(p, s) 문자열 쓰기 (덮어쓰기) Files.writeString(f, "hi") 경로 반환
Files.writeString(p, s, APPEND) 이어 쓰기 Files.writeString(f, "more\n", StandardOpenOption.APPEND) 줄 수 4
Files.write(p, lines) 줄 리스트 쓰기 Files.write(p, List.of("x", "y")) 파일 생성
Files.createDirectories(p) 중간 경로까지 생성 Files.createDirectories(base.resolve("d1/d2/d3")) d1\d2\d3
Files.copy(src, dst, REPLACE_EXISTING) 복사 Files.copy(f, dst, StandardCopyOption.REPLACE_EXISTING) 대상 경로
Files.move(src, dst) 이동/이름 변경 Files.move(a, b) 대상 경로
Files.delete(p) / deleteIfExists(p) 삭제 Files.deleteIfExists(missing) false
Files.size(p) 바이트 크기 Files.size(f) 6
Files.list(dir) 1단계 목록 스트림 try (var s = Files.list(d)) { ... } [d2, e.txt]
Files.walk(dir) 재귀 순회 스트림 try (var s = Files.walk(d)) { ... } 모든 하위
Files.newBufferedReader(p) 큰 파일 줄 단위 읽기 try (var r = Files.newBufferedReader(p)) { r.readLine(); } "a"
Files.newBufferedWriter(p) 큰 파일 쓰기 try (var w = Files.newBufferedWriter(p)) { w.write("a"); } 파일 생성
02Path 조작
메서드/시그니처 설명 한 줄 예제 (p = data\logs\app.log) 결과
Path.of(String, String...) 경로 생성 (JDK 11+) Path.of("data", "logs", "app.log") data\logs\app.log
Paths.get(String) 구식 생성 (동일) Paths.get("a/b.txt") a\b.txt
getFileName() 마지막 요소 p.getFileName() app.log
getParent() 상위 (없으면 null) p.getParent() data\logs
getRoot() 루트 (상대 경로면 null) p.getRoot() + " " + Path.of("C:/x").getRoot() null C:\
getNameCount() 요소 수 p.getNameCount() 3
getName(i) i번째 요소 p.getName(0) data
subpath(a, b) 요소 구간 p.subpath(1, 3) logs\app.log
resolve(other) 하위 결합 (other가 절대면 other) Path.of("data").resolve("app.log") data\app.log
resolve(절대경로) 절대 경로는 그대로 Path.of("data").resolve("C:/abs/x.txt") C:\abs\x.txt
resolveSibling(name) 형제 경로 p.resolveSibling("app.bak") data\logs\app.bak
relativize(other) 상대 경로 계산 Path.of("a/b").relativize(Path.of("a/b/c/d.txt")) c\d.txt
relativize 상위로 .. 포함 Path.of("a/b/c").relativize(Path.of("a/x")) ..\..\x
normalize() ./.. 정리 Path.of("a/./b/../c").normalize() a\c
isAbsolute() 절대 경로 여부 p.isAbsolute() + " " + Path.of("C:/x").isAbsolute() false true
toAbsolutePath() 작업 디렉터리 기준 절대화 Path.of("x").toAbsolutePath() C:\...\x
toRealPath() 심볼릭 링크 해석 (존재해야 함) p.toRealPath() 실제 경로 (IOException 가능)
startsWith(s) / endsWith(s) 요소 단위 비교 p.startsWith("dat") + " " + p.endsWith(".log") false false
startsWith("data") / endsWith("app.log") 요소 일치 왼쪽 true true
toString() 문자열 (OS 구분자) p.toString() data\logs\app.log
toUri() URI Path.of("C:/a b.txt").toUri() file:///C:/a%20b.txt
toFile() / File.toPath() java.io 상호 변환 new File("x.txt").toPath() x.txt
iterator() / spliterator() 요소 순회 StreamSupport.stream(p.spliterator(), false).toList() [data, logs, app.log]
equals / compareTo 경로 비교 (문자열 기반) Path.of("a/b").equals(Path.of("a", "b")) true
File.separator / File.pathSeparator 구분자 상수 "[" + File.separator + "] [" + File.pathSeparator + "]" [\] [;]
FileSystems.getDefault().getSeparator() 동일 왼쪽 \
확장자 추출 (표준 메서드 없음) lastIndexOf('.') n.substring(n.lastIndexOf('.') + 1) log
  • Path는 파일이 실제로 존재하지 않아도 만들 수 있다. 존재 검사는 Files.exists.
  • 문자열 결합(dir + "/" + name) 대신 resolve를 쓰면 OS 구분자 문제가 사라진다.
03Files 읽기 / 쓰기
메서드/시그니처 설명 한 줄 예제 결과
readString(p) 전체 문자열 (UTF-8, 작은 파일) Files.readString(f) "line1\nline2\n한글\n"
readString(p, charset) 인코딩 지정 Files.readString(p, Charset.forName("EUC-KR")) "한글"
readAllLines(p) List<String> (메모리 전체 적재) Files.readAllLines(f) [line1, line2, 한글]
readAllBytes(p) byte[] Files.readAllBytes(p).length 6
lines(p) Stream<String> (lazy, try-with-resources) 아래 코드 [line1, line2]
newBufferedReader(p) 큰 파일 줄 단위 try (var r = Files.newBufferedReader(p)) { r.readLine(); } "a", 끝이면 null
newBufferedReader(p).lines() Reader → 스트림 r.lines().toList() [a, b]
newInputStream(p) 바이너리 읽기 try (var in = Files.newInputStream(p)) { in.read(); } 첫 바이트 1
writeString(p, s) 덮어쓰기 (CREATE+TRUNCATE+WRITE) Files.writeString(f, "line1\nline2\n") 경로 반환
writeString(p, s, charset, options...) 인코딩+옵션 Files.writeString(p, "한글", Charset.forName("EUC-KR")) 크기 4
write(p, bytes) 바이트 쓰기 Files.write(p, new byte[]{1, 2, 3}) 경로 반환
write(p, Iterable<String>) 줄 리스트 (줄마다 개행) Files.write(p, List.of("x", "y")) 파일 크기 6 (\r\n 포함)
newBufferedWriter(p, charset) 큰 파일 쓰기 try (var w = Files.newBufferedWriter(p, UTF_8)) { w.write("a"); } 파일 생성
newOutputStream(p) 바이너리 쓰기 try (var out = Files.newOutputStream(p)) { out.write(65); } 파일 내용 "A"
new PrintWriter(Files.newBufferedWriter(p)) printf/println pw.printf("%d-%s%n", 1, "a"); pw.println("b") [1-a, b]
copy(InputStream, p) 스트림 → 파일 Files.copy(new ByteArrayInputStream("from stream".getBytes()), p) 11 (바이트 수)
copy(p, OutputStream) 파일 → 스트림 Files.copy(p, byteArrayOut) 내용 복사
Scanner(path) 토큰 파싱 try (Scanner sc = new Scanner(f)) { sc.nextLine(); } 첫 줄
RandomAccessFile 임의 위치 접근 raf.seek(2); (char) raf.read() 3번째 문자

lines(p) 예제는 아래 코드와 같습니다.

java
try (Stream<String> s = Files.lines(f)) {
    return s.filter(l -> l.startsWith("line")).toList();
}
  • readString/readAllLines는 파일 전체를 메모리에 올린다. 수백 MB 이상은 lines()나 newBufferedReader.
  • Files.lines, Files.list, Files.walk는 파일 핸들을 잡고 있는 스트림이다. 반드시 try-with-resources.
04옵션 상수와 인코딩
상수/메서드 설명 한 줄 예제 결과
StandardOpenOption.CREATE 없으면 생성 (있으면 열기) writeString(p, "n", CREATE, APPEND) (새 파일) 생성됨
StandardOpenOption.CREATE_NEW 없을 때만 생성, 있으면 예외 writeString(f, "x", CREATE_NEW) (존재) FileAlreadyExistsException
StandardOpenOption.APPEND 끝에 추가 writeString(f, "more\n", APPEND) 줄 수 4
StandardOpenOption.TRUNCATE_EXISTING 열 때 비우기 writeString(f, "reset\n", TRUNCATE_EXISTING) 내용 reset
StandardOpenOption.WRITE 쓰기 (파일 있어야 함) writeString(missing, "x", WRITE) NoSuchFileException
StandardOpenOption.READ 읽기 Files.newInputStream(p, READ) 읽기 스트림
StandardOpenOption.DELETE_ON_CLOSE 닫을 때 삭제 임시 작업 파일 자동 삭제
StandardOpenOption.SYNC / DSYNC 동기 쓰기 내구성 필요 시 느림
기본 옵션 (writeString(p, s)) CREATE, TRUNCATE_EXISTING, WRITE 항상 덮어쓰기
StandardCopyOption.REPLACE_EXISTING 대상 있으면 덮어쓰기 Files.copy(f, dst, REPLACE_EXISTING) 성공
copy 옵션 없이 대상 존재 예외 Files.copy(f, existing) FileAlreadyExistsException
StandardCopyOption.COPY_ATTRIBUTES 수정시각 등 속성 복사 Files.copy(f, dst, COPY_ATTRIBUTES) 성공
StandardCopyOption.ATOMIC_MOVE 원자적 이동 (같은 파일시스템) Files.move(src, dst, ATOMIC_MOVE) 성공
LinkOption.NOFOLLOW_LINKS 심볼릭 링크 따라가지 않음 Files.exists(p, NOFOLLOW_LINKS) 링크 자체 검사
StandardCharsets.UTF_8 상수 (권장) Files.readString(p, StandardCharsets.UTF_8)
Charset.forName("EUC-KR") / "MS949" 한글 레거시 Files.writeString(p, "한글", Charset.forName("EUC-KR")) 크기 4
잘못된 인코딩으로 readString 예외 (JDK 11+ Files.readString) Files.readString(eucFile) (UTF-8 기본) MalformedInputException
잘못된 인코딩으로 new String 깨진 문자 (예외 없음) new String(Files.readAllBytes(eucFile), UTF_8) "�ѱ�"
Charset.defaultCharset() 기본 인코딩 (JDK 18+ UTF-8) Charset.defaultCharset() UTF-8
System.getProperty("file.encoding") 동일 왼쪽 UTF-8
-Dfile.encoding=UTF-8 / -Dstdout.encoding=UTF-8 JVM 옵션 콘솔 한글 깨짐 대응
  • Files.readString은 잘못된 바이트를 만나면 예외를 던지고, new String(bytes, cs)는 조용히 �로 바꾼다. 인코딩 오류를 잡으려면 전자가 낫다.
  • 레거시 시스템(EUC-KR/MS949)과 주고받는 파일은 인코딩을 반드시 명시한다. MS949는 EUC-KR의 확장이라 한글 8,822자를 모두 표현한다.
05존재 확인·속성·생성·복사·이동·삭제
메서드/시그니처 설명 한 줄 예제 결과
exists(p) / notExists(p) 존재 (둘 다 false면 판단 불가) Files.exists(f) + " " + Files.notExists(missing) true true
isRegularFile(p) / isDirectory(p) 종류 Files.isRegularFile(f) + " " + Files.isDirectory(base) true true
isReadable/isWritable/isExecutable/isHidden 권한·속성 Files.isReadable(f) + "/" + Files.isHidden(f) true/false
size(p) 바이트 Files.size(f) 6
getLastModifiedTime(p) FileTime Files.getLastModifiedTime(f).toInstant() Instant
setLastModifiedTime(p, FileTime) 수정 시각 변경 Files.setLastModifiedTime(f, FileTime.fromMillis(0)) → 조회 1970-01-01T00:00:00Z
readAttributes(p, BasicFileAttributes.class) 속성 묶음 아래 코드 6 dir=false created=...
getAttribute(p, "size") 문자열 키로 속성 Files.getAttribute(f, "size") 6
probeContentType(p) MIME 추정 Files.probeContentType(f) text/plain
isSameFile(a, b) 같은 파일인지 Files.isSameFile(f, base.resolve("./a.txt")) true
getOwner(p) 소유자 Files.getOwner(f) UserPrincipal
createDirectories(p) 중간 경로 포함 생성 (있어도 OK) Files.createDirectories(base.resolve("d1/d2/d3")) d1\d2\d3
createDirectory(p) 1단계만, 있으면 예외 Files.createDirectory(existing) FileAlreadyExistsException
createFile(p) 빈 파일 생성, 있으면 예외 Files.createFile(existing) FileAlreadyExistsException
copy(src, dst) 복사 (대상 있으면 예외) Files.copy(f, base.resolve("a_copy.txt")) a_copy.txt
copy(src, dst, REPLACE_EXISTING) 덮어쓰기 왼쪽 성공
copy 디렉터리 빈 디렉터리만 복사 (내용은 walk로) 스니펫 참고
move(src, dst) 이동/이름 변경 Files.move(a, base.resolve("renamed.txt")) renamed.txt
move(src, dst, REPLACE_EXISTING) 덮어쓰기 이동 Files.move(a, existing, REPLACE_EXISTING) 성공
move(src, dst, ATOMIC_MOVE) 원자적 (안전한 덮어쓰기용) Files.move(tmp, target, ATOMIC_MOVE) 성공
delete(p) 삭제, 없으면 예외 Files.delete(missing) NoSuchFileException
deleteIfExists(p) 삭제 여부 반환 Files.deleteIfExists(missing) + "/" + Files.deleteIfExists(f) false/true
delete 비어있지 않은 디렉터리 예외 Files.delete(nonEmptyDir) DirectoryNotEmptyException
부모 없는 경로에 쓰기 예외 Files.writeString(base.resolve("nodir/f.txt"), "x") NoSuchFileException
File API (구식) exists/length/getName/isFile/getParentFile f.toFile().length() 6
File.listFiles() 구식 목록 (null 가능) dir.toFile().listFiles() File[]

readAttributes 예제는 아래 코드와 같습니다.

java
var attrs = Files.readAttributes(p, BasicFileAttributes.class);
attrs.size() + " dir=" + attrs.isDirectory() + " created=" + attrs.creationTime();
  • 파일을 쓰기 전에 Files.createDirectories(p.getParent())를 호출하는 습관을 들인다.
  • 디렉터리 삭제는 안쪽부터. Files.walk(...).sorted(reverseOrder()) 순서로 삭제한다(스니펫).
06디렉터리 순회 (list / walk / find / DirectoryStream)
메서드/시그니처 설명 한 줄 예제 결과
list(dir) 1단계만, lazy 스트림 아래 코드 1 [d2, e.txt]
walk(dir) 재귀 (자기 자신 포함, 깊이 우선) try (var s = Files.walk(d1)) { s.map(base::relativize).toList(); } 아래 결과
walk(dir, maxDepth) 깊이 제한 Files.walk(d1, 1).count() 3 (자신 + 1단계 2개)
walk + 필터 확장자 검색 s.filter(x -> x.toString().endsWith(".java")) [x.java, y.java]
find(dir, depth, (path, attrs) -> bool) 속성 조건 검색 Files.find(d1, 10, (x, a) -> a.isRegularFile() && a.size() > 5) [x.java, y.java]
newDirectoryStream(dir, glob) glob 필터 (for-each) 아래 코드 2 [x.java, y.java]
FileSystems.getDefault().getPathMatcher("glob:...") 경로 매처 pm.matches(Path.of("a/b/c.java")) (glob:**/*.{java,txt}) true
walkFileTree(dir, FileVisitor) 콜백 방식 (preVisit/visitFile/postVisit) SimpleFileVisitor 오버라이드 [e.txt, x.java, y.java, z.txt]
FileVisitResult.CONTINUE/SKIP_SUBTREE/TERMINATE 순회 제어 return SKIP_SUBTREE (node_modules 등) 하위 건너뜀
File.listFiles() 구식, 배열 Arrays.stream(dir.toFile().listFiles()).map(File::getName) [d2, e.txt]

아래 코드 1: list(dir) 예제. 아래 코드 2: newDirectoryStream 예제. walk(dir) 예제의 아래 결과는 d1 아래 모든 경로입니다.

java
// 아래 코드 1
try (var s = Files.list(d1)) {
    s.map(x -> x.getFileName().toString()).sorted().toList();
}

// 아래 코드 2
try (var ds = Files.newDirectoryStream(d2, "*.java")) {
    for (Path x : ds) { ... }
}
text
아래 결과: [d1, d1/d2, d1/d2/d3, d1/d2/d3/z.txt, d1/d2/x.java, d1/d2/y.java, d1/e.txt]
  • walk는 심볼릭 링크를 기본으로 따라가지 않는다. 필요하면 FileVisitOption.FOLLOW_LINKS.
  • 권한 없는 디렉터리를 만나면 walk가 UncheckedIOException을 던진다. 견고하게 하려면 walkFileTree의 visitFileFailed를 오버라이드.
07java.io 스트림 계층 요약
분류 클래스 용도 한 줄 예제 결과
바이트 입력 FileInputStream 파일 바이트 new FileInputStream(file) 스트림
바이트 출력 FileOutputStream 파일 바이트 (2번째 인자 true = append) new FileOutputStream(file, true) 스트림
버퍼 BufferedInputStream / BufferedOutputStream 8KB 버퍼로 감싸 성능 new BufferedInputStream(new FileInputStream(f)) 스트림
기본형 DataInputStream / DataOutputStream int/double/UTF 쓰고 읽기 out.writeInt(42); out.writeUTF("hi"); out.writeDouble(1.5) → 읽기 42/hi/1.5 (16바이트)
객체 ObjectInputStream / ObjectOutputStream 직렬화 (8절) oos.writeObject(obj) 바이트
메모리 ByteArrayInputStream / ByteArrayOutputStream 바이트 배열 ↔ 스트림 아래 코드 1 "abc"
문자 입력 FileReader 파일 문자 (charset 지정 생성자 JDK 11+) new FileReader(f, UTF_8) Reader
문자 출력 FileWriter 파일 문자 new FileWriter(f, UTF_8, true) (append) "appended"
브리지 InputStreamReader / OutputStreamWriter 바이트 ↔ 문자 (charset 명시) 아래 코드 2 Reader
버퍼 문자 BufferedReader / BufferedWriter readLine(), newLine() r.readLine() 한 줄
출력 편의 PrintWriter / PrintStream printf/println (예외 삼킴 주의) new PrintWriter(sw, true).printf("%s=%d", "a", 1) "a=1"
메모리 문자 StringReader / StringWriter 문자열 ↔ Reader/Writer new BufferedReader(new StringReader("l1\nl2")).lines().toList() [l1, l2]
토큰 Scanner 공백 구분 파싱 아래 코드 3 30
임의 접근 RandomAccessFile seek/read/write raf.seek(2); (char) raf.read() 's'
공통 InputStream.readAllBytes() 전체 읽기 (JDK 9+) in.readAllBytes() byte[]
공통 InputStream.readNBytes(n) n바이트 new ByteArrayInputStream("abcdef".getBytes()).readNBytes(3) "abc"
공통 InputStream.transferTo(out) 복사 (JDK 9+) in.transferTo(byteArrayOut) "xyz"
표준 System.in / System.out / System.err 콘솔 new BufferedReader(new InputStreamReader(System.in)).readLine() 입력 한 줄

아래 코드 1~3은 각각 메모리 스트림, 브리지, Scanner 예제입니다.

java
// 아래 코드 1
new String(new ByteArrayInputStream("abc".getBytes()).readAllBytes());

// 아래 코드 2
new BufferedReader(new InputStreamReader(new FileInputStream(f), UTF_8));

// 아래 코드 3
Scanner sc = new Scanner("10 20 hello");
sc.nextInt() + sc.nextInt();

try-with-resources 규칙:

상황 동작 예제 결과
여러 리소스 선언 역순으로 close try (A a = ...; B b = ...) { } [body, close B, close A]
본문 예외 + close 예외 close 예외는 getSuppressed()에 try (r) { throw new IllegalStateException("body fail"); } body fail suppressed=close fail
effectively final 변수 JDK 9+ try (existingVar) BufferedReader r = ...; try (r) { } 닫힘
AutoCloseable 람다 테스트용 try (AutoCloseable a = () -> log("close")) { } 호출됨
  • 바이트 스트림에서 문자를 읽을 때는 항상 InputStreamReader(in, charset)으로 감싼다. FileReader의 인자 없는 생성자는 플랫폼 기본 인코딩을 쓴다.
  • PrintWriter는 IOException을 삼킨다. checkError()로 확인하거나 BufferedWriter를 쓴다.
08직렬화 기초
항목 설명 한 줄 예제 결과
implements Serializable 마커 인터페이스 record User(String name, int age) implements Serializable {} 직렬화 가능
ObjectOutputStream.writeObject(o) 쓰기 아래 코드 파일
ObjectInputStream.readObject() 읽기 (캐스팅, ClassNotFoundException) (User) ois.readObject() User[name=Kim, age=30]
transient 직렬화 제외 (읽으면 기본값) transient String secret = "pw" → 역직렬화 Session[id=s1, secret=null]
serialVersionUID 클래스 버전 (명시 권장) private static final long serialVersionUID = 1L; 불일치 시 InvalidClassException
미구현 클래스 예외 oos.writeObject(new Object()) NotSerializableException
record 직렬화 생성자 통해 복원 (JDK 16+) 위 User 예제 정상
포함 객체 모든 필드가 Serializable이어야 List<User> 필드 ArrayList은 OK
보안 신뢰할 수 없는 바이트 역직렬화 금지 ObjectInputFilter 설정 필터링

writeObject 예제는 아래 코드와 같습니다.

java
try (var oos = new ObjectOutputStream(Files.newOutputStream(p))) {
    oos.writeObject(new User("Kim", 30));
}
  • 실무 데이터 교환은 Java 직렬화 대신 JSON(Jackson)/Protobuf를 쓴다. Java 직렬화는 캐시·세션 저장 같은 내부 용도로 제한한다.
09Properties 파일
메서드/시그니처 설명 한 줄 예제 결과
new Properties() 생성 Properties props = new Properties() 빈
load(Reader) 읽기 (Reader로 UTF-8) try (var r = Files.newBufferedReader(p, UTF_8)) { props.load(r); } 로드
load(InputStream) ISO-8859-1로 읽음 (한글 깨짐) props.load(Files.newInputStream(p)) → getProperty("app.title") "íê¸ ì 목"
getProperty(key) 값 (없으면 null) props.getProperty("app.name") "Demo"
getProperty(key, default) 기본값 props.getProperty("app.missing", "default") "default"
숫자 변환 직접 파싱 Integer.parseInt(props.getProperty("app.port")) 8080
목록 변환 split Arrays.asList(props.getProperty("app.list").split(",")) [a, b, c]
setProperty(k, v) 설정 props.setProperty("app.new", "v") 이전 값
stringPropertyNames() 키 집합 new TreeSet<>(props.stringPropertyNames()) [app.list, app.name, app.port, app.title]
store(Writer, comment) 저장 (날짜 주석 자동 추가) 아래 코드 app.new=v 줄 포함
System.getProperty(key) JVM 시스템 프로퍼티 System.getProperty("java.version") 21.0.8
System.getProperties() 전체 System.getProperties().size() 수십 개
파일 형식 key=value, # 주석, \ 줄 연속 app.name=Demo

store 예제는 아래 코드와 같습니다.

java
try (var w = Files.newBufferedWriter(out)) {
    props.store(w, "saved");
}
properties
# comment
app.name=Demo
app.port=8080
app.title=한글 제목
app.list=a,b,c
  • load(InputStream)은 ISO-8859-1 고정이다. 한글은 load(Reader)를 쓰거나 \uXXXX로 이스케이프한다(JDK 9+ .properties 리소스 번들은 UTF-8 기본).
  • 클래스패스 리소스는 getClass().getResourceAsStream("/app.properties")로 읽고, 없으면 null이므로 검사한다.
10압축 (Zip / GZIP) 과 임시 파일
메서드/시그니처 설명 한 줄 예제 결과
ZipOutputStream(out) zip 쓰기 아래 코드 1 엔트리 추가
하위 경로 엔트리 /로 구분 new ZipEntry("dir/n.txt") 폴더 구조
ZipInputStream(in) zip 순차 읽기 while ((e = zis.getNextEntry()) != null) { zis.readAllBytes(); } [hello.txt:hello zip, dir/n.txt:nested]
ZipFile(file) zip 임의 접근 zf.stream().map(ZipEntry::getName).toList() + " size=" + zf.size() [hello.txt, dir/n.txt] size=2
ZipFile.getInputStream(entry) 특정 엔트리 읽기 zf.getInputStream(zf.getEntry("hello.txt")) 스트림
ZipEntry.isDirectory() 디렉터리 엔트리 e.isDirectory() boolean
FileSystems.newFileSystem(zipPath) zip을 파일시스템으로 (JDK 13+) 아래 코드 2 "hello zip"
GZIPOutputStream(out) gzip 쓰기 out.write("gzip content ".repeat(100).getBytes()) 1300자 → 44바이트
GZIPInputStream(in) gzip 읽기 new String(in.readAllBytes()).length() 1300
gzip 텍스트 줄 읽기 Reader로 감싸기 아래 코드 3 첫 줄
Files.createTempFile(prefix, suffix) 임시 파일 (java.io.tmpdir) Files.createTempFile("pre_", ".tmp") pre_123.tmp 존재
Files.createTempFile(dir, prefix, suffix) 위치 지정 Files.createTempFile(base, "x", ".log") base 아래
Files.createTempDirectory(prefix) 임시 디렉터리 Files.createTempDirectory("tmpdir") tmpdir123
System.getProperty("java.io.tmpdir") 임시 디렉터리 위치 왼쪽 C:\Users\...\Temp
file.deleteOnExit() JVM 종료 시 삭제 (java.io) tmp.toFile().deleteOnExit() 등록
StandardOpenOption.DELETE_ON_CLOSE 닫을 때 삭제 Files.newOutputStream(tmp, DELETE_ON_CLOSE) 자동 삭제

아래 코드 1~3은 각각 zip 쓰기, zip 파일시스템, gzip 줄 읽기 예제입니다.

java
// 아래 코드 1
zos.putNextEntry(new ZipEntry("hello.txt"));
zos.write(bytes);
zos.closeEntry();

// 아래 코드 2
try (FileSystem zfs = FileSystems.newFileSystem(zip)) {
    Files.readString(zfs.getPath("hello.txt"));
}

// 아래 코드 3
new BufferedReader(new InputStreamReader(new GZIPInputStream(in), UTF_8))
        .readLine();
  • 임시 파일은 자동 삭제되지 않는다. finally에서 deleteIfExists하거나 DELETE_ON_CLOSE를 쓴다.
  • Zip 엔트리 이름에 ../가 들어간 파일을 그대로 풀면 경로 탈출(Zip Slip)이 된다. dst.resolve(name).normalize().startsWith(dst)를 확인한다.
11실무 스니펫

스니펫 1: 안전한 덮어쓰기 (임시 파일 + 원자적 이동)

설정 파일이나 결과 파일을 직접 덮어쓰면 쓰는 도중 프로세스가 죽었을 때 파일이 반쯤 깨진다. 같은 디렉터리에 임시 파일로 완전히 쓴 뒤 ATOMIC_MOVE로 교체하면 언제나 "이전 버전 아니면 새 버전"만 존재한다.

java
static void safeOverwrite(Path target, String content) throws IOException {
    Path tmp = target.resolveSibling(target.getFileName() + ".tmp");
    Files.writeString(tmp, content, StandardCharsets.UTF_8,
            StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
    Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
}

Files.writeString(target, "{\"v\":1}");
safeOverwrite(target, "{\"v\":2}");
Files.readString(target);                          // 출력: {"v":2}
Files.exists(target.resolveSibling("config.json.tmp"));   // 출력: false

스니펫 2: 디렉터리 재귀 크기와 확장자별 목록

Files.walk로 재귀 순회하고 일반 파일만 걸러 Files.size를 합산한다. 람다 안에서는 checked 예외를 던질 수 없으므로 내부에서 잡는다. 확장자별 그룹핑은 groupingBy로.

java
static long dirSize(Path dir) throws IOException {
    try (Stream<Path> s = Files.walk(dir)) {
        return s.filter(Files::isRegularFile)
                .mapToLong(p -> { try { return Files.size(p); } catch (IOException e) { return 0L; } })
                .sum();
    }
}
static Map<String, List<String>> byExtension(Path dir) throws IOException {
    try (Stream<Path> s = Files.walk(dir)) {
        return s.filter(Files::isRegularFile).collect(Collectors.groupingBy(p -> {
            String n = p.getFileName().toString();
            int i = n.lastIndexOf('.');
            return i < 0 ? "(none)" : n.substring(i + 1).toLowerCase();
        }, TreeMap::new, Collectors.mapping(p -> p.getFileName().toString(), Collectors.toList())));
    }
}

dirSize(d1);        // 출력: 20
byExtension(d1);    // 출력: {java=[x.java, y.java], txt=[z.txt, e.txt]}

스니펫 3: 대용량 파일 라인 카운트와 grep

10만 줄(1.1MB) 파일. Files.lines().count()는 메모리에 다 올리지 않고 세며, BufferedReader.readLine() 루프도 같다. 특정 패턴 검색이나 앞 N줄 미리보기도 스트림으로 처리하면 필요한 만큼만 읽는다.

java
static long countLinesStream(Path p) throws IOException {
    try (Stream<String> s = Files.lines(p)) { return s.count(); }
}
static long countLinesReader(Path p) throws IOException {
    try (BufferedReader r = Files.newBufferedReader(p)) {
        long n = 0;
        while (r.readLine() != null) n++;
        return n;
    }
}

countLinesStream(big);     // 출력: 100000
countLinesReader(big);     // 출력: 100000
Files.size(big);           // 출력: 1188890

try (Stream<String> s = Files.lines(big)) { s.filter(l -> l.endsWith("99999")).toList(); }
// 출력: [line 99999]
try (Stream<String> s = Files.lines(big)) { s.limit(3).toList(); }
// 출력: [line 0, line 1, line 2]

스니펫 4: 디렉터리 복사와 재귀 삭제

Files.copy는 디렉터리 내용을 복사하지 않는다. walk로 모든 경로를 돌며 디렉터리는 createDirectories, 파일은 copy. 삭제는 자식이 먼저 지워져야 하므로 역순 정렬(reverseOrder)로 돈다.

java
static void copyDir(Path src, Path dst) throws IOException {
    try (Stream<Path> s = Files.walk(src)) {
        for (Path p : s.toList()) {
            Path t = dst.resolve(src.relativize(p));
            if (Files.isDirectory(p)) Files.createDirectories(t);
            else Files.copy(p, t, StandardCopyOption.REPLACE_EXISTING);
        }
    }
}
static void deleteRecursively(Path root) throws IOException {
    if (!Files.exists(root)) return;
    try (Stream<Path> s = Files.walk(root)) {
        for (Path p : s.sorted(Comparator.reverseOrder()).toList()) Files.delete(p);
    }
}

copyDir(d1, d1_copy);
try (Stream<Path> s = Files.walk(d1_copy)) { s.count(); }   // 출력: 7
deleteRecursively(d1_copy);
Files.exists(d1_copy);                                       // 출력: false

스니펫 5: WatchService로 디렉터리 변경 감지

디렉터리를 register하고 poll/take로 이벤트를 받는다. 파일 하나를 쓰면 ENTRY_CREATE와 ENTRY_MODIFY가 연달아 오는 게 보통이며, 큰 파일은 MODIFY가 여러 번 온다. 처리 후 반드시 key.reset(). 배치에서 "폴더에 파일이 떨어지면 처리" 패턴의 기초다.

java
static String watchDemo(Path dir) throws Exception {
    Path wd = Files.createDirectories(dir.resolve("watch"));
    try (WatchService ws = FileSystems.getDefault().newWatchService()) {
        wd.register(ws, StandardWatchEventKinds.ENTRY_CREATE,
                        StandardWatchEventKinds.ENTRY_MODIFY,
                        StandardWatchEventKinds.ENTRY_DELETE);
        Files.writeString(wd.resolve("n.txt"), "x");
        WatchKey key = ws.poll(3, TimeUnit.SECONDS);        // 무한 대기는 ws.take()
        if (key == null) return "no event";
        List<String> ev = new ArrayList<>();
        for (WatchEvent<?> e : key.pollEvents()) ev.add(e.kind().name() + ":" + e.context());
        key.reset();                                        // 다음 이벤트 받으려면 필수
        return ev.toString();
    }
}
// 출력: [ENTRY_CREATE:n.txt, ENTRY_MODIFY:n.txt]

스니펫 6: 출력 파일 준비 (부모 생성 + 날짜 파일명) 와 CSV 읽기

배치 결과 파일은 out/2024/01/ 같은 경로에 날짜가 붙은 이름으로 쓴다. 부모 디렉터리를 먼저 만들고, 파일명은 BASIC_ISO_DATE(20240305)로. CSV는 헤더를 skip(1)하고 split으로 컬럼을 뽑는다(따옴표 포함 CSV는 명령어 01 스니펫 참고).

java
Path out = base.resolve("out/2024/01/r.txt");
Files.createDirectories(out.getParent());
Files.writeString(out, "r");
Files.exists(out);                                   // 출력: true

String name = "orders_" + LocalDate.of(2024, 3, 5).format(DateTimeFormatter.BASIC_ISO_DATE) + ".csv";
// 출력: orders_20240305.csv

Path csv = base.resolve("d.csv");
Files.write(csv, List.of("id,name", "1,Kim", "2,Lee"));
try (Stream<String> s = Files.lines(csv)) {
    s.skip(1).map(l -> l.split(",")).map(a -> a[1]).toList();
}
// 출력: [Kim, Lee]

스니펫 7: 바이너리 레코드 쓰기/읽기 (DataStream)

고정 포맷 바이너리는 DataOutputStream으로 쓰고 같은 순서로 읽는다. writeUTF는 앞에 2바이트 길이를 붙인다(int 4 + UTF 2+2 + double 8 = 16바이트).

java
try (var out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(bin)))) {
    out.writeInt(42); out.writeUTF("hi"); out.writeDouble(1.5);
}
try (var in = new DataInputStream(new BufferedInputStream(Files.newInputStream(bin)))) {
    in.readInt() + "/" + in.readUTF() + "/" + in.readDouble();
}
// 출력: 42/hi/1.5
Files.size(bin);   // 출력: 16
12자주 하는 실수

❌ 실수 1: Files.lines/list/walk를 닫지 않음

java
long n = Files.lines(p).count();          // 파일 핸들 누수 (Windows에서는 삭제/이동 실패로 이어짐)
try (Stream<String> s = Files.lines(p)) { long n = s.count(); }   // 올바름

❌ 실수 2: 인코딩 미지정 / InputStream으로 Properties 로드

코드 결과
new FileReader(f) 플랫폼 기본 인코딩 (환경 의존)
new FileReader(f, StandardCharsets.UTF_8) 명시 (JDK 11+)
props.load(Files.newInputStream(p)) → 한글 íê¸ ì 목 (ISO-8859-1)
props.load(Files.newBufferedReader(p, UTF_8)) → 한글 한글 제목
Files.readString(eucKrFile) MalformedInputException

❌ 실수 3: 부모 디렉터리 없이 쓰기 / 대상 존재 시 copy

java
Files.writeString(base.resolve("nodir/f.txt"), "x");   // NoSuchFileException
Files.createDirectories(p.getParent()); Files.writeString(p, "x");   // 올바름

Files.copy(src, existing);                                // FileAlreadyExistsException
Files.copy(src, existing, StandardCopyOption.REPLACE_EXISTING);   // 올바름

❌ 실수 4: Path.startsWith/endsWith를 문자열 접두/접미로 착각

java
Path p = Path.of("data/logs/app.log");
p.endsWith(".log");        // false — 경로 요소 단위 비교
p.endsWith("app.log");     // true
p.toString().endsWith(".log");   // 문자열 검사는 이렇게

❌ 실수 5: 문자열로 경로 결합

java
String path = dir + "/" + name;                    // OS 구분자, 중복 슬래시 문제
Path path = Path.of(dir).resolve(name);            // 올바름
Path.of("data").resolve("C:/abs/x.txt");           // C:\abs\x.txt — 절대 경로가 오면 그대로 (주의)

❌ 실수 6: 비어 있지 않은 디렉터리 delete / createDirectory로 중간 경로

java
Files.delete(nonEmptyDir);                 // DirectoryNotEmptyException
deleteRecursively(nonEmptyDir);            // walk 역순 삭제 (스니펫 4)
Files.createDirectory(base.resolve("a/b/c"));    // a가 없으면 NoSuchFileException
Files.createDirectories(base.resolve("a/b/c"));  // 올바름

❌ 실수 7: 큰 파일을 readAllLines/readString으로 통째 읽기

java
List<String> all = Files.readAllLines(huge);       // OutOfMemoryError 위험
try (var r = Files.newBufferedReader(huge)) { String line; while ((line = r.readLine()) != null) process(line); }

❌ 실수 8: 버퍼 없이 바이트 단위 I/O

java
try (var in = new FileInputStream(f)) { int b; while ((b = in.read()) != -1) ... }   // 시스템 콜 폭증
try (var in = new BufferedInputStream(new FileInputStream(f))) { ... }               // 8KB 버퍼
// 또는 Files.newInputStream + readAllBytes / transferTo

❌ 실수 9: Windows에서 열려 있는 파일 이동/삭제

java
BufferedReader r = Files.newBufferedReader(p);
Files.move(p, q);        // Windows: FileSystemException (다른 프로세스가 사용 중)
// 스트림을 먼저 close (try-with-resources 블록 밖에서 이동)
13관련 레슨
  • 배치 02 파일 I/O: 대용량 CSV 읽기/쓰기, 인코딩, 스트리밍 처리 설계
  • 중급 03 예외 처리: try-with-resources, suppressed 예외, checked IOException 처리 전략
  • 배치 01 아키텍처 / 03 청크 처리: 파일 기반 배치 입출력, 체크포인트 파일
  • 배치 04 트랜잭션: 안전한 덮어쓰기(임시 파일 + 원자적 이동)와 파일 기반 커밋
  • 고급 04 스트림: Files.lines/walk 스트림 조합
  • 명령어 01 String: CSV 파싱, 인코딩(getBytes/new String)
  • 명령어 05 java.time: 날짜 기반 파일명, FileTime ↔ Instant 변환
목차
  • 한눈에 보기
  • Path 조작
  • Files 읽기 / 쓰기
  • 옵션 상수와 인코딩
  • 존재 확인·속성·생성·복사·이동·삭제
  • 디렉터리 순회 (list / walk / find / DirectoryStream)
  • java.io 스트림 계층 요약
  • 직렬화 기초
  • Properties 파일
  • 압축 (Zip / GZIP) 과 임시 파일
  • 실무 스니펫
  • 자주 하는 실수
  • 관련 레슨
참고 영상 · 인터넷 연결 시 유튜브 검색이 열립니다자바 Files · Path · I/O명령어 Files Path NIO
이전03 Stream · Collectors · 함수형 인터페이스다음05 java.time · BigDecimal · 기타 유틸