CodeGym /행동 /JAVA 25 SELF /파일 작업을 위한 모범 사례

파일 작업을 위한 모범 사례

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

1. 오류 처리: 예외를 무시하지 마세요!

파일 작업은 언제나 매우 예측 불가능한 외부 세계와의 상호작용입니다. 디스크가 가득 찰 수도 있고, 파일이 사라질 수도 있으며, 권한이 바뀔 수도 있고, 사용자는 상상 밖의 일을 하기도 합니다(예: 공백이 포함된 폴더에서 프로그램을 실행하거나, 곧 뽑힐 USB 메모리에서 실행). 코드가 이에 대비되어 있지 않다면, 단지 “크래시”로 끝나지 않고 사용자에게 필요한 데이터를 잃게 만들 수도 있습니다.

모범 사례는 단순한 “유행 조언”이 아니라, 시간으로 검증된 기법들의 모음으로서 가장 불쾌한 시나리오(데이터 손실, 메모리/리소스 누수, 민감 정보 노출, 그리고 동료와 특히 사용자에게 부끄러운 어처구니없는 버그)를 피하도록 도와줍니다.

왜 빈 catch를 쓰면 안 될까요?

Java(그리고 다른 언어에서도)에서는 다음과 같이 작성하고 싶은 유혹이 큽니다:

try {
    // 파일 작업
} catch (IOException e) {
    // 음, 실패했네 — 그냥 넘어가기!
}

이건 최악의 선택입니다. 이런 코드는 단지 오류를 “삼키는” 것을 넘어, 오류를 사용자와 개발자 본인에게 보이지 않게 만듭니다. 그 결과 문제가 생겨도 무엇이 언제 잘못됐는지 결코 알 수 없습니다.

올바른 방법

  • 오류를 로깅하세요: 최소한 콘솔에 메시지를 출력하거나 로그 파일에 기록하세요.
  • 사용자에게 알려 주세요: 오류가 치명적이라면 친절한 메시지를 보여 주세요.
  • 불필요한 정보는 노출하지 마세요: 시스템 내부 세부사항은 보여 주지 마세요(예: 전체 stack trace는 개발자용입니다).

예시:

try {
    List<String> lines = Files.readAllLines(Path.of("data.txt"));
    // 데이터 처리
} catch (IOException e) {
    System.err.println("파일을 읽는 중 오류: " + e.getMessage());
    // 자세한 내용을 로그 파일에 기록할 수 있습니다
    e.printStackTrace(System.err);
}

왜 구체적인 예외를 잡는 것이 중요할까요?
오류마다 대응이 다르기 때문입니다. 예를 들어 파일이 없으면 사용자가 다른 파일을 고르게 할 수 있습니다(NoSuchFileException 또는 FileNotFoundException). 권한이 없으면 필요한 권한으로 프로그램을 실행하도록 안내하세요(AccessDeniedException). 디스크가 가득 찼다면 공간을 확보하도록 제안하세요(쓰기 중 IOException).

2. 권한과 보안

작업 전에 접근 권한을 확인하세요

파일을 읽거나 쓰기 전에 권한이 있는지 확인하는 것이 좋습니다. Java에는 다음 메서드가 있습니다:

  • File.canRead()
  • File.canWrite()

이 메서드가 true를 반환하더라도 성공을 보장하지는 않습니다 — 권한은 언제든 바뀔 수 있습니다(예: 다른 프로세스가 권한을 변경). 그러므로 항상 예외에 대비하세요.

예시:

File file = new File("config.properties");
if (!file.canRead()) {
    System.err.println("파일을 읽을 권한이 없습니다!");
    return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
    // 파일 읽기
} catch (IOException e) {
    System.err.println("읽는 중 오류: " + e.getMessage());
}

내부 세부 정보를 노출하지 마세요

프로그램이 비밀번호처럼 민감한 파일을 다룬다면, 사용자에게 보이는 오류에 해당 파일의 경로나 내용 등을 출력하지 마세요.

3. 중요한 작업에 상대 경로를 사용하지 마세요

상대 경로(new File("data.txt"))는 현재 작업 디렉터리를 기준으로 하는데, 프로그램이 어떻게 실행되었는지(예: IDE에서 실행 vs. 커맨드라인에서 실행)에 따라 달라질 수 있습니다. 이는 혼란과 오류를 유발할 수 있습니다.

모범 사례: 중요한 파일에는 절대 경로를 사용하거나 작업 디렉터리를 명시적으로 지정하세요.

예시:

String userHome = System.getProperty("user.home");
Path configPath = Path.of(userHome, "myapp", "config.properties");

4. 임시 파일과 디렉터리 다루기

임시 파일은 왜 필요할까요?

임시 파일은 다양한 작업에 필요합니다. 예를 들어 중간 처리 단계에서 먼저 데이터를 임시 파일에 기록한 뒤, 이 파일로 본 파일을 교체할 수 있습니다. 또 다른 경우로, 프로그램 종료 후 필요하지 않아 안전하게 삭제해도 되는 정보를 임시로 저장하는 데 유용합니다.

임시 파일을 안전하게 만드는 방법

java.nio.file.Files의 메서드를 사용하세요:

Path tempFile = Files.createTempFile("myapp_", ".tmp");
// ... 파일 작업
Files.deleteIfExists(tempFile);

임시 디렉터리

Path tempDir = Files.createTempDirectory("myapp_");

5. 신뢰성: 백업과 무결성 검사

중요한 파일을 변경할 때는 백업을 사용하세요

설정 같은 중요한 파일을 덮어쓰기 전에 백업을 만들어 두세요:

Path config = Path.of("config.properties");
Path backup = Path.of("config.properties.bak");
if (Files.exists(config)) {
    Files.copy(config, backup, StandardCopyOption.REPLACE_EXISTING);
}

쓰기 중 문제가 생기면 언제든 백업에서 복원할 수 있습니다.

데이터 무결성을 확인하세요

특히 중요한 데이터에는 체크섬(예: MD5 또는 SHA-256)을 사용할 수 있습니다. 파일을 쓴 뒤에는 checksum을 계산해 함께 저장하고, 읽을 때 변경 여부를 검증하세요.

SHA-256 계산 예시(암호화 애호가를 위해):

import java.security.MessageDigest;
import java.nio.file.Files;
import java.nio.file.Path;

byte[] data = Files.readAllBytes(Path.of("important.dat"));
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
// 해시를 별도의 파일로 저장하거나 읽을 때 비교합니다

6. 파일 검사와 사용 사이의 창을 최소화하세요

이는 익숙한 고전적인 TOCTOU(Time Of Check To Time Of Use) 문제입니다: 파일이 존재하는지 확인한 순간과 실제로 읽기 시작하는 순간 사이에 파일이 사라지거나 변경될 수 있습니다.

그래서 검증과 사용을 항상 하나의 try 블록 내에서 수행하려고 하세요. 그리고 방금 검사를 했더라도 반드시 예외를 처리하세요.

예시:

Path filePath = Path.of("data.txt");
if (Files.exists(filePath)) {
    try (BufferedReader reader = Files.newBufferedReader(filePath)) {
        // 파일 읽기
    } catch (IOException e) {
        System.err.println("파일을 읽는 중 오류(파일이 사라졌을 수 있음): " + e.getMessage());
    }
}

7. 추가로 유용한 몇 가지 팁

모든 리소스에 try-with-resources를 사용하세요
AutoCloseable을 구현한 모든 클래스(대부분의 Java IO/NIO 스트림)는 try-with-resources로 사용할 수 있습니다. 이는 리소스 누수를 방지합니다.

try (BufferedReader reader = Files.newBufferedReader(Path.of("data.txt"))) {
    // 읽기
}

임시 파일 삭제를 잊지 마세요

Files.deleteIfExists(tempFile);

리소스를 이중으로 닫지 마세요
try-with-resources를 사용한다면 close()를 수동으로 호출하지 마세요 — 오류와 중복 종료 시도를 야기할 수 있습니다.

8. 파일 작업에서 흔한 실수

오류 1: 예외 무시.
catch를 쓰는 것은 파리를 손으로 잡아놓고 다시 놓아주는 것과 같습니다. 무엇이 잘못됐는지 항상 로깅하거나 최소한 사용자에게 알려 주세요.

오류 2: 스트림을 닫지 않음.
스트림을 닫지 않으면 파일이 잠긴 채로 남을 수 있고, 시스템의 파일 디스크립터가 고갈될 수 있습니다. try-with-resources를 사용하세요.

오류 3: 중요한 파일에 상대 경로 사용.
작업 디렉터리가 항상 기대한 값이라고 믿지 마세요. 경로를 명시적으로 지정하거나 특별한 디렉터리(user.home, java.io.tmpdir)를 사용하세요.

오류 4: 백업 없이 중요한 파일을 덮어쓰기.
중요한 것을 덮어쓰기 전에 백업을 만드세요. 이것이 여러분과 사용자의 데이터를 지켜 줍니다.

오류 5: 권한을 확인하지 않음.
사용자에게 필요한 파일이나 디렉터리에 대한 읽기/쓰기 권한이 있는지 확인하세요 — 그렇지 않으면 예기치 않은 AccessDeniedException을 만나게 됩니다.

오류 6: TOCTOU 창.
검사와 사용 사이에 다른 누군가가 파일을 변경하거나 삭제할 수 있습니다. 검사 후에도 항상 예외를 처리하세요.

오류 7: 임시 파일과 잔여물을 남겨둠.
프로그램이 비정상 종료되거나 오류가 발생하면 임시 파일이 남을 수 있습니다. 특히 민감한 데이터라면 더더욱 잊지 말고 삭제하세요.

1
설문조사/퀴즈
파일 작업 시 발생하는 오류, 레벨 38, 레슨 4
사용 불가능
파일 작업 시 발생하는 오류
파일 작업 시 발생하는 오류
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION