출시된 앱의 소스가 git에 없었다: 1인 개발 서버의 백업 구멍 전수 점검기

앱 스토어에서 실제 사용자에게 배포 중인 앱이 있습니다. 매일 크론이 실행하는 운영 스크립트도 있습니다. 그런데 어느 날 확인해 보니, 그중 상당수가 버전 관리 시스템(git) 바깥에 있었습니다. 서버 디스크가 죽으면 그대로 소실되는 상태였다는 뜻입니다. 이 글은 그 사실을 발견하게 된 경위, 왜 "자동 커밋이 있으니 괜찮겠지"라는 가정이 틀렸는지, 그리고 하루 만에 전 프로젝트를 점검해 백업 체계에 넣은 과정의 기록입니다.

1. 발견: 커밋하려다 알게 된 것

발단은 평범했습니다. 한 주 동안 금융 분석 시스템에 기능을 여럿 추가했고, 마무리로 "변경사항 커밋해서 백업해 두자"는 단순한 작업이었습니다. 그런데 git status를 열어 보니 이상했습니다. 이번 주에 만든 파일들이 전부 untracked(미추적) 상태로 쌓여 있었던 겁니다. 그것도 36개.

이 서버에는 작업이 끝날 때마다 자동으로 WIP 커밋을 남기는 훅이 걸려 있습니다. 그래서 "커밋은 알아서 되고 있다"고 믿고 있었습니다. 실제로 커밋 로그도 매일 쌓이고 있었으니까요. 문제는 그 자동 커밋이 수정된(modified) 파일만 담고, 새로 만든(untracked) 파일은 건드리지 않는다는 점이었습니다.

# 자동 커밋 로그는 멀쩡해 보였다
WIP(analyzers): 분석 모듈 개선
WIP(targets): 브리핑 갱신
...

# 하지만 git status에는
?? analyzers/새기능_A.py      ← 운영 크론이 매일 실행 중
?? analyzers/새기능_B.py      ← 대시보드가 import 중
?? tests/ (16개 파일)
?? docs/plans/ (7개 문서)

즉, 몇 주 동안 만든 신규 모듈 전부가 — 매일 크론으로 돌아가는 운영 코드까지 포함해서 — 로컬 디스크에만 존재했습니다. 커밋 기록이 쌓이는 걸 보며 안심하고 있었을 뿐, 정작 가장 중요한 새 코드는 한 번도 저장소에 들어간 적이 없었던 겁니다.

2. 전수 점검: 구멍은 하나가 아니었다

하나가 이 모양이면 다른 프로젝트도 봐야 합니다. 서버의 프로젝트 전부를 점검했고, 결과는 세 단계로 나뉘었습니다.

2-1. 신규 파일이 미추적인 저장소

금융 분석 시스템 외에 AI 비서 프로젝트에서도 같은 패턴이 나왔습니다. 특히 뼈아픈 건 매일 크론이 실행하는 운영 스크립트 4종이 미추적이었다는 점입니다. 신호 수집기, 시장 가드, 에러 메시지 사전 같은 것들 — 서버가 날아가면 크론은 남고 스크립트만 사라져서, 복구 때 "이 크론이 뭘 실행했더라?"부터 시작해야 했을 파일들입니다.

2-2. 원격 저장소가 없는 저장소

한 앱 프로젝트는 git 관리는 잘 되고 있었지만 원격(remote)이 없는 로컬 전용이었습니다. 커밋 히스토리가 아무리 깔끔해도 디스크와 함께 사라지는 히스토리입니다.

2-3. git 자체가 없는 프로젝트

가장 심한 케이스입니다. 앱 스토어 내부 테스트 트랙으로 실제 배포 중인 안드로이드 앱 두 개가 git 저장소조차 아니었습니다. 버전 코드를 14까지 올려 가며 배포한 앱인데, 소스는 서버 디렉토리에만 존재했습니다. 빌드 산출물(AAB)은 스토어에 올라가 있으니 "앱은 살아있다"는 착시가 생기지만, 소스 히스토리는 0벌이었던 셈입니다.

3. 해결: 반나절의 정리 작업

정리 자체는 단순 작업입니다. 순서가 중요했습니다.

3-1. 시크릿부터 확인한다

급하게 git add -A부터 하면 사고가 납니다. 안드로이드 프로젝트에는 서명 키스토어(.jks), 키 비밀번호(keystore.properties), 그리고 스토어 자동 업로드용 서비스 계정 키(JSON)가 디렉토리 안에 같이 살고 있었습니다. 커밋 전에 .gitignore를 먼저 만들고, 스테이징된 파일 목록에서 시크릿 패턴을 검색해 0건임을 확인한 뒤에 커밋했습니다.

# 커밋 전 검증 — 스테이징 목록에 시크릿이 없어야 한다
git diff --cached --name-only | grep -iE "jks|keystore|service-account|local.properties"
# (출력 없음 = 안전)

3-2. 프라이빗 원격 저장소 생성·연결

세 프로젝트에 프라이빗 저장소를 만들어 전체 히스토리를 푸시했습니다. 한 가지 함정: CLI로 저장소를 만들면 리모트가 SSH 주소로 붙는 경우가 있는데, 서버에 SSH 키 대신 토큰 인증만 설정돼 있으면 첫 푸시가 실패합니다. HTTPS 주소로 바꾸고 자격 증명 헬퍼를 연결하면 해결됩니다.

3-3. 커밋 메시지에 "왜"를 남긴다

이번 일괄 커밋의 메시지에는 "자동 WIP 커밋이 신규 파일을 놓쳐 미추적이던 운영 코드를 추적 시작"이라고 경위를 적었습니다. 몇 달 뒤의 내가 "왜 이 시점에 파일 36개가 한꺼번에 들어왔지?"라고 물을 것이기 때문입니다.

4. 교훈

4-1. 자동화는 자기가 안 하는 일을 말해주지 않는다

자동 커밋 훅은 거짓말을 하지 않았습니다. 수정 파일을 커밋하겠다고 했고 그 일을 정확히 했습니다. 문제는 제가 "커밋이 되고 있다"를 "전부 커밋되고 있다"로 확대 해석한 것입니다. 자동화가 있는 곳일수록 그 자동화의 경계 밖을 주기적으로 점검해야 합니다. 이번 경우 점검 명령은 git status 한 줄이었습니다. 몇 주 동안 아무도 그 한 줄을 실행하지 않았을 뿐입니다.

4-2. "배포됐다"와 "백업됐다"는 완전히 다른 문장이다

스토어에 앱이 올라가 있다는 사실이 주는 안도감이 소스 백업의 부재를 가렸습니다. 산출물의 존재는 소스의 안전과 아무 관련이 없습니다.

4-3. 시크릿 옆에서 일괄 커밋하지 않는다

백업 구멍을 막으려다 키 유출 사고를 만들면 더 나쁜 거래입니다. "gitignore 먼저, 스테이징 스캔으로 검증, 그다음 커밋"의 순서는 몇 분이면 되고, 반대 순서의 사고는 키 폐기·재발급까지 며칠이 듭니다.

4-4. 점검은 겸사겸사 할 때 이루어진다

이 구멍은 백업 감사를 하겠다고 마음먹어서 발견된 게 아니라, 평범한 "커밋해 줘" 작업의 부산물로 발견됐습니다. 지난번 자동 발행 파이프라인의 4일 침묵 장애도 API 키 정리 중에 우연히 발견됐습니다. 정기 감사를 계획하는 것도 좋지만, 일상 작업에서 스치는 위화감을 그냥 넘기지 않는 것이 1인 운영에서는 더 실용적인 감사 체계였습니다.

FAQ

Q. 자동 커밋이 untracked 파일도 담게 하면 되지 않나요?

가능하지만 권하지 않습니다. 신규 파일에는 시크릿·대용량 산출물·임시 파일이 섞여 들어오기 쉽고, 자동으로 add -A를 하는 순간 위 3번 교훈의 사고 경로가 열립니다. 신규 파일 추적은 사람이 gitignore를 확인하며 결정하는 편이 안전하고, 대신 "미추적 파일 수가 N개를 넘으면 알림" 정도의 감시가 현실적인 절충입니다.

Q. 1인 프로젝트인데 프라이빗 저장소까지 필요한가요?

백업 관점에서는 필수에 가깝습니다. 협업 기능 때문이 아니라, "서버와 물리적으로 분리된 곳에 히스토리 사본이 있는가"의 문제입니다. 클라우드 서버는 생각보다 쉽게 사라집니다 — 요금 문제, 리전 장애, 실수로 인한 인스턴스 삭제까지.

Q. 어디까지 커밋하고 어디부터 제외하나요?

이번에 적용한 기준: 운영 코드·테스트·설계 문서·크론 스크립트는 전부 추적. 런타임 데이터베이스 파일, 로그, 사용자 대화 기록, 스크린샷, 서명 키·자격 증명은 전부 제외. 특히 대화 기록류는 "코드가 아니라 데이터"라서 저장소에 넣지 않는 것을 원칙으로 삼았습니다.