기술

파일을 원천으로 두고 FastAPI 서버를 미러로 만들었더니, 명령줄 작업은 DB 없이도 돌았습니다

2026.10.0610분 읽기

자사 영상 콘텐츠 프로젝트의 제작 파이프라인은 지금까지 명령줄로만 돌았습니다. 장면을 나누고, 인물 시트를 만들고, 그림과 목소리를 뽑고, 자막을 맞추는 단계가 전부 터미널이었습니다. 단계가 늘고 편당 장면이 수십 개가 되자 진행 상황을 알려면 사람이 파일을 하나씩 열어 봐야 하는 상태가 됐습니다.

그래서 화면을 붙이기로 했습니다. 이틀에 걸쳐 두 단계로 만들었고, 1단계가 플러그인 훅과 서버, 2단계가 화면 다섯 개입니다. 만들기 전에 제작 상태의 기준을 파일로 정했습니다. 서버의 DB 는 화면 조회를 위한 사본으로만 두고, 서버 자체는 제작 상태의 원천이 되지 않게 했습니다.


화면을 붙일 때 상태의 원천도 달라질 수 있습니다

화면을 붙이려면 서버가 필요합니다. 그런데 서버를 세우는 순간 그 서버가 자연스럽게 '진짜 상태'를 갖게 됩니다. 진행 상황이 DB 에 들어가고, 화면은 DB 를 읽고, 파일은 산출물 보관소로 내려갑니다. 여기까지는 아무도 반대하지 않습니다. 화면을 구현하기에도 가장 단순한 방식입니다.

문제는 그다음입니다. 기존 명령줄 파이프라인이 서버를 거쳐야만 동작하게 됩니다. 서버가 안 떠 있으면 아무것도 못 합니다. 그리고 파일을 손으로 고치는 순간 DB 와 어긋나는데, 어긋났다는 사실을 알 방법이 없습니다. 나중에 붙은 입구가 먼저 있던 입구를 부차적인 것으로 만든 것입니다.

이 갈림은 질문 하나로 바로 드러납니다. '서버가 안 떠 있어도 되나'입니다. 안 된다고 답하게 되는 설계는 원천을 이미 옮긴 것입니다.


세 갈래를 놓고 골랐습니다

원천을 어디 둘지는 실질적으로 세 갈래였습니다. 표로 놓고 보면 무엇을 사고 무엇을 파는지가 분명해집니다.

갈래

얻는 것

내주는 것

A. 서버가 원천 (DB 에 상태)

트랜잭션·동시성·조회가 전부 따라옵니다. 화면 만들기가 가장 쉽습니다

명령줄이 서버 없이는 멈춥니다. 파일을 손으로 고치면 어긋나는데 알 수가 없습니다

B. 파일이 원천 (DB 는 미러)

두 입구가 같은 것을 봅니다. 서버를 껐다 켜거나 DB 를 지워도 명령줄 작업은 파일 상태를 읽어 이어서 갑니다

잠금·수정시각 대조·캐시 무효화를 직접 해야 합니다

C. 양쪽 다 원천 (동기화)

겉보기에는 타협입니다

A 와 B 의 비용을 다 내고, 어느 쪽이 맞는지 판정하는 일까지 새로 삽니다

C 는 검토 단계에서 뺐습니다. B 를 골랐고, 그 대가로 낸 것이 잠금 설계와 수정시각 대조입니다. 파일 계층은 원자적 쓰기에 수정시각 대조를 얹어, 읽은 뒤에 파일이 바뀌었으면 409 로 돌려보냅니다. 상태 파일 잠금은 원자적 파일 생성(`O_EXCL` — 파일이 이미 있으면 생성이 실패하는 방식)으로 잡고, 이미 잡혀 있으면 전용 종료 코드로 빠져나옵니다. 서버 러너는 그 코드를 보고 실패가 아니라 '잠금 대기'로 처리합니다.

파일이 원천이고 명령줄과 화면이 같은 파일을 보는 구조를 서버 원천 구조와 좌우로 비교한 도식


서버가 결제를 못 하게 만들었습니다

돈 경로는 별도 축이었습니다. 화면에 '다시 만들기' 버튼이 있으면 그 버튼이 곧 지출입니다. 서버가 유료 API 를 부를 수 있게 하면, 잘못된 요청 하나가 서버 권한으로 돈을 씁니다. 화면이 늘어날 때마다 지출 경로도 같이 늘어납니다.

그래서 추정과 집행을 나눴습니다. 서버는 비용을 추정만 하고, 실제 유료 호출은 플러그인 스크립트에만 둡니다. 서버 코드에는 유료 API 를 부르는 경로가 아예 없습니다. 화면은 스크립트를 부르고, 스크립트가 결제 직후 상태 파일에 기록합니다. 유료 버튼은 세 관문을 지납니다.

추정 — 이 작업이 얼마나 나갈지 계산해 버튼 옆에 보여줍니다

확인 모달 — 실제로 실행될 명령을 그대로 복사할 수 있게 띄우고 사람이 확인합니다

금액 상한 — 상한을 넘으면 거기서 막습니다

이 구조의 이득은 점검 범위가 고정된다는 것입니다. 화면을 몇 개 더 붙여도 '여기서도 돈이 나가나'를 확인할 곳은 스크립트 한 층뿐입니다.


되돌린 결정 다섯 개가 설계서보다 쓸모 있었습니다

만들면서 다섯 가지 결정을 되돌렸습니다. 전부 설계 시점에는 옳아 보였던 것들입니다.

처음

되돌린 뒤

이유

장면 나누기를 스크립트로 빼기

스킬로 되돌림

화면이 그 스크립트를 부를 수단이 없었습니다. 코드를 빼도 실행 주체가 없습니다

파이썬 경로를 시스템 기본으로

서버 가상환경으로

실행기가 PATH 를 덮어써서, 플러그인을 서버가 부르면 엉뚱한 인터프리터가 잡혔습니다

편 전환 초기화를 이펙트로

태그 붙인 아톰으로

상태 라이브러리가 3렌더 안에 수렴하지 못하는 것을 실측했습니다

잡 실행 함수가 바로 결과를 돌려줌

모달이 닫힌 뒤에 확정

안 그러면 사용자 취소와 요청 실패가 같은 모습이 됩니다

잡이 끝나면 화면이 알아서 갱신

조회 무효화 + 미디어 주소에 버전 붙이기

캐시 때문에 새로 만든 그림이 옛 것으로 보였습니다

네 번째 줄(잡 실행 함수가 바로 결과를 돌려줌)이 특히 조용한 자리였습니다. 사용자가 '아니오'를 누른 것과 요청이 400 으로 떨어진 것은 다르게 처리돼야 하는데, 결과를 모달이 닫히기 전에 확정하면 둘이 한 덩어리가 됩니다. 화면에는 둘 다 '아무 일도 안 일어남'으로 보입니다.

여기에 하나 더 있습니다. 검토 필요 표시를 '사람이 지우면 끝'으로 뒀다가, 입력 해시가 바뀌면 다시 살아나게 고쳤습니다. 사람이 지운 표시가 그대로 남아 있으면, 재료가 바뀐 장면이 검토를 마친 것처럼 보입니다.

유료 작업이 추정과 확인 모달과 금액 상한 세 관문을 지나 스크립트에서만 집행되는 흐름도


미러라고 이름 붙여도 지켜지지 않습니다

DB 는 편과 잡 두 표를 미러하고 30초마다 파일을 살핍니다. 원본이 아니라 조회 편의용입니다. 그런데 'DB 는 미러다'를 문서에 적는 것만으로는 지켜지지 않습니다. 다음 커밋에서 조회 하나가 DB 를 먼저 읽기 시작하면, 그 순간부터 조용히 원천이 됩니다.

그래서 DB 없이 동작하는 것을 테스트로 고정했습니다. 그 테스트가 있으면 DB 를 먼저 읽는 코드가 들어올 때 거기서 깨집니다. 문서는 사람이 읽어야 지켜지고, 테스트는 안 읽어도 지켜집니다.

화면은 정적 내보내기라 서버 라우팅이 없습니다. 편과 장면은 쿼리 문자열로 넘기고, 빌드 결과를 서버가 루트에 그대로 얹습니다. 그래서 운영 포트가 하나입니다. 리버스 프록시도, 화면용 프로세스도 없습니다. 테스트는 서버 80 개, 화면 69 개(추가로 e2e 1 개), 플러그인 180 개로 닫았습니다. e2e 는 라우트를 모킹해 서버 없이 돕니다.

상태를 어디에 두느냐가 동작을 바꾸는 자리는 전에도 한 번 밟았습니다. 서버 메모리에 기록을 남겼더니 배포할 때마다 그 기록이 초기화되던 건입니다.


옮기기 전에 확인할 것

명령줄 도구에 화면을 붙일 계획이 있다면, 만들기 전에 아래를 먼저 답해 두면 나중에 되돌릴 일이 줄어듭니다.

서버가 안 떠 있어도 기존 입구가 동작하나 — 안 된다고 답하게 되면 원천을 옮긴 것입니다

미러 없이 도는 테스트가 있나 — 문서에 미러라고 적는 것만으로는 다음 커밋에서 뒤집힙니다

돈이 나가는 코드가 몇 층에 있나 — 한 층이면 그 층만 보면 되고, 여러 층이면 화면을 늘릴 때마다 다시 세야 합니다

취소와 실패가 화면에서 구분되나 — 확인 모달이 닫히기 전에 결과를 확정하면 둘이 한 덩어리가 됩니다

같은 파일에 두 프로세스가 붙을 수 있나 — 붙을 수 있으면 잠금과 그 잠금의 종료 신호를 먼저 정합니다


이 방식이 안 통하는 자리

파일 원천은 '같은 기계의 두 입구'라서 성립했습니다. 사용자가 여럿이고 원격에서 붙으면 파일 잠금으로는 못 버티고, 그때는 DB 가 원천이 되는 쪽이 맞습니다.

조회가 복잡해지면 미러가 원천이 되고 싶어 한다는 압력도 계속 있습니다. 목록 정렬·검색·집계가 늘면 '그냥 DB 에서 읽자'가 매번 이깁니다. 그 압력을 버틸 근거가 'DB 없이도 돈다'는 요구 하나뿐이라, 그 요구를 포기하는 순간 구조가 무너집니다. 그리고 돈 경로를 한 층에 몰면 그 층이 비대해집니다. 스크립트가 결제·상태 기록·재시도·보관을 다 갖게 됩니다. 층이 하나라서 안전한 것이지 그 층이 단순해서 안전한 것이 아닙니다.

파일 원천 구조가 성립하는 조건과 무너지는 조건을 위아래로 나눠 정리한 판단표

만들면서 가장 값진 산출물은 코드가 아니라 되돌린 결정 다섯 개였습니다. 최종 설계서는 '이렇게 됐다'만 말하고, 왜 다른 길이 아닌지는 안 적습니다. 그래서 전부 원장 문서에 '처음엔 이렇게 했고 왜 바꿨다' 형태로 남겼습니다. 대개 그 기록이 설계서보다 짧습니다. 다만 되돌린 결정 기록도 낡습니다. 조건이 바뀐 뒤에도 '왜 안 했나'만 남아 있으면 지금은 되는 것을 안 된다고 믿게 만들기 때문에, 조건을 같이 적습니다.

화면을 붙이는 일은 기능을 더하는 일처럼 보이는데, 실제로는 원천을 어디 둘지를 다시 정하는 일입니다. 먼저 정해 두지 않으면 만드는 도중에 저절로 정해지고, 그때는 이미 옮겨간 뒤입니다.

#단일원천#FastAPI#파일잠금#상태관리#정적내보내기