셀프호스팅 앱은 왜 “가볍게” 만들기가 어려운가 — 진단편

SELF-HOSTING 운영기 · 1/3

🔍 셀프호스팅 앱은 왜 “가볍게” 만들기가 어려운가 — 진단편

529MB까지 부풀어버린 Docker 학습 앱. “필요 없는 파일 다 지우자”는 단순한 요청이 어떻게 구조 진단으로 이어졌는가. 추측 대신 명령어로 증거를 모으는 과정 전체 기록.

#셀프호스팅 #Docker #Flask #기술부채 #SQLite #운영

📋 이 글에서 다루는 것

  • 1. 상황 — 패치 스크립트 14개가 쌓인 디렉토리
  • 2. 첫 번째 원칙 — “마운트되지 않은 파일은 런타임에 안 읽힌다”
  • 3. 용량의 진범 찾기 — du로 드러난 75%의 정체
  • 4. 삭제해도 되는가? — MARK 마커 대조 검증법
  • 5. 진단 결론 — 표면 증상 vs 진짜 문제
셀프호스팅으로 굴리던 개인 학습용 Flask 앱이 있다. 기능을 하나씩 붙이다 보니 디렉토리에 패치 스크립트 14개, 백업 파일 5개, 백업 디렉토리 3개가 쌓였다. “실제로 쓰는 파일만 남기고 다 지워서 가볍게 만들고 싶다”가 출발점이었다. 그런데 막상 지우려고 보니 — 뭘 지워도 안전한지 어떻게 확신하는가? 이 글은 그 확신을 추측이 아니라 명령어 출력으로 만들어가는 과정이다.

1. 상황 — 무엇이 쌓여 있었나

앱은 Docker Compose로 도는 단일 Flask 컨테이너다. 기능 추가를 patch_*.py 스크립트 방식으로 누적해 왔다. 각 패치는 원본 파일에서 특정 문자열을 찾아 교체하는 방식이고, 적용 후에도 스크립트가 디렉토리에 그대로 남았다. 그 결과:

디렉토리 현황
app.py 본체 + 14개 패치 + 5개 .bak 백업 + 3개 데이터 백업 디렉토리
app.py
app.py.bak-p12 / .bak-p14 / .bak-p15 / .bak-p16   # 백업 4종
patch_again_weak_v1_1.py
patch_keyword_ticker_v2.py
patch_tags_v1.py ... (총 14개)
backups/                    # 데이터 백업 1
data-backup-bc-20260514/    # 데이터 백업 2
data-backup-bc-20260520/    # 데이터 백업 3

“지저분하다”는 직관은 있지만, 그게 “무겁다”와 같은 뜻인지, 그리고 무엇을 지워야 하는지는 아직 모른다. 여기서 첫 번째 원칙을 세운다.

2. 첫 번째 원칙 — 런타임이 읽는 파일은 정해져 있다

Docker 환경에서 “이 파일이 실제로 쓰이는가?”의 답은 추측할 필요가 없다. docker-compose.yml의 volumes에 명시된 것만 컨테이너가 읽는다. 거기 마운트되지 않은 파일은 런타임에 단 한 줄도 실행되지 않는다.

실제 마운트 확인 (compose 파일 말고 실행 중인 컨테이너 기준)
compose 파일을 수정하고 재시작을 안 했으면 어긋날 수 있다. 실제 컨테이너에 물어본다.
docker inspect <container_name> \
  --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
출력 결과
/opt/my-flashcard/data      -> /app/data
/opt/my-flashcard/app.py    -> /app/app.py
/opt/my-flashcard/templates -> /app/templates

여기에 빌드용 파일(Dockerfile, docker-compose.yml, requirements.txt)을 더하면 실사용 파일은 6종뿐이다. 패치 스크립트 14개는 이 목록에 없다 — 즉 런타임 무관이다.

💡 Dockerfile의 COPY . . 함정

compose volumes에는 없어도, Dockerfile에 COPY . .가 있으면 빌드 시점엔 디렉토리 전체가 이미지에 복사된다. 즉 패치 스크립트들이 빌드 때마다 이미지 안으로 따라 들어간다. 정리하면 다음 docker compose build 때 이미지도 같이 가벼워지는 부수 효과가 있다.

3. 용량의 진범 — du가 드러낸 75%

“파일이 많다”와 “디스크를 많이 먹는다”는 다른 문제다. 둘을 구분하지 않으면 엉뚱한 걸 지운다. du로 항목별 용량을 큰 순서대로 본다.

항목별 용량 (큰 것부터)
du -sh /opt/my-flashcard/* | sort -rh
항목용량성격
backups/133M데이터 백업 (DB+이미지)
data-backup-bc-20260520132M데이터 백업
data-backup-bc-20260514132M데이터 백업
data/ (실사용)132M삭제 금지
app.py.bak ×4~236K소스 백업
patch_*.py ×14~300K적용 완료 스크립트
📊 진단 결과: 전체 529M 중 397M(75%)가 백업 디렉토리 3개

패치 스크립트 14개를 전부 합쳐도 약 0.3MB다. 이걸 다 지워도 용량은 거의 안 줄어든다. “디렉토리가 지저분한” 문제와 “용량이 무거운” 문제는 완전히 다른 곳에 있었다. 경량화의 실익 99%는 백업 디렉토리 처리에 있다.

한 발 더 들어가면 흥미로운 구조가 보인다. 실사용 data/가 132M인데 DB 자체는 3MB였다. 즉 129M는 이미지 폴더다. 그리고 백업 3개도 똑같이 이미지를 통째로 복사해서 각각 132M가 됐다. 거의 동일한 이미지의 사본 3벌이 디스크를 먹고 있었던 것이다.

💡 이미지 파일명이 이미 콘텐츠 해시였다

이 앱의 이미지는 migrated_<sha>.png / inline_<sha>.png 형태로, 파일명이 내용 기반 해시다. 즉 images 폴더는 내부적으로 이미 중복 제거된 상태였다. 그런데 백업은 그걸 또 통째로 복사했다. 스냅샷 하나면 충분한데 3개는 순수 낭비였던 셈.

4. 삭제해도 되는가 — MARK 마커 대조 검증법

패치 스크립트가 “런타임 무관”인 건 확인했다. 하지만 더 중요한 질문이 남았다: 이 패치들의 결과가 본 파일에 정말 다 반영됐는가? 만약 미적용 패치가 있다면 지우는 순간 그 기능 복원 수단을 잃는다.

다행히 각 패치는 적용 시 본 파일에 MARK:pNN-… 형태의 고유 마커를 심도록 설계돼 있었다. 그렇다면 검증은 간단한 집합 연산이 된다 — “패치가 심으려는 마커” 집합에서 “본 파일에 실제로 박힌 마커” 집합을 뺀 차집합이 비어 있으면, 모든 패치가 반영 완료된 것이다.

집합 대조 — comm으로 차집합 추출
[본 파일에 박힌 마커]와 [패치가 심으려는 마커]를 정렬 후 비교. 패치에만 있고 본 파일에 없는 것만 출력.
comm -13 \
  <(grep -oh "MARK:p[0-9a-z-]*" app.py templates/*.html | sort -u) \
  <(grep -oh "MARK:p[0-9a-z-]*" patch_*.py | sort -u)

출력에 6개가 떴다. 처음엔 “미적용 패치가 있나?” 싶었지만, 하나씩 추적하니 전부 나중 패치가 같은 코드 영역을 통째로 덮어쓰면서 마커 이름만 바뀐 경우였다.

차집합에 뜬 마커진실
p10v201-app-weakpaging이후 패치가 이 블록 전체 교체 → 새 마커로 흡수됨
p10v11-dash-weak / -header렌더 함수 전체가 다음 버전에서 재작성됨
p12-dash-tagrow태그 행 로직이 후속 패치로 교체됨
p15-admin-desc안내문이 다음 버전에서 개정됨
p9- (단독)정규식 오탐 (주석 안의 문자열). 마커 아님
✅ 검증 결론: 옛 마커를 “잡아먹은” 새 마커들이 전부 본 파일에 존재

차집합에 뜬 마커들의 후속 버전(p11-app-weakquery, p11-dash-renderfn, p13-dash-tagclick, p16-admin-desc-rev)이 모두 본 파일에 살아 있었다. 즉 기능이 빠진 게 아니라 순차적으로 잘 덮였다는 건강한 신호다. → 패치 스크립트 14개 + 백업 4개는 삭제해도 기능 100% 유지.

⚠ 문자열 매칭 패치의 근본 취약성

이 검증 과정에서 드러난 더 큰 문제: 문자열 치환 기반 패치는 본질적으로 깨지기 쉽다. 실제로 한 패치는 과거에 “OLD 문자열 불일치”로 적용 실패한 이력이 있었고, grep으로 실제 코드를 떠서 재작성한 흔적이 남아 있었다. Phase가 16까지 오면서 다음 패치는 더 높은 확률로 충돌한다. 이게 진짜 부채다.

5. 진단 결론 — 증상과 원인의 분리

출발은 “파일이 많아서 지우고 싶다”였지만, 명령어로 증거를 모으니 전혀 다른 그림이 나왔다.

층위표면 증상실제 원인
시각파일이 많아 지저분함패치 14개 (용량 0.3M, 무해)
용량디렉토리가 무거움중복 백업 3개 = 397M (75%)
구조—git 부재 → 파일 복사로 버전관리 흉내
🎯 핵심 통찰

이 디렉토리의 진짜 부채는 “파일이 많은 것”이 아니라 “파일 복사로 버전 관리를 흉내내고 있는 것”이었다. 패치 스크립트도, .bak 파일도, 데이터 백업 디렉토리도 — 전부 “이전 상태를 보존하고 싶다”는 동일한 욕구의 수동 구현이다. 버전 관리(git)와 제대로 된 백업 전략이 없어서 생긴 증상들이다.

따라서 해결책은 “파일 지우기”가 아니라 git 도입 + 백업 인프라 구축이다. 그래야 다시는 이 청소를 반복하지 않는다.

다음 편에서는 실제 정리 작업을 다룬다. git을 도입해 소스를 영구 보존한 뒤, “안전망을 먼저 확인하고 나서만 삭제한다”는 원칙으로 529M를 133M로 줄이는 전 과정 — 그리고 그 과정에서 한 번도 데이터를 위험에 빠뜨리지 않은 명령어 순서를 기록한다.