2026년 2월 27일 금요일

갈라질 결심(Creating a Git Branch)

갈라진다고 하여 사람이나 조직과 인연을 끊거나 헤어진다는 뜻은 아니고, Git에서 새로운 기능을 넣기 위해 처음으로 메인에서 독립하여 별도의 브랜치를 만들겠다는 뜻이다.


Nano Ardule 드럼 패턴학 생태계에서 PC쪽 패턴 생성 및 편집기, 즉 APS(Ardule Pattern Studio)는 기본적으로 키보드를 두드려서 입력을 하는 데 충실하게 만들었다. 실시간 연주를 받아들이는 것이 아니라 StepSeq, 즉 16분음표 단위의 스텝으로 커서을 이동하여 선정된 악기의 타격 신호를 기록한다. 2차원 그리드 위에 체크 표시를 한다고 생각하면 간단하다. 어제도 코드 효율을 높이기 위한 약간의 수정을 하여 commit를 한 상태이다.

이번 단계의 목표는 AKAI MPK Mini MKII를 입력 장치로 쓰게 만드는 것이다. 그러면 컨트롤러의 4x2 패드를 이용하여 드럼 패턴을 입력할 수 있기 때문이다. 이미 패드를 두드려서 스텝 단위 입력을 할 수 있게 만들어 두었으나, 욕심을 좀 더 부려서 마치 루퍼처럼 오버더빙 형식의 실시간 입력을 가능하게 만들고 싶다.

그러려면 상당한 수준의 코드 수정이 필요하기에 아예 이번 기회에 branch를 새롭게 생성해 보기로 하였다. 브랜치 생성 요령은 별도의 문서('Safe Git Branching Guide for StepSeq Refactoring')로 정리해 두었다. 어제까지 완성하여 안정화된 기능(키보드를 통한 스텝 입력)은 일절 건드리지 않아야 한다.

그리고 한가지 더. GUI나 DAW로 관심이 이동하면 절대로 안 된다! 그건 나의 본분을 잊는 행위이다. 목표를 명확히 하고 개발의 시행착오를 줄이기 위하여 이번 단계의 설계 개념은 문서로 충분히 작성해 두었다. 

레코딩('녹음'이라고 표현하면 오디오 신호 자체의 기록이라는 느낌이 강하게 든다)을 자체 개발 파이썬 스크립트로 소박하게 구현해 보면서 몇 가지 중요한 개념을 공부하게 되어 좋은 기회라고 생각한다.  녹음과 재생에 대한 각종 설정 또는 제어 상태는 다음과 같은 계층 구조로 이해하면 좋다.

  1. Transport layer: 플레이헤드 진행 및 루프 범위 제어
  2. Record arm layer: 입력이 패턴을 수정할 수 있는지 결정
  3. Write policy (overwrite or overdub): 기존 데이터와 새 입력의 병합 방식 정의

만약 이 브랜치에서 성공적으로 개발이 완료된다면, APS의 StepSeq은 아주 쓸모 있는 장난감이 될 것이다. 

그리고... 업무 스트레스를 달리기로 푼 어제의 기록. 기온이 많이 올라서 야간 달리기를 하기에 아주 좋았다. 479kcal를 태웠으니 라면 한 그릇의 열량을 소모한 정도?

만족스럽지 못한 2월의 달리기 기록. 일본 여행과 날씨 탓이 크다. 어제는 오랜만에 7km를 넘게 달렸다.


2026년 2월 26일 목요일

데이터 분야 'AI 준비도'(AI readiness)의 압박

지금은 인공지능 대전환기이자 대혼란기이다. '대혼란기'라고 한 이유는, AI와 관련하여 '몇 시까지 자료 만들어 주세요(보통 2~3시간 이내)'라는 급박한 요청이 부쩍 늘어났기 때문이다. 자발적인 것이든 외부 요인에 의해 어쩔 수 없는 것이든 실제로 AI에 적응하려면 조직, 나아가 개인의 일하는 방식이나 사고 방식에 이르기까지 해체에 가까운 변혁이 필요하다. 좀 천박하게 말한다면 매일 'AI 대환장 파티'가 벌어지고 있다.



IBM의 웹사이트에서  Thriving through AI disruption이라는 글을 읽어 보았다. AI 준비도(AI readiness)의 5대 축은 다음과 같이 묘사된다.

  1. Strategy & business alignment
  2. Data foundation
  3. Technology & architecture
  4. Talent, skills & culture
  5. Operating model & governance

내가 일하고 있는 조직, 특히 바이오와 같은 high-stakes 영역에서는 2번 data foundation이 가장 중요하다. 왜냐하면 우리는 바이오 연구 데이터의 수집, 관리 및 활용 기반을 제공하는 곳이기 때문이다. 이러한 체제를 갖추어서 실제로 부끄럽지 않은 수준으로 돌아가게 된 지는 그렇게 오래 되지 않았다.

논문에 쓰인 연구 데이터를 과학 발전을 위해 공유하는 아름다운 관행은 인공지능이 유행을 타기 훨씬 전부터 널리 퍼져 있었다. 이를 위해서 데이터 리포지토리가 매우 중요한 역할을 해 왔다. 현재의 문제는 리포지토리에 모인 데이터를 인공지능 학습용으로 그대로 가져다 쓸 수 있느냐에 있다. 

완벽한 데이터란 실제로 그렇게 많이 존재하지 않는다. 그러나 우리는 다음과 같은 말을 많이 듣는다.

'쓸 만한 데이터가 없다'
'AI 학습이 가능한 형태로 가공되어 있지 않다' 

수요자 입장에서는 충분히 나올 수 있는 이야기이다. 하지만 이것이 'AI 투입을 위해 모든 자원이 갖추어져야 한다'라는 입장으로 변질되어 권력의 틀을 쓰면 모두가 힘들어진다.

우리가 늘 준거집단으로 삼는 미국에서는 최근 Genesis Mission의 출범을 통해 AI 총동원 기조가 강화되고 있다. 데이터와 (인적·컴퓨팅)자원을 AI에 총동원해야 한다는 주장은 무한 경쟁 시대의 국가적 생존이나 경제·안보 등의 키워드를 등에 업으면 그 자체가 '권력'이 된다. 특히 더 나은 모델을 늘 벤치마킹해야 한다는 압박에 시달리는 우리나라에서는 국외의 사례가 정책 정당성의 근거로 과도하게 사용될 위험이 있다.

  • 선진국 사례가 곧 '정당성'이 아니다.
  • 국가안보 또는 경쟁 프레임은 안전과 권리에 대한 논의를 약화시킨다.
  • AI 에이전트/파운데이션 모델은 검증 비용을 없애지 않는다.

나에게 본업이 아닌 취미 활용 영역에서 AI란 매우 흥미롭고 효율을 높여주는 도구이지 동반자였다. AI가 제시한 전자회로와 코드는 반드시 나의 실증을 거쳤고, 그러한 최종 마무리 과정에는 결코 적지 않는 노력이 들었다. 확인되지 않은 AI 활용 결과물은 개인 차원의 재미 추구에는 문제가 없다. 그러나 그것이 정책으로 번져 나가면 그 파급효과는 너무나 크다. 가뜩이나 쏠림이 심한 우리나라 사회에서 역적으로 몰리기는 너무나 쉬운 일이니까.

AI가 애써 고민하고 결론을 내리는 인간의 영역을 완전히 대체하는 방향까지 나아가는 것은 경계한다.

다음과 같은 글에도 관심을 가져 보자.

데이터센터 모라토리엄 - 경향신문 에디터의 창, 2월 26일.

'AI 데이터센터 특별법처럼 이익은 사유화하고 비용은 공공이 지게 하려는 시도에 제동을 걸어야 한다.'

2026년 2월 25일 수요일

포커스 그룹 인터뷰(FGI)의 그늘: 우리가 놓치기 쉬운 부작용

A lot of times, people don't know what they want until you show it to them. - 스티브 잡스, Bloomberg, May 1998.

It's really hard to design products by focus group. A lot of times, people don't know what they want until you show it to them. - 스티브 잡스, Forbes, Oct 2011.

포커스 그룹 인터뷰(Focus Group Interview, FGI)는 정책 설계와 서비스 기획에서 널리 활용되는 질적 연구 방법입니다. 소수의 참여자를 모아 특정 주제에 대해 자유롭게 토론하게 함으로써, 설문조사로는 포착하기 어려운 맥락과 인식을 드러내는 데 강점이 있습니다.

그러나 최근 여러 현장에서 FGI 결과가 과도하게 일반화되거나 정책 판단의 근거처럼 사용되는 사례를 보며, 이 방법론이 가진 구조적 한계를 다시 짚어볼 필요성을 느낍니다. FGI는 유용한 도구이지만, 잘못 쓰이면 오히려 의사결정을 왜곡할 수 있습니다.


1. 작은 표본이 만드는 큰 착시

FGI의 가장 근본적인 한계는 표본 규모입니다. 일반적으로 한 그룹은 6~10명 수준에 불과합니다. 이는 심층 의견 탐색에는 적절할 수 있지만, 그 자체로 어떤 집단의 ‘대표 의견’을 말해 주지는 못합니다.

문제는 이 결과가 때때로 다음과 같이 해석된다는 점입니다.

  • “현장에서는 이렇게 생각한다”
  • “사용자들은 이것을 원한다”
  • “전문가 의견이 모였다”

이러한 표현은 FGI의 본래 용도를 넘어서는 일반화입니다. 소수의 목소리가 전체의 경향처럼 포장될 때, 정책과 서비스는 잘못된 방향으로 흘러갈 위험이 있습니다.


2. 그룹 다이내믹이 만들어내는 편향

FGI는 ‘집단 토론’이라는 형식 자체에서 여러 편향을 내포합니다.

  • 목소리 큰 참여자의 의견이 토론을 지배
  • 분위기에 따른 동조 압력(groupthink)
  • 사회적으로 바람직한 답변으로의 수렴
  • 진행자의 질문 방식에 따른 유도 효과

특히 기술이나 정책처럼 정답이 불분명한 주제에서는, 토론 분위기 하나로 의견 분포가 크게 달라질 수 있습니다. 이 경우 우리가 관찰하는 것은 ‘개별 참여자의 진짜 생각’이 아니라, 토론 상황에서 형성된 집단 산물일 가능성이 큽니다.


3. 탐색 도구가 결정 도구로 오용되는 문제

FGI의 본래 목적은 결론을 확정하는 것이 아니라, 다음과 같은 탐색적 역할입니다.

  • 가설 생성
  • 문제 맥락 이해
  • 사용자 언어와 인식 파악
  • 추가 연구 설계의 단서 확보

그럼에도 불구하고 실무에서는 FGI 결과가 정책 방향을 정당화하는 근거처럼 사용되는 경우가 적지 않습니다. 질적 탐색 도구가 정량적 근거를 대체하는 순간, 의사결정의 기반은 급격히 취약해집니다.


4. ‘기대 담론’이 증폭되기 쉬운 구조

최근 AI, 데이터, 디지털 전환과 같은 분야에서는 긍정적 기대가 사회 전반에 강하게 형성되어 있습니다. 이런 환경에서 진행되는 FGI는 자연스럽게 다음과 같은 경향을 보이기 쉽습니다.

  • 기술 낙관론의 과대표출
  • 위험 요소의 과소 언급
  • 실행 비용과 현실 제약의 축소

참여자들은 종종 “바람직한 미래”를 말하지, “실제로 작동 가능한 현실”을 말하지 않습니다. 이 차이를 구분하지 못하면, 정책은 구호는 크고 실행은 어려운 방향으로 흘러갈 수 있습니다.


5. 책임의 비대칭성

FGI에서 나온 아이디어나 방향이 실제 정책이나 시스템으로 구현될 경우, 흥미로운 비대칭이 발생합니다.

  • 의견 제시는 소수 참여자가 수행
  • 실행과 책임은 기관과 현장이 부담

특히 데이터 거버넌스나 AI 활용처럼 법적·윤리적 부담이 큰 영역에서는, 초기 아이디어의 파급력을 과소평가하기 쉽습니다. 즉흥적 제안 하나가 실제 운영 단계에서 상당한 비용과 위험을 동반할 수 있습니다.


균형 잡힌 활용을 위하여

FGI는 여전히 가치 있는 도구입니다. 사용자 맥락을 이해하고, 문제의 언어를 포착하며, 새로운 가설을 만드는 데 있어 강력한 방법임은 분명합니다.

다만 다음 원칙은 반드시 지켜질 필요가 있습니다.

  • FGI 결과를 일반화하지 말 것
  • 정량 자료와 교차 검증할 것
  • 정책 결정의 단독 근거로 사용하지 말 것
  • 참여자 구성과 토론 맥락을 투명하게 공개할 것

FGI는 방향을 비추는 손전등이지, 결론을 확정하는 판결문이 아닙니다. 이 기본을 잊지 않을 때, 우리는 이 유용한 도구를 과신도 과소평가도 하지 않는 균형 잡힌 위치에서 활용할 수 있을 것입니다.

이 글은 현장 경험을 바탕으로 FGI 방법론의 한계를 성찰하기 위해 작성되었습니다.


이 글은 정해영의 아이디어를 바탕으로 AI(ChatGPT)의 도움을 받아 작성되었습니다. CC0 1.0으로 자유 이용 가능하며, 정확성 및 법적 책임은 보장하지 않습니다. This text was written with AI (ChatGPT) assistance based on the author’s ideas. Released under CC0 1.0. No guarantee of accuracy is provided, and no liability is assumed.

바꿀 수 있는 것과 없는 것 - 고장난 전원 스위치 하나가 남긴 질문

43 오극관 싱글 앰프의 망가진 전원 스위치

내 사무실에는 내가 직접 만든 오디오 앰프가 몇 대 있다. 그중 진공관으로 자작한 것이 두 대쯤 되는데, 사무실에 처음 찾아온 사람과 가볍게 이야기를 풀어나갈 때 꽤 좋은 소재가 되곤 했다. 나의 오디오 앰프 자작 기록은 공식 웹사이트의 위키 문서에 따로 정리해 두었다.

그 가운데 하나가 1940년대에 만들어진 항아리 형태의 43 오극관을 사용한 소출력 앰프였다(자작 기록). 상판은 CAD를 이용해 설계한 뒤 도면을 가공업체에 보내서 만들었고, 바닥의 나무틀도 손수 도색하여 나름대로 정성을 들여 만든 기기였다. 진광관 자체가 고전적인 항아리 모습이라 개인적으로도 꽤 애착을 갖고 있던 진공관 앰프였다. 처음에는 빵 굽는 트레이 위에 프로타입을 만드는 것으로 시작하였었다(링크). 프로토타입 제작 당시에는 R-core에 직접 감은 출력트랜스포머를 여기에 처음 연결해서 사용했었다.

그런데 어제 전원 스위치가 들어오지 않는 것을 발견했다. 조명이 들어오는 시소형 전원 스위치였는데, 아무리 움직여도 ON 포지션에 고정이 되지 않았고 불도 들어오지 않았다. 수동 부품의 내구성에 대해 나름 높은 기준을 갖고 있던 나로서는 꽤 실망스러운 상황이었다. 일단 인터M R150 PLUS '레퍼런스' 앰프로 바꾸어 놓았다. 이는 대출력 앰프라서 사무실 책상 위에서 쓰기에는 적합한 수준은 아니다.

R150 PLUS 파워앰프. 책상 위에 놓고 잔잔하게 음악을 듣기에는 너무 출력이 커서 레벨 조정용으로 베링거 믹서를 달아 놓았다.

참고로 말하자면 내가 블로그에 쓴 글 중에서 램프형 스위치의 올바른 결선법(2016.5.29.)은 항상 가장 많은 조회수를 보이고 있다.

물론 전원 스위치 수리는 어려운 일이 아니다. 상판 타공 사이즈에 맞는 부속을 다시 구해 납땜만 하면 되는 문제이기 떄문이다. 전기/전자제품이란 당연히 수명이라는 것이 있다. 특히 자작 기기인 경우 스위치나 소켓, 커넥터와 같은 수동/기구 부품은 값이 좀 비싸더라도 좋은 것을 써야 후회가 없는데 제작 당시에는 좋은 결심을 하지 못하는 것이 늘 문제이다.

이 일 자체는 매우 사소한 일이었다. 부품을 구해서 바꾸어 끼고 납땜을 새로 하면 해결될 문제였다.

하지만 최근 2년 동안 내가 직장에서 맞닥뜨린 몇 가지 일들은 그렇게 간단하지 않았다.

어디선가 읽은 글이 떠올랐다. 어떤 이는 자신의 성공 비결이 아무리 노력해도 바꿀 수 있는 것과 바꿀 수 없는 것을 빨리 구별하는 데 있다고 말했다. 즉, 바꿀 수 없는 일을 개선하려고 쓸데없는 에너지를 낭비하지 말자는 뜻이었다.

물론 아주 작고 조금만 조절하면 될 일까지도 바꿀 수 없다고 성급히 판단해버리는 것은, 자신의 능력과 가능성을 스스로 잠재워버리는 게으른 핑계가 될 수도 있겠다고 생각했다.

그럼에도 불구하고 아무리 노력해도 바꿀 수 없는 것들이 우리 주변에 너무 많다는 사실도 부정할 수 없었다. 그것은 내 안에도 있었고, 특히 외부 환경에도 많이 존재했다. 그럴 때마다 적지 않은 무력감을 느꼈다.

지난 2년 동안 나는 그런 경험을 유난히 많이 했다. 그리고 아직 뚜렷하게 나아질 기미도 보이지 않았다.

무엇을 바꿀 수 있을 것인가? 차라리 나 자신을 바꾸는 것이 가장 올바른 길일까?

망가진 오디오 앰프의 파워 스위치를 바라보며, 그런 생각을 하게 된 아침이었다.

GitHub를 위한 파일과 폴더명 쓰는 방법

REAME.md? ReadMe.md? read_me.md? read-me.md?

프로그래밍 세계에서는 변수명의 대소문자(case)를 표기하는 여러 관행이 존재한다. 이에 대해서는 워낙 많은 글이 널려 있으니 여기서 구태여 반복할 필요는 없겠다. 가장 중요한 공통점은 중간에 공백이 들어가면 안된다는 것 정도일 것이다. 참고로 '_'를 흔히 국내에서 under bar라고 부르는데, 영어권의 공식 명칭은 underscore이다.

대표 표기 스타일

스타일 예시 특징 / 용도
camelCase fileName 첫 단어는 소문자, 이후 단어의 첫 글자만 대문자
PascalCase FileName 모든 단어의 첫 글자를 대문자(클래스/타입 이름에 자주 사용)
snake_case file_name 단어 사이를 언더스코어(_)로 구분(Python에서 흔함)
kebab-case file-name 단어 사이를 하이픈(-)으로 구분(URL/HTML/CSS에서 흔함)
SCREAMING_SNAKE_CASE FILE_NAME 대문자 + 언더스코어(상수/환경변수에 자주 사용)

낙타의 혹(camelCase), 땅바닥에 붙어서 기어가는 뱀(snake_case), 꼬챙이에 각종 재료의 가운데를 꽂아서 만든 케밥(kebap-case)을 떠올리면 왜 이런 스타일 이름이 붙는지 쉽게 이해할 수 있을 것이다.

이 스타일을 파일명에나 폴더(디렉토리)명에 그대로 적용해도 별 상관은 없다. 그러나 소스 파일에 따라서는 약간 다른 관례가 존재한다. 예를 들어 웹에서는 kebab-case를 많이 쓰고, 파이썬에서는 sanke_case(.py)가 거의 표준이다. OS에 따라서는 대소문자를 구분하기도 하고 그렇지 않기도 한다. GitHub는 리눅스나 윈도우 양측에서 접근하는 경우가 많으니 파일과 디렉토리 이름에 신경을 좀 써야 한다. 강제되는 규칙이 있는 것은 아니지만 대체로 다음을 권장한다.

  • 디렉토리 이름: kebab-case(1순위), snake_case(2순위)
  • 파일 이름: snake_case(C/C++/Arduino에서 강력 권장), kebab-case 또는 camelCase(JavaScript/웹)

AudioDriver.cpp와 audiodriver.cpp는 리눅스에서는 다른 파일이지만 윈도우에서는 같은 것으로 인식한다. 따라서 파일명은 전부 소문자를 쓰는 것을 강력히 권장한다. Windows의 국문 표준 표기는 '윈도'이다. 그러나 이렇게 쓰려면 영 어색하다...

파이썬 파일명은 특별히 주의해야 한다. 다른 스크립트에서 모듈로 임포트할 소스에는 하이픈('-')을 사용할 경우 에러를 낸다. 예를 들여 midi-parser.py라는 파일이 별도로 존재하는데 이를 다른 스크립트에서 'import midi-parser'로 읽어들이면 에러가 발생한다. 따라서 import_parser.py로 이름을 붙여야 한다. 

그러면 GitHub에서 문서 파일명은 어떻게 해야 하는가? 코드 파일과는 또 약간 다른 관례가 있다고 한다. 가장 흔한 것은 kebab-case이고 그 다음으로는 snake_case이다. camelCase나 PascalCase는 권장하지 않는다. 그러나 매우 중요한 예외가 있으니 그것은 바로 루트에 존재하는 표준 문서다. README.md, LICENSE, CHANGELOG.md 등은 전부 대문자로 고정한다.

정리해 보자. 이 글을 작성하는데 ChatGPT의 도움을 많이 받았다.


🎯 선택 가이드

  • 📚 문서 공개/오픈소스 강조 → kebab-case

  • 🧩 임베디드/툴체인 중심 → snake_case

  • 👤 개인 프로젝트 → 일관성만 유지하면 OK


🔷 한 줄 결론

GitHub 문서 파일명:

  • 일반 문서 → kebab-case ⭐ 또는 snake_case

  • 루트 표준 문서 → README.md 등 대문자 고정

  • camelCase / PascalCase → 문서에는 비권장


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을 분석하여 연도별로 폴더를 나누어서 재분류하는 기능을 한다. 최종 결과물은 원드라이브에 옮겨 영구 보관하는 것이 목표이다. 입력 zip 파일은 반드시 Google Takeout으로 받은 것이 아니어도 작동한다. 즉, 컴퓨터에 보관된 미디어 파일을 zip으로 묶은 뒤 투입하면 하위 폴더 구조와 무관하게 잘 작동함을 확인하였다.

하나의 미디어 파일에 대해 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 개인 아카이브 구축의 전초 단계

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

요약

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


2026년 2월 20일 금요일

구글 포토에서 원드라이브로: PowerShell 스크립트를 이용한 개인 사진 아카이브 재정비

구글 드라이브가 점점 파일로 채워져서 용량을 업그레이드하라는 경고를 자주 받게 되었다. 가장 많은 용량을 차지하는 것은 구글 '포토'이다. 마이크로소프트 오피스 365 구독을 통해 소유하고 있는 1TB의 공간에 이를 옮기기로 결정하고 실제 실행에 옮긴 기록을 남기고자 한다. 이 파이프라인의 핵심은 구글 테이크아웃과 Windows PowerShell 스크립트이다. ChatGPT의 도움으로 개발한 이 스크립트의 특징은 글 마지막 부분에 소개하였다.

구글 테이크아웃은 구글 서비스에 등록된 모든 자료를 2GB 단위의 zip 파일로 분할하여 다운로드 링크를 제공한다. 나의 경우에는 총 93개의 파일이 생성되었다. 이 파일의 이름(001, 002...)은 연도순으로 붙여지는 것이 아님에 유의하자.

Zip 파일을 모두 다운로드하여 PC에서 압축을 해제한 뒤 그냥 원드라이브에 밀어 넣으면 되는가? 안타깝게도 구글 포토와 같이 촬영일자에 따른 미디어 정리라든가 앨범 정보 같은 것이 그대로 따라가지는 않는다. 따라서 PC에서 zip 파일을 받은 뒤 내부에 포함된 JSON 파일을 이용하여 각 미디어 파일을 연도별 폴더로 나누어 넣는 일이 핵심이다. 이 과정에서는 PowerShell 스크립트인 takeout_batch_year.ps1가 매우 중요한 역할을 하였다(개인 리포지토리로 이용하는 DokuWiki에서는 업로드할 수 있는 파일의 확장자를 제한하고 있기 때문에 zip으로 압축하여 올렸음). 이 스크립트는 여러 차례에 걸쳐 수정하면서 기능을 개선하였고 안정적인 동작을 하는 것을 최종적으로 확인하였다.... 아, 나중에 발견하였지만 꽤 심각한 문제점이 있었다. 이 글의 끝부분을 참조하기 바란다.

스크립트 작성에서는 로컬 PC에 저장 공간이 별로 많이 남지 않았다는 것을 고려하여 작동하도록 많은 신경을 썼다. 내 노트북 컴퓨터에서는 여유 공간이 별로 없어서 180GB가 넘는 총 93개의 zip 파일을 한꺼번에 다운로드하여 작업하는 것이 근본적으로 불가능하였다. 컴퓨터에 남은 여유 저장 공간을 점검하면서 일정 개수의 zip 파일을 다운로드하여 처리한 뒤 원드라이브 업로드 후에는 지워서 공간을 확보하고, 다음 차례의 zip 파일을 처리하는 방식으로 진행하는 것이 기본 아이디어였다.

그러나 ChatGPT와 더불어 개발 작업을 계속하면서 스크립트의 성능이 점점 좋아졌다. -ZipDir로 지정한 디렉토리에 다운로드한 파일을 계속 밀어 넣으면 스크립트는 자동적으로 -BatchSize 단위로 처리를 한 뒤 작업이 끝난 zip 파일을 다른 위치(-DestRoot 하위의 _processed 폴더)로 옮긴다. 반복문을 돌릴 필요도 없으며, 다음 배치 작업 전에 자동적으로 저장공간이 얼마나 남아 있는지 확인하여 안전한 수준이 아니면(-MinFreeGB로 지정한 값보다 적은 경우) 자동으로 작업을 중단한다. 사용자는 작업이 끝나서 다른 위치로 옮겨진 zip 파일을 이따금씩 지우면 된다.

스크립트는 Downloads 폴더에 두고 여기에서 PowerShell 창을 열어서 다음과 같이 명령어를 실행하면 된다. 첫 줄 명령어는 지금 열려 있는 PowerShell에서 .ps1 스크립트를 실행하기 위해 정책을 변경하는 명령으로서 현재 열린 창에서만 유효하다. 반복된 테스트에 의하면 -BatchSize는 4~8 정도가 적당한 것 같다. 빠른 압축 해제를 위해서 반드시 7-Zip이 필요하다. 이 유틸리티를 설치한 뒤 실행파일인 7z.exe의 위치를 Path 환경변수에 지정해야 한다.

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass

.\takeout_batch_year.ps1 `
  -ZipDir "C:\Users\jeong\Downloads\Takeout_Zip" `
  -DestRoot "C:\Users\jeong\Downloads\Photos_Backup\From_Google_Takeout" `
  -BatchSize 4 `
  -MinFreeGB 25

takeout_batch_year.ps1은 모든 미디어 파일에 대한 해시를 생성하여 한번 처리한 파일은 다시 작업하지 않는다. 따라서 작업 결과로 만들어진 _hashes_sha256.txt를 절대로 지워서는 안 된다. 1년이 지난 뒤 다시 구글 포토로부터 테이크아웃을 할 때, 증분 백업 같은 것은 지원하지 않는다. 지워지지 않고 남은 파일에 대해서 언제나 전체 백업을 할 뿐이다. 따라서 이 해시 파일은 유일한 작업 기록인 셈이니 이것 역시 작업 종류 후에는 원드라이브에 보관하는 것을 권장한다. zip 파일을 가져다가 똑같은 스크립트를 돌리면 압축 해제는 어쩔 수 없이 다시 돌겠지만, 해시에 이미 기록된 파일은 추가 작업을 하지 않는다.

그러므로 이번 이전 작업이 성공적으로 끝난 뒤에는 구글 포토에는 최근 5년 정도의 파일만 남겨 두는 것이 좋을 것이다. 

한 배치에 대한 작업이 끝난 뒤 Photos_Backup/From_Google_Takeout 아래에는 다음과 같이 연도별로 정리된 폴더가 생기고 미디어 파일은 그 아래에 분류된다. JSON 파일의 분석에 실패하면 자동으로 가장 최근 연도인 2026 폴더로 들어간다. 

스크립트 실행 후 연도별 폴더에 미디어 파일이 자동으로 분류되어 들어간다. 해시 파일과 문제점이 있는 파일의 기록이 여기에 남는다.

너무나 당연한 이야기지만 -ZipDir이나 DestRoot에 원드라이브를 지정해서는 안 된다. 스크립트 작업이 다 끝나면 C:\Users\jeong\Downloads\Photos_Backup\From_Google_Takeout은 폴더 그대로 원드라이브의 '사진' 폴더 하위로 보내면 된다. 

원드라이브는 구글 포토와 같은 수준의 앨범 작성이나 편집 기능 같은 것은 없다. 휴대폰에서 직접 원드라이브로 사진을 백업하게 만들면 휴대폰에 남아 있는 최신 사진의 경우 구글 포토와 비슷하게 관리할 수는 있다. 이는 PC에서는 원드라이브 -> 사진 -> Camera Roll에서 접근 가능하다. 오늘 글에서 설명한 아카이브 재정비 작업과는 별개이다. 

PowerShell 기반 takeout_batch_years.ps1 사진 아카이브 자동화 파이프라인의 주요 특징

  • Google Takeout ZIP을 배치 처리
    • ZipDir에 있는 *.zip 전체를 정렬해 순차 처리
    • -BatchSize(기본 4)개씩 묶어서 반복 실행(자동으로 다음 배치로 넘어감)
  • 배치별 임시 작업 폴더로 “누적 스캔” 방지
    • ZipDir\_work\<batchId>\... 형태로 배치 전용 작업 폴더 생성
    • 스캔 대상은 해당 배치 폴더만(이전 실행 잔여물로 미디어 수가 튀는 문제 방지)
    • -KeepWork 옵션이 없으면 배치 종료 후 작업 폴더 자동 삭제
  • 7-Zip 우선 사용 + 미설치 시 Expand-Archive 폴백
    • 7z.exe를 PATH에서 찾으면 7-Zip으로 빠르게 추출
    • 없으면 PowerShell Expand-Archive로 추출(상대적으로 느림)
  • 미디어 확장자만 선별 스캔
    • JPG/JPEG/PNG/GIF/WEBP/HEIC, MP4/MOV 등(스크립트의 $MediaExt 목록 기준)
  • 연도 폴더 분류 로직(우선순위 명확)
    • 1순위: Takeout JSON의 photoTakenTime.timestamp 또는 creationTime.timestamp
    • 2순위: (JPG/JPEG 한정) EXIF DateTimeOriginal(0x9003) → 없으면 DateTime(0x0132)
    • 3순위: 최후 수단으로 파일 LastWriteTime.Year 사용
  • JSON 매칭 성능 최적화
    • 폴더 단위로 JSON을 한 번만 읽어 dir → (basename→year) 캐시($JsonYearCache) 생성
    • 같은 폴더 내 파일은 캐시로 빠르게 연도 조회
  • 중복 제거의 핵심: 파일 내용 기반 SHA-256 해시
    • Get-FileHash SHA256로 파일 내용을 해싱
    • _hashes_sha256.txt에 누적 저장하여 다음 배치/다음 실행에서도 중복 스킵
    • 파일명/폴더가 달라도 내용이 같으면 중복으로 제거됨
  • 해시 DB 기록 안정화(버퍼링 + 재시도)
    • 해시를 메모리 버퍼($HashBuffer)에 모았다가 배치 끝에 한 번에 기록
    • Add-Content 실패 시 슬립을 두고 재시도(AppendTextWithRetry)
  • 파일 이동 실패에 대한 복구 로직
    • Move-Item 실패 시 재시도(MoveWithRetry)
    • 그래도 실패하면 Copy-Item 재시도 후 원본 삭제(가능한 경우)
  • 동일 파일명 충돌 방지
    • 대상 경로에 같은 이름이 있으면 __1, __2…를 붙여 저장(덮어쓰기 방지)
  • 문제 파일은 멈추지 않고 건너뛰며 기록
    • 해시 계산 실패/해시 null/이동·복사 실패 등의 경우 처리 중단 없이 스킵
    • _bad_files.txt에 유형과 경로(및 일부 오류 메시지) 기록
  • 디스크 여유공간 가드
    • -MinFreeGB(기본 25GB) 미만이면 배치를 진행하지 않음(안전 정지)
    • 배치 크기가 큰 경우 8→6→4로 자동 축소 시도 로직 포함
  • 처리 완료 ZIP 관리
    • 기본: 처리한 ZIP을 ZipDir\_processed로 이동(재처리 방지)
    • 옵션: -DeleteZips 지정 시 처리한 ZIP을 삭제(주의 필요)
  • 운영 로그가 명확
    • 배치 시작/추출/스캔된 미디어 개수/누적 해시 수/여유공간/ZIP 이동(또는 삭제) 등 타임스탬프 로그 출력
    • -VerboseLog 켜면 중복 스킵 등 상세 로그도 출력

이 스크립트는 아직 개선할 점이 남아 있다. 중복을 제거한 미디어 파일에 대해서 원본의 JSON 파일도 같이 가져와야 하지만, 현재는 미디어 파일만 최종 결과물로 남기게 되어 있기 때문이다. 고난의 시작! 더 큰 문제는 JSON이나 EXIF 정보가 있는 원본 미디어 파일임에도 불구하고 촬영일 정보를 제대로 추출하지 못하여 2026 fallback으로 분류되는 것이 상당히 많았다는 점이다. 지금은 이에 대한 개선 작업을 진행하고 있다. 결과는 다음번 글에 쓰도록 하겠다.