1. 손상된 파일의 징후
이상적인 세계에서는 파일이 항상 오류 없이 읽히고 쓰이며, 그 안의 데이터는 갓 구운 빵처럼 부드럽고 향기롭고 가지런하고 온전합니다. 하지만 현실에서는 파일이 ‘깨지거나’, ‘틀어지거나’, ‘반쪽짜리이거나’, ‘형식이 맞지 않는’ 경우가 있습니다. 원인은 다양합니다. 예를 들어, 디스크에 기록하는 도중 전원이 갑자기 꺼져 쓰기 오류가 발생하거나, 파일 전송 중 네트워크 오류가 생기거나, 저장매체 자체가 고장났을 수 있습니다(골칫거리인 bad sector 등). 혹은 부적절한 프로그램으로 파일을 수동 편집하면 파일이 손상될 수 있고, 기대한 데이터 형식과 실제 형식이 일치하지 않아도 문제가 생깁니다.
Java에서는 이러한 상황이 보통 읽기/쓰기 중 예외로 드러나며, 때로는 프로그램의 이상 동작(예: 데이터가 갑자기 끝나거나 의미 없는 문자가 나타남)으로 나타납니다.
읽기 중 발생하는 예외
가장 분명한 신호는 예기치 않은 예외입니다. 예를 들면 다음과 같습니다:
- EOFException — 예기치 않은 파일 끝(End Of File). 파일에 아직 데이터가 더 있을 것이라 기대했지만 실제로는 없습니다.
- MalformedInputException (또는 구형 API에서는 NIO의 MalformedInputException) — 파일이 기대한 인코딩 또는 구조를 따르지 않습니다.
- ZipException — 아카이브를 일반 파일처럼 읽으려 할 때.
- StreamCorruptedException — 직렬화된 객체를 읽는 중 파일이 손상된 경우.
데이터 형식 불일치
때로는 예외 없이 읽히지만, 내용이 기대한 형식과 맞지 않을 수 있습니다:
- 문자열을 기대했는데 알 수 없는 문자들의 집합을 받음.
- 특정 개수의 숫자를 기대했는데 실제로는 더 적음.
- CSV 형식을 기대했는데 실제로는 JSON(혹은 그 반대).
실제 사례
예를 들어, 각 줄이 하나의 작업을 나타내는 텍스트 파일에 작업 목록을 저장하는 애플리케이션을 작성하고 있다고 합시다. 그런데 사용자가 그 파일을 Excel로 열어 수정한 뒤 다른 형식으로 저장했다면... 이제 프로그램은 그 파일을 읽지 못할 수 있습니다.
2. 손상된 파일 처리 전략
로깅과 사용자 알림
첫 번째 원칙: 당황하지 마세요! (그리고 사용자도 당황하지 않게 하세요.) 항상 오류를 로깅하고, 문제가 발생했음을 사용자에게 알려 주세요. 다만 Java 스택의 모든 공포를 상세히 보여줄 필요는 없습니다.
try {
// 파일 읽기
} catch (EOFException e) {
System.err.println("파일이 예상치 못하게 끝났습니다. 손상되었을 수 있습니다.");
// 자세한 내용 로깅
e.printStackTrace();
}
부분 복구 시도
때로는 데이터의 일부라도 ‘구출’할 수 있습니다. 예를 들어 파일을 줄 단위로 읽을 수 있다면, 첫 오류가 발생할 때까지의 모든 줄을 처리할 수 있습니다.
백업(backup) 활용
중요한 프로그램은 기록 전에 중요한 파일의 백업을 자주 만듭니다. 기본 파일이 손상되었다면, 백업에서 데이터를 복원할 수 있습니다.
3. 실습: 예기치 않은 파일 끝(EOF)에서의 읽기
전형적인 상황
int 타입 숫자가 순서대로 기록된 바이너리 파일이 있다고 합시다. 프로그램은 정확히 5개를 기대하지만, 파일이 손상되어 3개만 기록되었다고 가정합니다.
import java.io.*;
public class DamagedFileExample {
public static void main(String[] args) {
String filename = "numbers.bin";
// 예시: 5개 대신 3개의 숫자가 들어 있는 파일 생성
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(filename))) {
out.writeInt(42);
out.writeInt(7);
out.writeInt(2024);
// out.writeInt(1); out.writeInt(2); // 의도적으로 기록하지 않음!
} catch (IOException e) {
System.err.println("파일 생성 중 오류: " + e.getMessage());
}
// 이제 5개의 숫자를 읽어 보자
try (DataInputStream in = new DataInputStream(new FileInputStream(filename))) {
for (int i = 0; i < 5; i++) {
int number = in.readInt();
System.out.println("읽은 숫자: " + number);
}
} catch (EOFException e) {
System.err.println("파일이 예상치 못하게 끝났습니다! 손상되었을 수 있습니다.");
} catch (IOException e) {
System.err.println("읽기 오류: " + e.getMessage());
}
}
}
출력:
읽은 숫자: 42
읽은 숫자: 7
읽은 숫자: 2024
파일이 예상치 못하게 끝났습니다! 손상되었을 수 있습니다.
첫 오류가 날 때까지 읽기
많은 경우, 예외가 발생할 때까지 루프에서 데이터를 읽는 것이 합리적입니다. 이렇게 하면 최소한 일부 정보라도 얻을 수 있습니다.
try (DataInputStream in = new DataInputStream(new FileInputStream(filename))) {
while (true) {
try {
int number = in.readInt();
System.out.println("읽은 숫자: " + number);
} catch (EOFException e) {
System.out.println("데이터가 끝났습니다(또는 파일이 손상되었습니다).");
break;
}
}
} catch (IOException e) {
System.err.println("읽기 오류: " + e.getMessage());
}
4. 텍스트 파일과 인코딩 다루기
인코딩 문제
파일이 한 인코딩으로 저장되고 다른 인코딩으로 읽히면 디코딩 오류가 발생할 수 있습니다:
import java.nio.charset.*;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("tasks.txt"), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (MalformedInputException e) {
System.err.println("인코딩 오류! 파일이 손상되었거나 UTF-8이 아닙니다.");
} catch (IOException e) {
System.err.println("읽기 오류: " + e.getMessage());
}
중요: 때로는 예외 대신 문자가 깨져 보이는 현상을 볼 수 있습니다 — 이것 또한 손상되었거나 인코딩이 잘못되었음을 나타내는 신호입니다.
어떻게 처리할까?
- 사용자에게 문제를 알립니다.
- 다른 인코딩으로 열어 보기를 시도합니다.
- 데이터가 중요하다면 — 백업에서 복구하도록 안내합니다.
5. 데이터 복구: 전략
부분적으로 올바른 데이터 읽기
파일 구조가 허용한다면, 오류가 발생하기 전까지 읽어들인 데이터만이라도 ‘건져낼’ 수 있습니다. 예를 들어 파일이 줄 목록(한 작업 = 한 줄)이라면, 실패 지점 전까지의 모든 줄을 처리할 수 있습니다.
try (BufferedReader reader = new BufferedReader(new FileReader("tasks.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
// 한 줄 처리
}
} catch (IOException e) {
System.err.println("읽기 오류: " + e.getMessage());
// 이미 읽은 데이터를 저장하거나 사용자에게 복구를 제안할 수 있습니다
}
backup 파일 사용
미리 파일 사본을 만들어 두었다면(예: tasks.txt.bak), 그 사본에서 데이터를 복구할 수 있습니다:
File original = new File("tasks.txt");
File backup = new File("tasks.txt.bak");
if (!original.exists() && backup.exists()) {
// backup을 원본 위치로 복사
Files.copy(backup.toPath(), original.toPath(), StandardCopyOption.REPLACE_EXISTING);
System.out.println("백업에서의 복구가 완료되었습니다.");
}
체크섬과 검증
중요한 파일이라면 체크섬(예: MD5 또는 SHA-256)을 저장해 두고, 파일을 열 때마다 현재 값과 비교하세요. 일치하지 않으면 파일이 손상된 것입니다.
// 개략적인 예시(해시 계산 구현은 단순화를 위해 생략)
String expectedHash = "..."; // 이전에 저장해 둔 체크섬
String actualHash = calculateFileHash("tasks.txt");
if (!expectedHash.equals(actualHash)) {
System.out.println("파일 tasks.txt가 손상되었습니다! 백업에서 복구해 보세요.");
}
6. 손상된 파일 처리 시 흔한 실수
오류 №1: 파일 형식을 검증하지 않음. 각 줄이 숫자라고 기대했는데 실제로는 텍스트가 있다면 NumberFormatException이 발생합니다. 읽는 동안 지속적으로 데이터를 검증하는 것이 좋습니다.
오류 №2: try-with-resources를 사용하지 않음. try-with-resources를 사용하지 않으면 오류가 발생했을 때도 파일이 닫히지 않은 상태로 남아 복구나 삭제가 어려워질 수 있습니다.
오류 №3: 백업 없이 손상된 파일을 즉시 덮어씀. 오류가 났다고 곧바로 파일을 덮어쓰면 복구 가능성이 줄어듭니다. 먼저 백업을 만드는 것이 좋습니다.
오류 №4: 불충분한 안내. 파일이 손상되어 백업에서 복구되었음을 사용자에게 알려야 합니다. 그렇지 않으면 왜 일부 데이터가 사라졌는지 이해하지 못할 수 있습니다.
GO TO FULL VERSION