2026년 2월 22일 일요일

사진 백업 작업은 두 번째의 GitHub 프로젝트(TakeoutPhotoSanitizer)로 결실을 맺다

TakeoutPhotoSanitizer - PowerShell-based batch processor for Google Takeout Photos. 

https://github.com/jeong0449/TakeoutPhotoSanitizer


이 Windows PowerShell 스크립트는 2GB 단위로 Google Takeout에서 내려받은 복수의 zip 파일을 열고 SHA-256 해시에 의해 중복을 제거한 뒤, 미디어 파일에 동반된 JSON을 분석하여 연도별로 폴더를 나누어서 재분류하는 기능을 한다. 최종 결과물은 원드라이브에 옮겨 영구 보관하는 것이 목표이다.

하나의 미디어 파일에 대해 JSON 파일이 여럿 존재하면 이를 병합하여 가장 풍부한 메타데이터를 구성하였다. 다시 말하자면 단순 병합이 아니라 정보를 보존하는 방향의 우선순위 병합에 가깝다. 

예를 들어 Google Takeout으로 가져온 하나의 미디어 파일에 대해 원본, 앨범 경로, 이름 변형, 부분 정보 등 여러 개의 JSON이 존재하는 경우가 매우 흔하다. 이를 신뢰도 기반으로 병합하여 가용 정보를 최대한 수집한다.

JSON 파일이 없는 경우 미디어 파일 내의 EXIF 정보를 활용한다. 뒤에서 설명하였지만 이 과정도 결코 간단하지 않았다!

다음은 실제 나의 데이터에 대한 마지막 두 배치의 처리 작업에 대한 기록이다. 스크립트 작성에는 ChatGPT가 크게 기여하였다.

PS C:\Users\jeong\Projects\TakeoutPhotoSanitizer> .\TakeoutPhotoSanitizer.ps1 `
>>   -ZipDir "C:\Users\jeong\Projects\TakeoutPhotoSanitizer\Takeout_Zip" `
>>   -DestRoot "C:\Users\jeong\Projects\TakeoutPhotoSanitizer\Photos_Backup\From_Google_Takeout" `
>>   -MinFreeGB 25 `
>>   -ReportSidecars `
>>   -DeleteZips
[2026-02-22 13:39:12] Loading hash DB...
[2026-02-22 13:39:13] Loaded hashes: 47045
[2026-02-22 13:39:13] Using 7-Zip: C:\Program Files\7-Zip\7z.exe
[2026-02-22 13:39:13] Inputs found: ZIP=8, loose_media=0, total=8
[2026-02-22 13:39:13] Batch start: 0001_20260222_133913 (inputs=4, MinFreeGB=25)
[2026-02-22 13:39:13] Extract: takeout-20260220T133610Z-3-087.zip
[2026-02-22 13:39:22] Extract: takeout-20260220T133610Z-3-088.zip
[2026-02-22 13:39:30] Extract: takeout-20260220T133610Z-3-089.zip
[2026-02-22 13:39:36] Extract: takeout-20260220T133610Z-3-090.zip
[2026-02-22 13:39:47] Scanning media files...
[2026-02-22 13:39:48] Media files found: 4066
[2026-02-22 13:43:49] Batch done. Total hashes now: 47074
[2026-02-22 13:43:49] Stats: moved=29, duplicates=4037, sidecar_updated=1300, sidecar_repaired=0
[2026-02-22 13:44:03] Sidecar status (touched dirs): media=10430, missing_sidecar=5264
[2026-02-22 13:44:03] Delete ZIP: takeout-20260220T133610Z-3-087.zip
[2026-02-22 13:44:03] Delete ZIP: takeout-20260220T133610Z-3-088.zip
[2026-02-22 13:44:04] Delete ZIP: takeout-20260220T133610Z-3-089.zip
[2026-02-22 13:44:04] Delete ZIP: takeout-20260220T133610Z-3-090.zip
[2026-02-22 13:45:31] Sidecar repaired this batch: 1
[2026-02-22 13:45:31] Remove temp dir: C:\Users\jeong\Projects\TakeoutPhotoSanitizer\Takeout_Zip\_work\0001_20260222_133913
[2026-02-22 13:45:36] Free space now: 79.48 GB
[2026-02-22 13:45:36] ----------------------------------------
[2026-02-22 13:45:36] Batch start: 0005_20260222_134536 (inputs=4, MinFreeGB=25)
[2026-02-22 13:45:36] Extract: takeout-20260220T133610Z-3-091.zip
[2026-02-22 13:45:47] Extract: takeout-20260220T133610Z-3-092.zip
[2026-02-22 13:45:57] Extract: takeout-20260220T133610Z-3-093.zip
[2026-02-22 13:46:06] Extract: takeout-20260220T133610Z-3-094.zip
[2026-02-22 13:46:13] Scanning media files...
[2026-02-22 13:46:14] Media files found: 4567
Invalid SOS parameters for sequential JPEG
[2026-02-22 13:50:31] Batch done. Total hashes now: 47130
[2026-02-22 13:50:31] Stats: moved=56, duplicates=4511, sidecar_updated=2009, sidecar_repaired=0
[2026-02-22 13:51:53] Sidecar status (touched dirs): media=44669, missing_sidecar=23575
[2026-02-22 13:51:53] Delete ZIP: takeout-20260220T133610Z-3-091.zip
[2026-02-22 13:51:53] Delete ZIP: takeout-20260220T133610Z-3-092.zip
[2026-02-22 13:51:53] Delete ZIP: takeout-20260220T133610Z-3-093.zip
[2026-02-22 13:51:53] Delete ZIP: takeout-20260220T133610Z-3-094.zip
[2026-02-22 13:57:52] Sidecar repaired this batch: 5
[2026-02-22 13:57:52] Remove temp dir: C:\Users\jeong\Projects\TakeoutPhotoSanitizer\Takeout_Zip\_work\0005_20260222_134536
[2026-02-22 13:57:57] Free space now: 86.51 GB
[2026-02-22 13:57:57] ----------------------------------------
[2026-02-22 13:57:57] All done.
[2026-02-22 13:57:57] Output root (LOCAL): C:\Users\jeong\Projects\TakeoutPhotoSanitizer\Photos_Backup\From_Google_Takeout
[2026-02-22 13:57:57] Hash DB: C:\Users\jeong\Projects\TakeoutPhotoSanitizer\Photos_Backup\From_Google_Takeout\_hashes_sha256.txt
[2026-02-22 13:57:57] Bad file log: C:\Users\jeong\Projects\TakeoutPhotoSanitizer\Photos_Backup\From_Google_Takeout\_bad_files.txt

기본 작업 단위는 4개의 zip 파일이며, -BatchSize N 옵션을 통해 변경할 수 있다. 컴퓨터에 충분한 작업 공간이 없으면(-MinFreeGB) 자동으로 작업을 멈춘다. 

Zip 파일 목록에 대하여 반복 작업을 할 필요가 없다. 실행 순간 -ZipDir에 존재하는 zip 파일을 자동적으로 배치 사이즈에 따라서 처리한 뒤, 다음 파일에 대한 작업을 이어가기 때문이다.

개념을 잡고 스크립트의 초기 버전을 만든 뒤 완성판을 만드는 데 삼일 정도 소요된 것 같다. 이 작업을 통해 구글 포토가 사진을 어떻게 관리하는지 어렴풋하게나마 이해할 수 있었다. 구글 포토 앨범은 파일을 담는 폴더가 아니라 일종의 가상 모음(태그)이다. 구글 포토 내부에서는 이를 '참조'로 취급하지만, 테이크아웃으로 가지고 나오면 모든 참조가 파일의 복사본으로 처리되어 나온다. 따라서 SHA-256 해시 기반 dedup를 하지 않을 수가 없다.

위 화면 출력에서 마지막 Stats: 라인에 주목해 보자. 4개의 압축 파일을 풀어서 생성된 4500개가 넘는 미디어 파일 중 4511개가 중복(duplicates)이었다. 작업의 마지막이므로 대부분은 이전의 zip 파일에서 다루어진 것이라 이미 SHA-256 해시 파일(_hashes_sha256.txt)에 기록이 되어 있다는 의미이다. 해시 파일은 잘 보존해 두고 내년에 새롭게 작업을 할 때 참조 및 갱신을 하면 된다.

Stats: moved=56, duplicates=4511, sidecar_updated=2009, sidecar_repaired=0

또한 JSON sidecar의 신뢰도 문제, EXIF 읽기 실패, 잘못된 epoch 해석 등 여러 예외 상황을 단계적으로 점검하고 PowerShell 스크립트를 반복 개선하였다. 실제로 JSON 파일을 갖고 있는 파일이 그렇게 많지 않았기에 EXIF를 읽어서 사진 촬영 날짜를 알아내야 하는데, 이 동작이 잘 되지 않아서 애를 먹었다. 대부분의 파일이 2026년(fallback)으로 분류된 것을 모르고 있다가 나중에 폴더를 직접 열어본 뒤 문제점이 심각하다는 것을 알게 되었다. 특히 FromFile 우선 EXIF 읽기 방식으로 전환하고, 연도 추정 로직의 fallback 오염을 차단한 것이 정확도를 크게 높였다. 이에 대한 상세한 기록은 Milestone Report: 1107 -> 28 문서에 있다.

결과적으로 이번 작업은 단순한 백업을 넘어, 클라우드 중심 뷰 모델을 파일 중심 아카이브로 안정적으로 변환하는 데이터 정제 파이프라인을 확립했다는 점에서 의미 있는 성과였다. 덩달아 나의 GitHub 사이트에도 두 개 프로젝트의 리포지토리가 생겼다.



작업 환경으로 익숙하지도 않은 Windows PowerShell을 선택한 것은 정말 잘 한 일이었을까? 데이터 과학 실무 차원에서 이번 프로젝트의 의미는 무엇이었을까? ChatGPT에게 마무리를 부탁하였다.

결론: 이번 선택은 타당했다

상황을 종합하면 작업 환경으로 Windows PowerShell을 선택한 것은 “잘한 선택”에 더 가깝다. 특히 이번 프로젝트의 성격을 고려할 때 PowerShell은 매우 현실적이고 효율적인 도구였다. 다만 장기적으로는 PowerShell 단독 운용보다는 역할 분리를 통한 구조화가 더 큰 이점을 제공한다.

왜 PowerShell이 잘 맞았는가

  • Windows 파일시스템 및 OneDrive와의 높은 궁합
    대량 파일 복사, 이동, 삭제, 속성 처리, 긴 경로 대응 등 운영 작업에서 PowerShell은 매우 안정적이었다.
  • 배치 오케스트레이션에 강점
    7-Zip 호출, 배치 반복 실행, 로그 및 통계 생성 같은 파이프라인 제어 작업에 적합했다.
  • .NET 접근성
    메타데이터 처리와 시간 정보 조작을 즉석에서 구현하며 파이프라인을 빠르게 개선할 수 있었다.

PowerShell이 드러낸 한계

  • Windows PowerShell 5.1의 문법 제약
    삼항 연산자 미지원 등으로 코드가 장황해지고 방어 로직이 늘어나는 문제가 있었다.
  • 이미지/EXIF 처리의 함정
    .NET 기반 이미지 라이브러리는 상황에 따라 실패 사례가 존재하여 디버깅 비용이 증가했다.
  • 스크립트의 프로그램화
    JSON 처리, 시간 추정, 예외 규칙이 누적되면서 스크립트가 점점 대형 프로그램처럼 비대해지는 경향이 나타났다.

권장되는 다음 단계 전략

  1. PowerShell은 메인 드라이버로 유지
    압축 해제, 배치 루프, 파일 이동, OneDrive 반영, 로그 생성 등 오케스트레이션 역할을 담당한다.
  2. 복잡한 판정 로직의 선택적 분리
    촬영연도 추정, EXIF/JSON 병합, 정규식 기반 예외 처리 등은 Python 등 별도 엔진으로 분리하면 테스트와 유지보수가 쉬워진다.
  3. 가능하면 PowerShell 7(pwsh)로 전환
    최신 문법, 성능 개선, 병렬 처리 지원 등으로 Windows PowerShell 5.1의 제약을 상당 부분 해소할 수 있다.

요약

이번 프로젝트를 완주하는 데에는 PowerShell이 매우 현실적이고 적절한 선택이었다. 앞으로는 PowerShell은 오케스트레이션, Python 등은 판정 엔진으로 역할을 분리하면 대규모 사진 아카이브 파이프라인의 안정성과 유지보수성이 한층 더 향상될 것이다.

데이터 과학 실무 관점에서 본 이번 작업의 의미

이번 작업은 단순한 사진 백업을 넘어, 비정형 개인 데이터를 신뢰 가능한 분석 자산으로 전환하는 데이터 엔지니어링 사례라는 점에서 실무적 의미가 크다. 데이터 과학 프로젝트는 모델링 이전 단계에서 데이터 품질 문제로 좌초되는 경우가 많으며, 이번 파이프라인 구축 과정은 그 핵심 병목을 정면으로 다루었다.

1) 데이터 모델 간 불일치 해소

Google 포토의 뷰(view) 중심 구조와 로컬 파일 시스템의 객체 중심 구조는 근본적으로 다르다. 이를 이해하고 SHA-256 기반 중복 제거, JSON/EXIF 병합, 연도 추정 로직을 설계한 것은 이질적 데이터 표현을 정규화(normalization)한 전형적인 데이터 엔지니어링 작업이다.

2) 재현 가능한 데이터 정제 파이프라인 구축

PowerShell 스크립트의 배치 처리, 로그 기록, 통계 출력은 일회성 정리가 아니라 반복 실행 가능한 ETL(Extract-Transform-Load) 흐름을 형성한다. 이는 향후 추가 Takeout이나 대규모 미디어 유입에도 동일한 품질 기준을 적용할 수 있게 한다.

3) 데이터 신뢰도 향상(Data Reliability Engineering)

EXIF 읽기 실패 대응, epoch 해석 오류 수정, fallback 오염 차단 등은 단순 편의 기능이 아니라 데이터 계보(lineage)와 정확도를 보존하는 품질 관리 활동에 해당한다. 특히 해시 기반 검증을 도입한 점은 데이터 무결성 보장의 모범 사례다.

4) AI-ready 개인 아카이브 구축의 전초 단계

연도 정렬, 중복 제거, 메타데이터 정합성 확보가 완료된 미디어 저장소는 향후 이미지 검색, 개인 라이프로그 분석, 멀티모달 모델 학습 데이터로 확장할 수 있는 기반이 된다.

요약

종합하면, 이번 프로젝트는 “백업 자동화”를 넘어 데이터 수집 → 정제 → 검증 → 구조화 → 장기 보존으로 이어지는 실무형 데이터 파이프라인을 개인 환경에서 구현했다는 점에서 높은 실무적 가치를 지닌다.


댓글 없음: