[Momently 개발기 #10·完] 혼자 개발하면서 ADR을 쓰는 이유 — 운영과 회고
사진을 넣으면 블로그 초안이 나오는 로컬 LLM 파이프라인 Momently 개발기
마지막 편. 이번 편은 이 모든 걸 돌리는 바닥 — Mac mini 한 대 위의 운영 구성과
거기서 난 사고들, 그리고 시리즈 전체를 관통하는 질문인 "혼자인데 왜 이렇게까지
문서를 쓰는가"에 대한 답이다.
운영 토폴로지: Mac mini 한 대의 프로덕션
Momently의 프로덕션은 클라우드가 아니라 집의 Mac mini다. 로컬 LLM(1편)을
선택한 순간 GPU가 있는 내 머신이 곧 서버가 되는 건 자연스러운 귀결이었다.
@startuml
skinparam shadowing false
skinparam componentStyle rectangle
cloud "Cloudflare Edge\n(HTTPS 종료, Access 정책)" as CF
node "Mac mini" {
package "macmini-shared 네트워크" {
[cloudflared\n(터널, mac-infra 소속)] as T
package "Momently 스택 (docker-compose)" {
[momently-gateway\n(nginx)] as GW
[momently-orchestrator\n(Spring)] as O
database "momently-postgres" as PG
[momently-ollama] as OL
[FastAPI 에이전트 × 10\n(grouping, draft, style,\nreview, meta, voice ...)] as A
}
[Prometheus / Grafana / Loki\n(mac-infra, 중앙 관측)] as MON
}
}
CF ==> T : 터널 (아웃바운드 연결)
T --> GW : http://momently-gateway:80
GW --> O : /api/**
O --> PG
O --> A
A --> OL
MON ..> O : /actuator/prometheus 스크랩\n(외부 비노출)
@enduml
구성에서 짚을 결정들:
- 인바운드 포트 0개. Cloudflare Tunnel은
cloudflared가 밖으로 연결을
여는 방식이라 공유기 포트포워딩이 없다. 집 IP가 노출되지 않고, HTTPS는
엣지에서 종료된다. 콘솔(SPA)·워크플로 API·말투 API를 nginx 게이트웨이 하나
뒤에 모아 단일 호스트네임으로 서비스한다 — 브라우저 기준 동일 출처가
되므로 CORS 지옥도 피한다. - healthcheck가 기동 순서를 지킨다. 에이전트 전부에
/health체크를 달고,
orchestrator와 gateway는 의존 서비스의service_healthy조건으로만 뜬다.
"떴는데 아직 준비 안 된" 컨테이너로 트래픽이 가는 창을 없앤다. - 관측은 중앙 레포(mac-infra)로 분리. Prometheus 알람 룰, Grafana 대시보드,
Loki 룰은 Momently 레포가 아니라 머신 전체의 관측을 담당하는 별도 레포에
산다. 6·7편의 메트릭·알람이 전부 여기로 모인다. Prometheus는 외부에 열리지
않고 내부 네트워크에서/actuator/prometheus만 스크랩한다. - 터널의 물리 법칙도 설계 입력이다. Cloudflare 경유 요청에는 약 100초
제한이 있다. 말투 심층 분석처럼 Ollama를 몇 분씩 잡는 요청은 게이트 nginx의proxy_read_timeout을 600초로 늘려도 엣지에서 504가 난다 — 그래서 빠른
분석 모드를 두고, 긴 작업은 로컬 게이트로 확인하는 우회로를 문서화했다.
업로드도 마찬가지로 nginxclient_max_body_size(150m)와 Spring
multipart(140MB), 콘솔의 사전 검증(GET /api/v1/uploads/config)을 한 줄로
맞춰 뒀다 — 제한이 계층마다 다르면 어느 층에서 잘렸는지 아무도 모르게 된다.
사고: gateway만 재배포했는데 백엔드가 내려갔다
nginx 설정을 바꾸고 반영하려고 늘 하던 명령을 쳤다.
docker compose up -d --build momently-gateway
그런데 백엔드 전체가 내려갔다. 무슨 일이 있었나.
up -d --build <service>는 지정한 서비스만 빌드하지만, 의존 관계로 엮인
서비스가 재생성 대상이 되면 같이 재생성한다. 이때 orchestrator에 커밋되지
않은, 빌드가 깨지는 변경이 워킹 트리에 있었다 — orchestrator가 재생성되며
깨진 코드로 이미지를 다시 빌드했고, 부팅 실패. 이전의 멀쩡한 이미지는 이미
대체돼서 롤백할 이미지도 없었다.
사고의 교훈은 두 겹이다.
- 부분 재배포 명령의 반경은 명령어가 아니라 의존 그래프가 정한다.
"gateway만"이라고 쳤다고 gateway만 움직이는 게 아니다. - 워킹 트리가 더러운 상태의 머신은 배포 가능 상태가 아니다. 빌드 소스가
git이 아니라 로컬 디렉터리인 compose 운영에서는, 커밋 안 된 변경이 곧
"잠재적 배포물"이다.
이 사고 이후 배포 규칙을 워크플로에 박았다: PR 머지 후 도커 재빌드 + 헬스체크
확인까지가 작업의 끝이다. "머지했으니 끝"이 아니다 — 머지만 하고 재배포를
안 하면 서버에는 옛 이미지가 돌고, 재배포를 나중에 몰아서 하면 이번 사고처럼
의도치 않은 변경까지 함께 나간다. 배포를 머지에 붙여 원자화하는 것이 결국
가장 싼 안전장치였다.
개발 워크플로: 혼자지만 팀처럼
이 프로젝트의 커밋·리뷰 규칙은 1인 프로젝트치고 과하게 보일 수 있다.
- 브랜치 → 최소 단위 커밋 → PR → 리뷰 → 머지 → 재배포+헬스체크.
- 커밋은 같은 모듈 안에서도 "한 가지 이유" 단위로 잘게 쪼갠다. 동기는 미학이
아니라 부분 롤백 가능성이다. 사고는 항상 "이 변경만 되돌리고 싶다"의
형태로 오는데, 커밋이 뭉쳐 있으면 되돌리기의 단위가 커진다. - PR 리뷰는 자동화된 리뷰(봇 + AI 리뷰어)를 통과해야 머지한다. 6편의 production
review에서 4xx 뭉개기나 로그 인젝션을 잡아낸 게 바로 이 관문이다.
그런데 혼자인데 누가 리뷰하나? — 이 질문이 마지막 주제로 이어진다.
혼자 개발하면서 ADR을 쓰는 이유
이 시리즈 내내 ADR(Architecture Decision Record)이 등장했다. 모듈 경계(001),
API 경계(002), ID(003), DB(004), 에러 envelope(005), 서킷브레이커(006), 발행
확장(007). 혼자 하는 프로젝트에 이건 관료제 아닌가?
내 답은 세 가지다.
1) 미래의 나는 남이다. 3주 뒤의 나는 "왜 bulkhead가 semaphore였는지",
"왜 4xx는 서킷을 안 여는지" 기억하지 못한다. 결정만 남고 이유가 사라지면,
미래의 내가 그 결정을 실수로 뒤집는다. ADR의 "버린 대안" 섹션은 특히 그렇다 —
버린 이유가 기록돼 있지 않은 대안은 반드시 다시 검토된다. 같은 고민을 두 번
하는 것이 1인 개발의 가장 비싼 낭비다.
2) AI와 협업하려면 문서가 곧 컨텍스트다. 이 프로젝트의 실질적 팀원은 AI
에이전트들이었다. 파이프라인 모듈마다 개발-리뷰 페어를 두고, 전체 계약과 흐름을
지키는 중앙 리뷰 역할을 따로 두는 운영 모델을 문서로 정의해서 돌렸다. AI는
매 세션 기억이 리셋되는 팀원이다 — 그런 팀원에게 "우리 팀은 이렇게 일한다"를
전달하는 유일한 수단이 문서다. ADR과 계약 문서가 있으면 어느 세션의 AI든 같은
기준으로 코드를 쓰고 리뷰한다. 실제로 ADR 005는 채택 이후 모든 에이전트 변경
PR의 리뷰 기준으로 작동했다. 문서가 사람을 위한 기록에서 시스템을 움직이는
설정으로 승격되는 경험이었다.
3) ADR은 결정의 이력이지 선언이 아니다. 이 시리즈에서 가장 값진 문서
조각들은 처음 쓴 결정이 아니라 고쳐 쓴 흔적이었다 — 광역 advice를 disjoint로
좁힌 "원래 결정 수정"(6편), 가정한 메트릭이 실재하지 않아 고친 관측 설계(7편),
presign 가정이 실측에 무너진 구현 노트(9편). 틀린 흔적이 남아 있는 문서만이
다음 결정의 출발점을 앞당겨 준다.
시리즈 회고: 가장 잘한 결정, 가장 아픈 교훈
가장 잘한 결정 셋.
- 로컬 LLM (1편) — 비가역적인 것(데이터 유출)을 불변 조건으로 잡고, 가역적인
것(품질/속도)을 튜닝 대상으로 남긴 것. 이후 모든 트레이드오프의 기준점이 됐다. - 에이전트 = 독립 모듈 = 독립 장애 도메인 (2편) — 이 경계 하나가 모델 비교
실험(4편), 점진 에러 마이그레이션(6편), per-agent 서킷브레이커(7편)의 비용을
전부 낮췄다. 좋은 경계는 복리로 돌아온다. - 표준을 만들 때 reference와 점진 적용을 함께 설계한 것 (6편) — "표준
정의"가 아니라 "기존 것과의 공존 계획"이 진짜 산출물이었다.
가장 아픈 교훈 셋.
- 같은 사실의 사본이 둘이면 기계로 동기화하라 (3편) — enum과 CHECK 제약.
사본은 만들 때가 가장 싸고 어긋난 뒤가 가장 비싸다. - silent fallback은 실패를 숨긴다 (5편) — "보내고 끝" 버그. fallback을
두려면 발동 사실이 관측돼야 한다. 성공처럼 보이는 실패가 최악의 실패다. - 전달되지 않은 의도의 자리는 가장 구체적인 신호가 차지한다 (8편) — OCR
납치 사건. 결정권 서열은 명시하지 않으면 우연이 정한다.
남은 숙제. 서킷 OPEN 시 워크플로를 우아하게 다운그레이드하는 선택 단계
(meta 생략하고 완료 처리), 운영 데이터 기반 CB/bulkhead 파라미터 튜닝, 발행
확장의 스토어 심사와 네이버 정책 모니터링 체계, 그리고 만성 과제인 평가 suite
샘플 확충(4편). 파이프라인은 돌아가지만, 운영은 끝나는 종류의 일이 아니다.
시리즈를 한 문장으로 줄이면 이렇다. LLM 프로젝트를 지탱하는 것은 프롬프트가
아니라 경계·계약·상태·관측이라는 오래된 엔지니어링이었다. LLM은 이 구조물
안에서 일하는, 재능 있지만 신뢰할 수 없는 직원이다. 구조가 부실하면 그 직원의
실수가 시스템의 실수가 되고, 구조가 튼튼하면 실수는 후처리 한 줄에서 멈춘다.
여기까지 읽어주셔서 감사합니다. 질문이나 "우리는 이렇게 풀었다"는 다른 답은
댓글로 나눠 주시면 반갑겠습니다.
'AI블로그 자동 포스팅 - Momently' 카테고리의 다른 글
| Momently 개발기 #9] 자동 발행은 왜 안 되는가 — 네이버 발행과 브라우저 확장 (0) | 2026.07.19 |
|---|---|
| [Momently 개발기 #8] 사진 속 단어 하나에 글 전체가 납치당했다 — 사용자 의도와 프롬프트 우선순위 (0) | 2026.07.19 |
| [Momently 개발기 #7] 에이전트 하나가 느려지면 전체가 죽는다 — Circuit Breaker & Bulkhead (0) | 2026.07.19 |
| [Momently 개발기 #6] 에이전트 10개의 에러가 제각각이던 날 (0) | 2026.07.19 |
| [Momently 개발기 #5] 내 말투를 학습시키기 — voice profile과 "보내고 끝" 버그 (0) | 2026.07.19 |