본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
13장 : 파일 입출력

Path와 Files 기본 작업

현대 Java 파일 작업은 문자열 경로보다 Path로 위치를 표현하고 Files의 정적 메서드로 실제 작업을 수행합니다.

Path를 만들었다고 파일이 생성되거나 존재 여부가 검사되는 것은 아닙니다.

상대 경로는 현재 작업 디렉터리를 기준으로 해 실행 위치에 따라 가리키는 곳이 달라질 수 있습니다.

파일 존재 확인 뒤 작업하는 사이 다른 프로세스가 상태를 바꿀 수도 있으므로 실제 작업 예외를 처리해야 합니다.

이 절에서는 경로 표현과 파일 시스템 효과를 분리합니다.

normalize... 같은 경로 조각을 문자 수준에서 정리할 뿐, 실제 파일을 찾아 심볼릭 링크를 해석하지 않습니다. 실제 위치와 동일성까지 확인하려면 파일 시스템 접근이 일어나는 toRealPath가 필요합니다. 경로 계산과 실제 I/O를 구분하면 어떤 줄에서 실패할 수 있는지도 선명해집니다.


Path는 위치를 표현할 뿐 I/O를 하지 않는다

Path.ofresolve, normalize는 경로 객체를 조합합니다.

파일 생성과 읽기는 Files 메서드를 호출할 때 발생합니다.


상대 경로의 기준을 확인한다

toAbsolutePath로 현재 실행에서의 절대 위치를 출력해 볼 수 있습니다.

소스 파일 위치와 작업 디렉터리가 같다고 가정하지 않습니다.

예제는 OS가 제공한 임시 절대 경로 아래에서만 작업하므로 현재 프로젝트 파일을 건드리지 않습니다. resolveresolveSibling은 위치를 계산하고, Files 호출부터 실제 디렉터리와 파일 상태가 바뀝니다.

PathOperations.java
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public class PathOperations {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("java-path-");
        Path draft = root.resolve("notes").resolve("draft.txt");
        Files.createDirectories(draft.getParent());
        Files.writeString(draft, "Java 25");

        Path finalFile = draft.resolveSibling("final.txt");
        try {
            Files.move(draft, finalFile, StandardCopyOption.ATOMIC_MOVE);
        } catch (AtomicMoveNotSupportedException unsupported) {
            Files.move(draft, finalFile);
        }
        System.out.println(finalFile.isAbsolute());
        System.out.println(Files.readString(finalFile));
        Files.deleteIfExists(finalFile);
        Files.deleteIfExists(finalFile.getParent());
        Files.deleteIfExists(root);
    }
}

첫 줄은 createTempDirectory가 절대 경로를 반환했다는 관찰이고, 둘째 줄은 이동 뒤의 final.txt에서 다시 읽은 내용입니다. draft 경로 객체는 남아 있어도 그 위치의 파일은 이동 후 존재하지 않습니다.

true
Java 25

임시 루트 아래에서 경로 계산, 디렉터리 생성, 쓰기, 원자 이동, 삭제를 실제 Files API로 수행합니다.


Files 메서드가 실제 효과와 예외를 만든다

createDirectories, writeString, readString, move, deleteIfExists가 파일 시스템을 변경하며 IOException을 처리하거나 전달합니다.

아래 비교에서 Path.ofnormalize는 존재하지 않는 경로에도 성공하지만 Files.readString은 실제 파일 시스템 상태를 확인해 실패합니다. 이 차이를 이용해 순수한 경로 계산 코드를 I/O 경계 밖에 둘 수 있습니다.

비교 1
Path relative = Path.of("data", "..", "notes.txt");
System.out.println(relative.normalize());        // notes.txt
System.out.println(relative.toAbsolutePath());  // 작업 디렉터리 기준
비교 2
Path calculated = Path.of("missing.txt"); // I/O 없음
// Files.readString(calculated);           // 실행 시 NoSuchFileException
비교 3
// exists 확인과 생성 사이 상태가 바뀔 수 있으므로 실제 작업 예외를 처리한다.

확인 후 작업보다 원자적 작업 결과를 믿는다

exists를 확인한 뒤 생성하는 두 단계는 경쟁 상태가 있습니다.

생성·이동 메서드를 호출하고 FileAlreadyExistsException 같은 결과로 분기합니다.

이동 대상인 final.txt를 먼저 만든 뒤 REPLACE_EXISTING 없이 실행해 보세요. “없음을 확인한 뒤 이동”하는 코드보다 FileAlreadyExistsException이라는 실제 작업 결과를 처리하는 편이 경쟁 상태에 안전합니다. ATOMIC_MOVE가 지원되지 않을 때의 대체 이동은 같은 원자성을 보장하지 않는다는 점도 호출자 정책에 반영해야 합니다.

사용자 입력을 기준 경로에 resolve할 때는 normalize한 결과가 여전히 기준 경로 아래인지 확인하세요. ../../outside 같은 조각을 단순히 붙이면 의도한 작업 영역을 벗어날 수 있습니다. 파일 존재 여부와 별개로 경로 경계 자체를 검증하는 보안 문제입니다.


Path로 위치 계산을 끝낸 뒤 Files 호출 한 번의 성공과 실패를 파일 시스템 계약으로 다루는 것이 안전합니다.