1. 소개
가장 기본적인 것부터 시작해 봅시다. 이진 파일은 텍스트 파일과 무엇이 다를까요? 텍스트 파일은 일반 메모장에서 열어 문자, 숫자, 공백 등 기호를 눈으로 확인할 수 있는 파일입니다. 예를 들어 my_notes.txt 또는 poem.txt가 있습니다.
이진 파일은 텍스트가 아니라 임의의 바이트를 담는 파일입니다. 이미지(.jpg, .png), 음악(.mp3), 압축 파일(.zip), 실행 파일(.exe), 동영상(.mp4), 데이터베이스 파일 등일 수 있습니다. 이런 파일을 메모장에서 열면 ÿØÿà 같은 내용이나 이해할 수 없는 기호들의 벽이 보일 것입니다. 정상입니다! 컴퓨터는 바이트만 ‘이해’합니다 — 컴퓨터에게 텍스트, 이미지, 동영상은 모두 바이트의 연속일 뿐입니다. 텍스트 파일에서는 그 바이트를 문자로 해석할 수 있지만, 이진 파일에서는 사람이 읽도록 만들어지지 않은 ‘원시’ 데이터입니다.
이진 파일을 다루는 기본 클래스
Java에서는 이진 파일을 다루기 위해 바이트 스트림을 사용합니다:
- InputStream — 바이트를 읽는 기본 클래스.
- OutputStream — 바이트를 쓰는 기본 클래스.
파일을 대상으로 하는 구체 구현이 있습니다:
- FileInputStream — 파일에서 바이트를 읽습니다.
- FileOutputStream — 파일에 바이트를 씁니다.
FileReader와 FileWriter를 들어본 적이 있다면, 이들은 문자 기반으로 동작하며 텍스트에만 적합하다는 점을 기억하세요. 이진 파일에는 반드시 InputStream/OutputStream 과 그 하위 클래스를 사용하세요.
2. 이진 파일 읽기
바이트 단위로 읽기
가장 단순한 방법은 파일을 한 번에 1바이트씩 읽는 것입니다. 이해하기 쉽지만 매우 느립니다.
try (FileInputStream in = new FileInputStream("image.jpg")) {
int b;
while ((b = in.read()) != -1) {
// b는 0부터 255까지의 값(바이트)이며, -1은 파일의 끝을 의미합니다
// 바이트를 처리할 수 있습니다. 예를 들어 모든 바이트의 합을 계산할 수 있습니다
}
}
메서드 read()는 다음 바이트를 int(0부터 255)로 반환하고, 파일이 끝나면 -1을 반환합니다. 보통은 매우 특수한 경우(예: 파일 구조를 분석할 때)가 아니면 1바이트씩 읽지 않습니다.
블록 단위(버퍼 사용)로 읽기
1바이트씩 읽는 것은 사과를 하나씩 사러 상점에 여러 번 가는 것과 같습니다. 한 번에 봉지째로 가져오는 것이 훨씬 효율적이죠! Java에는 파일에서 바이트 배열을 채워 주는 메서드 read(byte[] buffer)가 있습니다.
try (FileInputStream in = new FileInputStream("image.jpg")) {
byte[] buffer = new byte[4096]; // 4 KB 버퍼
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
// buffer에는 파일에서 읽은 bytesRead 바이트가 들어 있습니다
// 이 바이트들을 처리할 수 있습니다. 예를 들어 다른 곳에 저장할 수 있습니다
}
}
메서드 read(buffer)는 실제로 읽은 바이트 수를 반환합니다(특히 마지막 읽기에서는 버퍼 크기보다 적을 수 있습니다). 이 방식은 디스크 접근 횟수가 줄어 더 빠릅니다.
예시: 파일 복사
어떤 이진 파일이든(예: 이미지) 한 위치에서 다른 위치로 복사하는 간단한 프로그램을 작성해 봅시다.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BinaryCopyExample {
public static void main(String[] args) {
String source = "cat.jpg";
String dest = "cat_copy.jpg";
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 8 KB — 대부분의 작업에 적합한 크기
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("복사가 완료되었습니다!");
} catch (IOException e) {
System.out.println("복사 중 오류: " + e.getMessage());
}
}
}
간단합니다. 원본 파일에서 블록을 읽어 바로 새 파일에 씁니다. 이 방법은 이미지, 압축 파일, 동영상 등 어떤 파일에도 동작합니다.
3. 이진 파일 쓰기
바이트 배열 쓰기
네트워크에서 받았거나 프로그램에서 생성한 바이트 배열이 있다면, 다음과 같이 파일에 쓸 수 있습니다:
byte[] data = new byte[] {1, 2, 3, 4, 5}; // 예시 배열
try (FileOutputStream out = new FileOutputStream("data.bin")) {
out.write(data); // 배열 전체를 파일에 씁니다
}
메서드 write(byte[])는 배열의 모든 바이트를 기록합니다. 배열의 일부만 기록할 수도 있습니다: out.write(data, offset, length).
부분적으로 파일 쓰기(예: 복사 시)
읽기 때와 마찬가지로 보통 버퍼를 사용합니다:
try (FileInputStream in = new FileInputStream("source.bin");
FileOutputStream out = new FileOutputStream("dest.bin")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
여기서는 파일에서 읽은 내용을 즉시 다른 파일에 씁니다. 이런 코드는 압축 프로그램, 다운로드 도구, 이미지 처리기 등에서 자주 볼 수 있습니다.
4. 유용한 팁과 주의점
왜 이진 파일에 Reader/Writer를 쓰면 안 될까요?
Reader와 Writer는 바이트가 아니라 문자(char)로 동작합니다. 이들은 인코딩(예: UTF-8)에 따라 바이트를 문자로 자동 변환합니다. 텍스트에는 편리하지만, 이진 파일에는 치명적입니다!
FileWriter로 그림을 기록하려고 하면 열 수 없는 손상된 파일이 됩니다. 기억하세요: 텍스트가 아닌 어떤 파일에도 InputStream/OutputStream 만 사용하세요!
이진 파일 작업의 핵심 차이점과 팁
- 버퍼 크기: 버퍼가 너무 작으면(디스크 접근이 너무 많아져) 느려지고, 너무 크면 불필요한 메모리를 차지합니다. 4–16 KB가 보통 적절합니다.
- 오류 처리: 항상 IOException을 처리하세요 — 파일이 없을 수도, 잠겨 있을 수도, 디스크 공간이 부족할 수도 있습니다.
- 스트림 닫기: try-with-resources를 사용하세요 — 오류가 나도 파일이 확실히 닫히도록 보장합니다.
- 파일 덮어쓰기: new FileOutputStream("file.bin")으로 열면 파일이 덮어써집니다. 끝에 이어 쓰려면 append = true 생성자를 사용하세요.
- 권한: 프로그램이 파일을 열지 못한다면 읽기/쓰기 권한을 확인하세요.
- readAllBytes(): 한 줄로 파일 전체를 바이트 배열로 읽을 수 있습니다. 큰 파일에는 메모리를 모두 소모할 수 있으니 사용하지 마세요!
5. 이진 파일 작업에서 흔한 실수
오류 №1: 이진 파일에 FileReader/FileWriter를 사용하는 경우. 이러한 클래스는 바이트를 문자로(그리고 그 반대로) 변환하므로 데이터가 손상됩니다. 이미지, 압축 파일 등에는 치명적입니다.
오류 №2: read()의 반환값을 무시함. 메서드 read(byte[])는 요청한 크기보다 적게 읽을 수 있으며, 특히 마지막 블록에서 그렇습니다. 실제로 처리한 바이트 수를 알기 위해 반환값을 항상 사용하세요.
오류 №3: 스트림을 닫지 않음. 파일을 닫지 않으면 잠긴 상태가 되거나 데이터가 끝까지 기록되지 않을 수 있습니다(특히 쓰기 시!). try-with-resources를 사용하세요.
오류 №4: 파일 크기를 고려하지 않고 전체를 메모리로 읽으려 함. 큰 파일에서는 OutOfMemoryError가 발생할 수 있습니다. 버퍼를 사용해 나누어 읽으세요.
오류 №5: 예외를 처리하지 않음. 파일 작업에서는 언제나 오류가 발생할 수 있습니다. 예: 파일 없음, 권한 부족, 디스크 가득 참. IOException을 반드시 처리하세요.
GO TO FULL VERSION