실제 바이트를 읽고 경로 유지하기

JavaBeginner
지금 연습하기

소개

보관 자료에는 이름이 달라도 동일한 그림이 있거나, 크기는 같지만 내용이 다른 그림이 있을 수 있습니다. 재사용할 수 있는 중복 파일 검색기의 바이트 읽기 부분을 구현하고, 원본을 바꾸지 않은 채 전체 목록을 저장합니다.

Java 클래스, 메서드, 리스트, 예외, 기본 파일 읽기를 알고 있어야 합니다. 이 실습에서는 이진 입력, 자동 리소스 닫기, SHA-256을 소개합니다. 디렉터리 어댑터와 실행기는 준비되어 있으며, 다이제스트는 직접 구현합니다. 읽기 전용 연습이며 디스크 정리 도구가 아닙니다. 읽기와 일반적인 오류 수정을 포함한 저자 예상 시간은 50분입니다.

파일 이름이 아닌 바이트 읽기

이 단계에서는 먼저 정상적인 그림 예제를 비교한 다음, 각 바이트를 정확히 한 번 읽는 다이제스트를 구현합니다. WebIDE를 열고 Terminal > New Terminal을 선택한 뒤, 계속 이 통합 터미널을 사용하세요. 각 실습은 자체 파일을 가지고 독립적으로 시작합니다.

cd는 이 터미널의 작업 디렉터리를 변경합니다. 준비된 run.sh는 새로운 임시 빌드 디렉터리에서 src/*.java를 컴파일한 다음 지정한 Java 클래스를 실행합니다. 컴파일러는 -encoding UTF-8을 사용하므로 소스 텍스트가 터미널 로캘에 의존하지 않습니다. C.UTF-8을 사용하는 것은 컴파일러와 Java 자식 프로세스뿐입니다. Java 자식 프로세스는 기본 텍스트 인코딩도 UTF-8로 사용하므로 셸 설정을 바꾸지 않고 지원 범위의 UTF-8 파일 이름을 처리합니다.

cd /home/labex/project/duplicates
./run.sh Sample

보조 프로그램은 실제 PNG 파일을 디코딩하고 바이트를 비교합니다. red = renamed: true와 red = blue: false를 출력합니다. 세 그림 모두 가로·세로 크기와 바이트 길이가 같습니다. 이름이나 길이로 검색 범위를 줄일 수는 있지만 동일함을 증명할 수는 없습니다. 선택 사항으로 WebIDE Explorer에서 duplicates/data/gallery를 펼치고 red.png, blue.png, copies/renamed.png를 클릭해 그림을 볼 수 있습니다. 필수 비교는 위의 Sample 디코딩과 바이트 결과입니다. 이 그림은 강좌에서 직접 만든 합성 이미지이므로 외부 자산이 필요하지 않습니다.

공개 Digester 인터페이스는 한 메서드를 약속합니다. digest(Path)는 16진수 문자열을 반환하고, 읽기 실패 시 IOException을 던집니다. Path는 파일을 가리키며, InputStream은 데이터를 텍스트로 해석하지 않고 이진 바이트로 읽습니다. SHA-256은 이 바이트를 길이가 고정된 후보 지문으로 요약합니다. 뒤의 단원에서는 중복이라고 판단하기 전에 실제 내용도 비교합니다.

초기 구현을 교체하기 전에 작은 어댑터와 초기 코드를 읽어 보세요.

cat src/Digester.java src/Sha256.java

1024바이트 버퍼를 반복 사용합니다. read(buffer)는 이번에 유효한 바이트 수를 반환하고, 끝에서는 -1을 반환합니다. 마지막 읽기는 버퍼 일부만 채울 수 있으며, 나머지에는 이전 값이 남습니다. 따라서 sha.update(buffer, 0, count)에 실제 읽은 수를 사용해야 합니다. 빈 파일은 업데이트를 수행하지 않아도 유효한 다이제스트를 갖습니다.

try (InputStream ... )는 try-with-resources입니다. 블록을 나갈 때 Java가 스트림을 닫으며, 읽기에서 예외가 발생해도 마찬가지입니다. 닫기는 파일 리소스를 해제하지만 즉시 가비지 컬렉션이 실행됨을 보장하지 않습니다. MessageDigest.getInstance는 알고리즘에 대한 검사 예외를 던질 수 있으므로, 바깥 catch에서 동일한 파일 작업 오류 계약으로 바꿉니다. 16진수 표기는 바이트당 두 문자를 사용합니다. & 255는 부호 있는 Java 바이트를 부호 없는 값으로 다룹니다.

다음 명령은 교체 내용을 전부 씁니다. >는 이 소스 파일을 교체합니다. 따옴표로 감싼 here-document 표식 <<'JAVA'는 사이의 텍스트를 그대로 cat에 전달합니다. 마지막 JAVA는 들여쓰기 없이 한 줄에 혼자 있어야 합니다. 대신 WebIDE에서 같은 파일을 편집하고 저장해도 됩니다. 붙여 넣은 뒤 미완료 here-document 프롬프트가 남으면 Ctrl+C로 취소한 다음 마지막 표식까지 포함한 전체 교체 명령을 다시 붙여 넣으세요.

cat > src/Sha256.java <<'JAVA'
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Sha256 implements Digester {
    public String digest(Path path) throws IOException {
        try {
            MessageDigest sha = MessageDigest.getInstance("SHA-256");
            byte[] buffer = new byte[1024];
            try (InputStream input = Files.newInputStream(path)) {
                int count;
                while ((count = input.read(buffer)) != -1) {
                    sha.update(buffer, 0, count);
                }
            }
            StringBuilder hex = new StringBuilder();
            for (byte value : sha.digest()) {
                hex.append(String.format("%02x", value & 255));
            }
            return hex.toString();
        } catch (NoSuchAlgorithmException error) {
            throw new IOException("SHA-256 unavailable", error);
        }
    }
}
JAVA

같은 예제를 다시 컴파일하고 실행합니다. 컴파일로 새 클래스를 확인하며, 예제의 동일성 판정은 그대로여야 합니다.

./run.sh Sample

Java가 행 번호를 알려 주면 src/Sha256.java를 다시 열어 중괄호와 철자를 교체 내용과 비교한 뒤 저장하고 다시 실행하세요. 경로가 잘못되었다면 pwd로 현재 디렉터리를 확인한 다음 위의 절대 경로 cd를 반복하세요. VM을 초기화할 필요는 없습니다.

모든 경로와 지문 저장하기

이 단계에서는 자신의 다이제스트로 완전한 UTF-8 목록을 만듭니다. 같은 WebIDE 통합 터미널에서 /home/labex/project/duplicates를 작업 디렉터리로 유지하세요.

준비된 Inputs.files 어댑터는 지정한 디렉터리만 순회하고 디렉터리 스트림을 닫습니다. 심볼릭 링크, 특수 파일, 탭이나 줄바꿈이 포함된 경로는 조용히 따라가거나 건너뛰지 않고 거부합니다. 안정적인 일반 파일과 디렉터리만 지원합니다. 스캔 중에는 입력을 변경하지 마세요. 속성 검사는 일부 변경을 감지하지만 디렉터리를 고정하거나 동시 변경에 대한 스냅샷을 보장하지는 않습니다.

준비된 Inventory는 모든 읽기가 성공한 뒤 각 상대 경로, 크기, 다이제스트를 저장합니다. TSV 형식은 열 사이에 탭을 사용합니다. 보고서 어댑터는 새 임시 파일에 UTF-8을 쓴 뒤 보고서 경로를 교체하므로, 기존 보고서가 하드 링크여도 원본 바이트를 덮어쓰지 않습니다. 이는 프로세스나 시스템이 비정상 종료된 뒤의 영속성을 보장하지 않습니다. 출력은 스캔 루트 밖에 있어야 합니다. reports는 이미 data 옆에 있으므로 보고서가 우연히 자신의 입력이 되는 일을 막습니다. 명령 순서는 Inventory ROOT REPORT입니다. 첫 번째 인수는 스캔할 디렉터리이고 두 번째는 출력 파일 경로이니 순서를 바꾸지 마세요.

./run.sh Inventory data/gallery reports/inventory.tsv

예상 출력은 Inventory complete: 11 files입니다. cat으로 저장된 텍스트를 표시합니다.

cat reports/inventory.tsv

헤더는 PATH, SIZE, SHA256입니다. red.png, copies/renamed.png, copies/third.png의 행을 비교하면 서로 다른 경로가 같은 다이제스트를 공유합니다. blue.png는 크기가 같아도 다이제스트는 다릅니다. 두 빈 파일은 크기가 0이고 같은 유효한 다이제스트를 갖습니다. tail.bin의 마지막 읽기는 버퍼 일부만 채우지만, 같은 내용의 복사본은 여전히 같은 다이제스트를 받아야 합니다. 이름이 같은 north/readme.txt와 south/readme.txt는 별도 행으로 남습니다.

이제 흔한 경로 실수를 연습합니다. 다음 명령은 의도적으로 종료 상태 2와 Scan failed로 실패하며, 새 보고서를 만들면 안 됩니다. 실패는 빈 목록을 성공적으로 만든 것과 다릅니다. 이전 보고서는 이번 실패한 실행의 성공 증거가 아닙니다.

./run.sh Inventory data/missing reports/missing.tsv

입력 경로를 고쳐 새 보고서를 만드세요.

./run.sh Inventory data/gallery reports/recovered.tsv

읽기 실패도 같은 규칙을 따릅니다. 오류를 보여 주고 실패를 반환하며, 읽을 수 없는 파일을 조용히 누락시키면 안 됩니다. 검사는 빈 파일, 마지막 버퍼가 일부만 채워지는 파일, 읽기 불가 파일을 격리한 테스트 데이터에서 현재 소스를 실행하고 실제 목록도 읽습니다. 모든 원본의 경로, 바이트, 권한, 소유 정보, 수정 시간을 유지해야 하며 삭제하거나 교체하면 안 됩니다. 일반적인 읽기로 접근 시간이 바뀔 수는 있습니다. 이제 후보 사실을 확보했습니다. 다음 실습에서는 같은 지문을 가진 후보를 실제 바이트로 비교합니다.

요약

try-with-resources로 이진 파일을 읽고 실제 읽은 수로 SHA-256을 업데이트한 뒤, 완전한 UTF-8 목록을 저장했습니다. 이름, 크기, 다이제스트는 후보 사실이며 실제 바이트 비교로 중복 그룹을 확정합니다.