폴더를 오래 사용하다 보면 이름이 다른데 내용은 같은 파일, 백업 과정에서 여러 위치에 복사된 파일이 함께 쌓이곤 합니다. 파일명이나 수정 날짜만 보고 지우면 원본을 잘못 삭제할 수 있으므로, 이번에는 파일 크기로 후보를 줄인 뒤 SHA-256으로 실제 내용을 확인하는 중복 파일 찾기 프로그램을 직접 만들고 실행했습니다.
검사에서 끝내지 않고, 정리 후보를 바로 삭제하지 않도록 별도의 격리 폴더로 옮겼습니다. 마지막 작업을 되돌리는 기능과 CSV 보고서, EXE 실행까지 실제 테스트했습니다.

이번 프로그램에서 확인한 기능
| 구분 | 구현·검증 내용 |
|---|---|
| 검사 범위 | 선택 폴더와 하위 폴더를 재귀적으로 검사했습니다. |
| 후보 축소 | 파일 크기가 같은 항목끼리 먼저 묶었습니다. |
| 내용 확인 | 같은 크기 후보에만 SHA-256 해시를 계산했습니다. |
| 안전 정리 | 영구 삭제하지 않고 격리 폴더로 이동했습니다. |
| 복구 | 마지막 격리 작업을 원래 위치로 되돌렸습니다. |
| 기록 | 중복 그룹과 경로, 크기, 해시를 CSV로 저장했습니다. |
| 배포 | PyInstaller onefile 방식으로 단일 EXE를 만들고 다시 검사했습니다. |
테스트 파일을 일부러 여러 형태로 준비했습니다
단순히 같은 이름의 파일만 복사하지 않았습니다. 실제 폴더 정리 상황을 재현하기 위해 다음 조건을 섞었습니다.
- 이름과 내용이 모두 같은 복사본
- 파일명과 확장자는 다르지만 내용은 같은 파일
- 이름은 비슷하지만 내용은 다른 파일
- 중복이 없는 고유 파일
- 크기가 0인 빈 파일
빈 파일은 기본 설정에서 제외했습니다. 이름이 같다는 이유만으로 중복이라고 판단하지 않고, 마지막에는 실제 바이트 내용이 같은 파일만 한 그룹으로 표시되도록 구성했습니다.
1. 파일 크기로 해시 계산 대상을 먼저 줄였습니다
내용이 완전히 같은 두 파일은 크기도 같습니다. 따라서 모든 파일을 처음부터 해시로 읽지 않고, 먼저 파일 크기를 기준으로 묶었습니다. 한 크기 그룹에 파일이 하나뿐이면 중복일 수 없으므로 해시 계산에서 제외했습니다.
size_groups: dict[int, list[Path]] = {}
for path in files:
size = path.stat().st_size
size_groups.setdefault(size, []).append(path)
candidates = [
group for group in size_groups.values()
if len(group) > 1
]
첫 검사에서는 총 10개 파일 가운데 SHA-256 확인이 필요한 파일이 7개로 줄었습니다. 파일이 많은 폴더일수록 이 사전 분류가 불필요한 디스크 읽기를 줄이는 데 도움이 됩니다.
2. 같은 크기 후보만 SHA-256으로 확인했습니다
파일 크기가 같아도 내용은 다를 수 있습니다. 최종 중복 여부는 SHA-256 해시가 같은지 확인했습니다. 큰 파일을 한 번에 메모리로 읽지 않고 1MB씩 나누어 처리했습니다.
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
while chunk := file.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
검사 결과는 중복 그룹 2개, 정리 후보 3개, 절약 가능한 용량 141.2KB, 오류 0개였습니다. 각 그룹에서 한 파일은 보관 기준으로 남기고 나머지만 정리 후보로 구분했습니다.

3. 바로 삭제하지 않고 격리 폴더로 이동했습니다
중복으로 판정되더라도 곧바로 영구 삭제하면 실수를 되돌리기 어렵습니다. 프로그램에서는 선택한 정리 후보를 검사 폴더 아래의 시간별 격리 폴더로 이동하도록 했습니다. 실행 전에는 선택 파일 수와 예상 확보 용량을 한 번 더 확인했습니다.

원래의 하위 폴더 구조를 격리 폴더 안에도 유지했습니다. 같은 이름의 파일이 서로 다른 폴더에 있더라도 덮어쓰지 않도록 하기 위한 처리입니다.
relative = source.relative_to(root)
destination = quarantine / relative
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(source), str(destination))

테스트에서는 선택한 3개 파일이 정상적으로 격리되었고 141.2KB가 이동되었습니다. 격리 폴더와 이전 작업 기록 파일은 다음 검사에서 자동으로 제외해, 방금 옮긴 파일이 다시 중복으로 잡히지 않도록 했습니다.
4. 마지막 격리 작업을 원래 위치로 되돌렸습니다
격리할 때 원래 경로와 이동 경로를 JSON 작업 기록으로 남겼습니다. 되돌리기에서는 목적지 파일이 존재하는지, 원래 위치에 다른 파일이 이미 생기지 않았는지 확인한 뒤 복구했습니다.
if source.exists():
raise FileExistsError("원래 위치에 파일이 이미 있습니다.")
source.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(quarantined), str(source))


세 파일을 모두 복원한 뒤 다시 검사했으며 중복 그룹 2개와 정리 후보 3개가 동일하게 나타났습니다. 단순히 완료 메시지만 확인하지 않고, 복구 후 재검사까지 진행해 원상 복구 여부를 확인했습니다.
5. CSV 보고서도 한글이 깨지지 않게 저장했습니다
검사 결과는 파일명, 폴더, 크기, SHA-256, 그룹, 보관 기준 여부를 포함한 CSV로 저장했습니다. Windows 환경에서 한글 파일명을 확인하기 쉽도록 UTF-8-SIG 인코딩을 사용했습니다. 보고서가 검사 대상 폴더 안에 생성되더라도 내용이 고유하면 중복 그룹에는 포함되지 않습니다.
with report_path.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.writer(file)
writer.writerow(["그룹", "구분", "파일명", "폴더", "크기", "SHA-256"])
6. 단일 EXE로 만든 뒤 같은 검사를 반복했습니다
Python 코드 실행만 확인하지 않고 PyInstaller의 onefile·windowed 방식으로 콘솔창 없는 단일 EXE를 만들었습니다.
py -m PyInstaller --noconfirm --onefile --windowed --clean --name "JKLAB365_Duplicate_File_Finder" main.py

생성된 EXE의 크기는 약 11.7MB였습니다. EXE를 직접 실행해 같은 테스트 폴더를 검사했으며, 앞서 만든 CSV 보고서가 추가되어 총 파일 수는 11개로 표시되었습니다.


| 실행 방식 | 검사 파일 | SHA-256 확인 | 중복 그룹 | 정리 후보 | 절약 가능 | 오류 |
|---|---|---|---|---|---|---|
| Python 실행 | 10개 | 7개 | 2개 | 3개 | 141.2KB | 0개 |
| onefile EXE | 11개 | 7개 | 2개 | 3개 | 141.2KB | 0개 |
직접 만들면서 실수하기 쉬웠던 부분
- 파일명만 비교하지 않았습니다. 이름이 달라도 내용이 같은 파일이 있고, 이름이 비슷해도 내용이 다른 파일이 있기 때문입니다.
- 모든 파일의 해시를 처음부터 계산하지 않았습니다. 먼저 크기로 후보를 줄여 불필요한 읽기를 줄였습니다.
- 파일 전체를 한 번에 메모리로 읽지 않았습니다. 1MB 청크 단위로 처리해 큰 파일에도 대응했습니다.
- 중복 후보를 영구 삭제하지 않았습니다. 격리와 되돌리기를 거치도록 해 오판에 대비했습니다.
- 격리 폴더를 재검사하지 않았습니다. 이동한 복사본이 다시 중복으로 잡히는 문제를 막았습니다.
- 완료 메시지만 믿지 않았습니다. 격리, 복원, 재검사, EXE 실행 순서로 결과를 다시 확인했습니다.
사용 전에 알아둘 점
- 대용량 파일이 많으면 SHA-256 계산에 시간이 걸릴 수 있습니다.
- 검사 중 파일이 변경되거나 잠겨 있으면 오류 내역으로 분리됩니다.
- SHA-256은 파일 식별에 널리 사용되는 해시이지만, 이 프로그램은 중요한 원본의 백업을 대신하지 않습니다.
- 격리 폴더는 안전 확인을 위한 임시 공간입니다. 중요한 자료는 별도의 백업을 유지해야 합니다.
- 이번 버전에는 영구 삭제 기능을 넣지 않았습니다. 원본을 확인한 뒤 사용자가 직접 최종 정리하도록 했습니다.
마무리
이번 작업에서는 중복 파일을 찾는 것보다 잘못 지우지 않는 과정을 더 중요하게 두었습니다. 파일 크기로 후보를 줄이고, SHA-256으로 내용을 확인하고, 격리와 되돌리기로 안전장치를 추가했습니다. Python 실행과 단일 EXE 실행에서 같은 중복 그룹과 정리 후보가 확인되어 실제 사용 흐름까지 검증할 수 있었습니다.