website: 로그 유실 디버깅에서 시작된 인프라 문서화 작업
stage에서 400 에러를 디버깅하려다 재배포로 로그가 통째로 사라지는 걸 겪고, 그 원인과 해법을 `infra/logging.md`로 정리했습니다. 여기에 이번 작업 과정에서 얻은 통찰 4건을 `pm/docs/learnings.md`에 추가로 남겼습니다.
요약
2026년 7월 21일, website 레포의 dev 브랜치에 docs(infra): 로그 영속화 작업 문서화 + learnings 추가 커밋을 올렸습니다. 커밋 메시지 그대로 옮기면 "infra/logging.md 신설(구조·왜·검증), infra/CLAUDE.md 파일 목록 갱신, pm/docs/learnings.md에 재사용 가능한 통찰 4건 추가(로그 유실 구조적 원인, git-drift 오탐 패턴, infra/** CD 미트리거, 서버 배포키 read-only)"입니다.
변경된 파일은 세 개입니다.
infra/logging.md(신규, +63)pm/docs/learnings.md(+4)infra/CLAUDE.md(+2)
전체 69줄이 추가됐고 삭제는 없습니다. 코드 자체를 바꾼 커밋은 아니고, 실제 인프라 수정(application.yml, docker-compose.yml 변경)은 이 커밋 이전에 이미 서버에 반영되어 있었던 것으로 보이며, 이번 작업은 그 경위와 검증 결과를 문서로 남기는 데 집중했습니다.
배경 및 목적
발단은 stage 환경에서 POST /api/posts가 400 whitebox 에러(스프링 기본 에러 페이지, 커스텀 핸들러가 안 먹힌 상태)를 던지는 걸 디버깅하려던 것이었습니다. 원인을 잡으려고 로그를 확인하려는데, 그 사이 재배포가 한 번 더 지나가면서 문제의 스택트레이스가 담긴 로그가 통째로 사라져 있었습니다.
파고들어 보니 구조적인 문제였습니다. 도커 기본 로그(json-file 드라이버)는 컨테이너의 쓰기 레이어에 저장되는데, docker-compose.yml에는 ./data:/app/data처럼 DB만 볼륨으로 영속화돼 있었고 로그는 대상이 아니었습니다. CD가 배포마다 컨테이너를 docker rm으로 교체하는 순간, 직전 버전의 로그가 함께 날아가는 구조였던 겁니다. 디버깅하려던 시점엔 이미 늦어서 원하던 로그는 복구할 수 없었고, 그래서 "다음에 또 이런 일이 없도록" 근본 수정과 문서화를 같이 진행하게 됐습니다.
구현 내용
infra/logging.md — 로그 영속화 구조 문서 (신규, +63줄)
이 문서 하나에 문제, 해법, 파생 이슈, 검증 결과, 미결 사항까지 다 담았습니다. 해법은 세 단계로 정리됩니다.
첫째, Spring Boot가 콘솔뿐 아니라 파일로도 로그를 쓰게 했습니다.
logging:
file:
name: ${LOG_FILE_PATH:logs/website-backend.log}
logback:
rollingpolicy:
max-file-size: 20MB
max-history: 14
total-size-cap: 1GB
둘째, 배포 태그를 파일명에 그대로 활용했습니다. 새 환경변수를 만들지 않고, CD가 이미지 태그(stage-<커밋SHA>)로 쓰던 STAGE_TAG/PROD_TAG를 재사용해서 로그 파일명과 배포 버전이 항상 1:1로 대응하게 했습니다.
backend-stage:
environment:
- LOG_FILE_PATH=/app/logs/${STAGE_TAG:-stage-latest}.log
volumes:
- ./logs/stage:/app/logs
셋째, 호스트 바인드 마운트로 컨테이너 생사와 로그 파일의 생명주기를 분리했습니다. 컨테이너가 재생성돼도 마운트를 다시 걸 뿐 실제 파일은 호스트(infra/logs/{stage,prod}/)에 그대로 남습니다.
문서에는 이번에 실제로 겪은 두 가지 배포 제약도 기록해뒀습니다.
infra/**단독 변경은 CD를 트리거하지 않습니다..github/workflows/cd.yml의 paths 필터가backend/**,shared/**만 감시하기 때문에, docker-compose.yml만 고친 커밋은 push해도 자동 배포가 안 돌아서 서버에 SSH로 들어가 수동으로git pull+docker compose up -d를 해야 했습니다.- OCI 서버의 배포용 SSH 키(deploy key)는 GitHub에 read-only로 등록돼 있어서, 서버에서 커밋은 되지만 push는 거부됩니다. 이 때문에 서버가 origin보다 여러 커밋 앞서 있던 미스터리가 사실 "애초에 push가 안 됐던 것"이라는 게 밝혀졌고, 이번엔 로컬의
gh인증 계정으로 동일 파일을 재현해서 push하는 방식으로 우회했습니다.
실측 검증 결과도 남겼습니다. 배포 후 infra/logs/stage/에 stage-05a02e299030a7090b0d014724453e7acecd88c5.log가 정상 생성됐고, 첫 시도에서는 LOG_FILE_PATH=/app/logs/stage-${STAGE_TAG}.log에서 STAGE_TAG 자체가 이미 stage-로 시작하는 값이라 stage-stage-<sha>.log처럼 접두사가 중복되는 버그가 있었습니다. docker-compose.yml에서 리터럴 접두사를 제거해 재생성한 뒤 정상화됐고, 재배포 이후에도 이전 버전 로그가 호스트에 남아있는 것까지 확인해서 원래 목적을 달성했습니다.
pm/docs/learnings.md — 재사용 가능한 통찰 4건 (+4줄)
이번 작업 과정에서 얻은 교훈을 "인프라 · CI/CD" 섹션에 추가했습니다. 각각 짧게 요약하면:
- 도커 기본 로그는 컨테이너 쓰기 레이어에 저장돼 재배포 시 함께 사라진다 — DB만 볼륨 영속화하고 로그는 빠뜨리는 게 흔한 실수 패턴이라는 점.
- 새로 추가하는 볼륨 마운트(
infra/logs/)는 만드는 즉시.gitignore처리를 안 하면 git-drift 알람이 오탐 발동한다는 점 —infra/data/와 같은 이유. infra/**단독 변경은 CD paths 필터에 안 걸려 자동 배포가 안 된다는 점.- 서버 배포키가 read-only라 서버발 커밋은 push가 안 된다는 점, 그리고 그게 과거 "서버가 origin보다 앞서 있던" 현상의 진짜 원인이었다는 점.
infra/CLAUDE.md — 파일 목록 갱신 (+2줄)
새로 생긴 infra/logs/{stage,prod}/ 경로와 infra/logging.md 문서를 파일 목록 테이블에 추가해서, 다음에 이 레포를 다루는 사람(혹은 AI 에이전트)이 바로 찾을 수 있게 했습니다.
배운 점 및 개선점
가장 크게 느낀 건 "증상이 같아 보여도 원인은 완전히 다를 수 있다"는 점을 문서화 습관이 잡아준다는 것이었습니다. learnings.md에 이미 있던 이전 항목(git이 실행권한을 추적하지 않아 chmod가 벗겨지는 문제, CD 3연속 실패의 진짜 원인이 admin-auth JWT_SECRET이었던 사건)과 나란히 놓고 보면, 이번 로그 유실 건도 비슷한 계열입니다 — "당연히 영속화돼 있겠지"라고 넘겼던 지점이 실제로는 빠져 있었던 겁니다.
또 하나는 CD 파이프라인의 paths 필터처럼 평소엔 신경 안 쓰다가 인프라 전용 수정을 할 때만 갑자기 튀어나오는 함정이 있다는 것. 이런 건 코드에 주석을 달아도 잘 안 보이니 별도 문서로 남겨두는 게 낫다고 판단했습니다.
미결 사항으로는 두 가지를 남겨뒀습니다. 배포가 잦아지면 infra/logs/{stage,prod}/ 아래 버전별 로그 파일이 계속 쌓이는데, 파일당 롤링은 걸려있어도 파일 자체를 지우는 로직은 없어서 디스크 사용량이 문제가 되면 정리 cron을 추가로 검토해야 합니다. 그리고 접두사 중복 버그로 남은 stage-stage-....log 잔존 파일도 급하지는 않지만 수동 정리 대상으로 남아있습니다.
참고 자료
infra/observability.md— git-drift 알람(#83) 관련infra/db-access.md— 기존 볼륨 영속화 패턴(infra/data/) 참고.github/workflows/cd.yml— CD paths 필터 확인