CodeGym /행동 /JAVA 25 SELF /파일 시스템 권한과 접근

파일 시스템 권한과 접근

JAVA 25 SELF
레벨 38 , 레슨 3
사용 가능

1. 운영 체제의 접근 권한

파일과 폴더를 다룰 때 운영 체제(OS)는 접근 권한 시스템으로 이를 보호합니다. 이는 모든 프로그램(또는 사용자)이 임의의 파일을 읽거나, 변경하거나, 삭제할 수 있는 것이 아님을 의미합니다.

POSIX(Linux, macOS 등)

POSIX 계열(Unix, Linux, macOS) 시스템에서는 각 파일과 폴더에 대해 다음 세 가지 범주별 권한이 있습니다:

  • 소유자 (user)
  • 그룹 (group)
  • 기타 (others)

각 범주에는 세 가지 유형의 권한이 지정됩니다:

  • r — read(읽기)
  • w — write(쓰기)
  • x — execute(실행)

예:
-rw-r--r--
이는 소유자는 읽기와 쓰기가 가능하고, 나머지는 읽기만 가능함을 의미합니다.

Windows

Windows에서는 ACL(Access Control List) 시스템을 통해 사용자와 그룹에 대한 권한을 설정합니다. 이를 통해 누가 파일이나 폴더를 읽고, 쓰고, 변경하고, 실행할 수 있는지 등을 유연하게 제어할 수 있습니다.

중요:
Java 프로그램은 실행된 사용자 권한의 범위에서 파일을 다룹니다. 사용자에게 해당 파일에 대한 권한이 없다면 프로그램도 그 파일을 읽거나 변경할 수 없습니다.

2. AccessDeniedException과 그 원인

Java에서 파일을 다룰 때(특히 NIO API 사용 시) 다음 예외를 만날 수 있습니다:

java.nio.file.AccessDeniedException

이 예외는 프로그램에 해당 파일이나 디렉터리에 대해 필요한 권한이 없을 때 발생합니다.

주요 원인:

  • 파일에 대한 읽기 권한이 없음(예: 읽기 금지 파일).
  • 파일이나 폴더에 대한 쓰기 권한이 없음(예: 시스템 폴더에 쓰기를 시도).
  • 파일에 대한 실행 권한이 없음(프로그램 실행 시 관련).
  • 폴더나 파일이 변경 불가로 설정됨(예: 읽기 전용).
  • 파일이나 폴더가 다른 프로세스에 의해 사용 중(특히 Windows에서 자주 발생).

예:

Path path = Paths.get("/etc/shadow"); // Linux 시스템 파일
Files.readAllLines(path); // AccessDeniedException!

무엇을 할 수 있을까요?

  • 파일/폴더의 접근 권한을 확인합니다.
  • 필요한 권한을 가진 사용자로 프로그램을 실행합니다.
  • 필요하지 않다면 시스템 디렉터리에 쓰기를 시도하지 않습니다.

3. Java에서의 권한 확인: Files.isReadable(), isWritable(), isExecutable()

Java는 파일이나 폴더의 권한을 확인하기 위한 편리한 메서드를 제공합니다:

Path path = Paths.get("example.txt");

System.out.println(Files.isReadable(path));   // 읽을 수 있으면 true
System.out.println(Files.isWritable(path));   // 쓸 수 있으면 true
System.out.println(Files.isExecutable(path)); // 실행할 수 있으면 true

이 메서드들은 현재 시점에서 시스템이 인식하는 당신의 권한을 보여 줍니다.

하지만!
이 메서드들이 성공을 보장하지는 않습니다. 파일이 다른 프로그램에 의해 잠겨 있거나, 확인 후 권한이 변경되었거나, 네트워크 드라이브에서는 서버 설정에 따라 결과가 달라질 수 있으며, 때로는 운영 체제가 한 가지를 보고하면서 실제로는 다른 제한을 적용하기도 합니다.

따라서 Java에서는 권한을 미리 확인하더라도 실제 읽기/쓰기 작업은 반드시 try-catch로 감싸 예외를 처리하는 것이 더 안전합니다.

TOCTOU(Time Of Check To Time Of Use) 문제

권한 확인 시점과 실제 사용 시점 사이에 무언가가 바뀌는 상황을 TOCTOU라고 합니다. 예를 들어:

  1. isWritable로 파일이 쓰기 가능함을 확인했습니다.
  2. 그 순간 다른 프로세스나 사용자가 권한을 변경해 파일이 보호 상태가 되었습니다.
  3. 데이터를 쓰려고 하면 AccessDeniedException이 발생합니다.

결론:
권한 확인은 현재 상태에 대한 힌트일 뿐, 성공을 보장하지 않습니다. 파일을 다룰 때는 항상 예외를 처리하세요.

4. “안전한 쓰기” 원칙(atomic write)

왜 안전한(원자적) 쓰기가 필요한가?

파일을 기록하는 중 프로그램이 중단되거나, 정전이 발생하거나, 디스크 공간이 부족할 수 있습니다. 그 결과 파일이 손상되거나 일부만 기록될 수 있습니다. 이는 설정, 데이터베이스, 문서 등 중요한 데이터에는 특히 위험합니다.

안전한 쓰기는 파일이 완전히 갱신되거나 기존 상태 그대로 남도록 보장하는 방법입니다. 이런 접근을 원자적 쓰기(atomic write)라고 부릅니다.

Java에서 안전한 쓰기를 구현하는 방법?

패턴:

  • 임시 파일에 데이터를 기록합니다(보통 같은 폴더).
  • 기록이 성공하면 임시 파일을 원본 위치로 원자적으로 이동하여 교체합니다.

왜 이것이 효과적인가?
동일한 파일 시스템 내에서의 파일 이동(rename/move)은 보통 원자적입니다. 즉, 파일이 완전히 교체되거나 전혀 교체되지 않습니다. 문제가 생기면 원본 파일은 그대로 유지됩니다.

코드 예시: 안전한 파일 쓰기

import java.nio.file.*;

public class SafeWriteDemo {
    public static void safeWrite(Path target, byte[] data) throws Exception {
        // 1. 같은 폴더에 임시 파일을 생성
        Path tempFile = Files.createTempFile(target.getParent(), "tmp_", ".tmp");

        try {
            // 2. 데이터를 임시 파일에 기록
            Files.write(tempFile, data);

            // 3. 임시 파일을 대상으로 원자적으로 이동
            Files.move(
                tempFile,
                target,
                StandardCopyOption.REPLACE_EXISTING,
                StandardCopyOption.ATOMIC_MOVE
            );
        } finally {
            // 문제가 발생하면 임시 파일을 삭제
            Files.deleteIfExists(tempFile);
        }
    }

    public static void main(String[] args) throws Exception {
        Path file = Paths.get("important.txt");
        byte[] content = "매우 중요한 데이터".getBytes();

        safeWrite(file, content);
        System.out.println("파일이 안전하게 기록되었습니다!");
    }
}

유의 사항:

  • Files.createTempFile()을 사용해 임시 파일을 생성합니다.
  • 이동 시 ATOMIC_MOVE 옵션을 사용합니다. 운영 체제와 파일 시스템이 지원하는 경우 원자성이 보장됩니다.
  • 문제가 발생하면 임시 파일을 삭제합니다(Files.deleteIfExists).

언제 특히 중요할까요?

  • 구성 파일, 데이터베이스, 로그 파일을 다룰 때.
  • 다른 프로세스가 언제든지 해당 파일을 읽을 수 있는 경우.
  • 쓰기 중 장애가 발생하면 데이터 손실이나 손상이 발생할 수 있는 경우.

5. 로깅과 접근 오류 처리

접근 오류를 올바르게 처리하는 방법

파일을 다룰 때는 항상 예외 처리(try-catch)를 사용하세요. 이를 통해 다음을 할 수 있습니다:

  • 문제를 사용자에게 올바르게 안내(예: “폴더에 쓸 권한이 없습니다”).
  • 향후 분석을 위해 로그에 오류를 기록.
  • 단일 실패로 전체 프로그램이 중단되는 것을 방지.

예: AccessDeniedException 처리

import java.nio.file.*;

public class FileAccessDemo {
    public static void main(String[] args) {
        Path file = Paths.get("/etc/shadow"); // Linux 예시

        try {
            Files.readAllLines(file);
        } catch (AccessDeniedException ade) {
            System.err.println("접근 오류: 파일을 읽을 권한이 없습니다 " + file);
            // 로그에 기록하거나 사용자에게 다른 파일을 선택하도록 안내할 수 있습니다
        } catch (Exception e) {
            System.err.println("다른 오류: " + e.getMessage());
        }
    }
}

오류 로깅

실제 애플리케이션에서는 로깅 시스템(예: java.util.logging, Log4j, SLF4J)을 사용하세요. 이를 통해:

  • 자세한 정보(스택 트레이스, 시간, 사용자 등)와 함께 오류를 기록할 수 있습니다.
  • 문제 식별과 해결을 위해 로그를 분석할 수 있습니다.
  • 사용자에게는 불필요하게 “무서운” 메시지를 보여 주지 않고, 로그에만 상세 정보를 남길 수 있습니다.

로깅 예시:

import java.nio.file.*;
import java.util.logging.*;

public class FileLoggerDemo {
    private static final Logger logger = Logger.getLogger(FileLoggerDemo.class.getName());

    public static void main(String[] args) {
        Path file = Paths.get("data.txt");

        try {
            Files.readAllLines(file);
        } catch (AccessDeniedException ade) {
            logger.severe("파일에 접근할 수 없습니다: " + file);
        } catch (Exception e) {
            logger.log(Level.SEVERE, "파일 처리 중 오류", e);
        }
    }
}

6. 자주 발생하는 실수

실수 1: 파일 작업에서 예외를 무시함.
Files.write(path, data)만 호출하고 try-catch를 생략하지 마세요 — 문제가 발생하면 프로그램이 중단될 수 있습니다.

실수 2: TOCTOU를 고려하지 않은 권한 확인.
Files.isWritable() 같은 메서드에만 의존하지 마세요. “가능”을 반환해도 실제 작업은 실패할 수 있습니다. 항상 예외(AccessDeniedException 등)를 처리하세요.

실수 3: 백업 없이 기존 파일 위에 덮어쓰기.
중요한 파일이라면 기록 전 백업을 만들거나 StandardCopyOption.ATOMIC_MOVE를 사용한 원자적 쓰기를 사용하세요.

실수 4: 장애 이후 임시 파일을 삭제하지 않음.
원자적 쓰기 중 문제가 발생하면 임시 파일이 남을 수 있습니다. finallyFiles.deleteIfExists()를 사용하세요.

실수 5: 접근 오류를 로깅하지 않음.
프로그램이 파일을 읽거나 쓰지 못했다면 사용자는 이를 알아야 하고, 개발자는 로그에서 상세 정보를 확인할 수 있어야 합니다. java.util.logging/SLF4J를 사용해 스택 트레이스와 함께 예외를 기록하세요.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION