「FastAPI로 그게 됩니까?」 1부 — 금요일 밤의 아키텍처
· Retrospect · 10 min
PyCon 발표 「FastAPI로 그게 됩니까?」 — 됩니다: 엔터프라이즈 백엔드를 지탱하는 모듈러 모놀리스 1년 회고를 준비하며 쓴 3부작의 첫 번째 글이다. 발표 슬라이드 전체(PDF, 71장)를 함께 볼 수 있다.
연사 선정 메일을 받은 날, 나는 발표 제목부터 지었다.
「FastAPI로 그게 됩니까?」 — 됩니다.
제목을 짓는 데는 오 분이 걸렸고, "됩니다"라는 두 글자를 증명할 자료를 만드는 데는 몇 주가 걸렸다. 증명의 방법은 하나뿐이었다. 내가 지난 일 년 동안 만들고 부수고 다시 지은 코드베이스 전체를, 남에게 보여줄 수 있을 만큼 정직하게 다시 파내는 것. 레거시 저장소의 git log를 처음부터 다시 읽고, 오래전에 봉인해 둔 배포 스크립트를 다시 열고, 잊고 싶었던 삽질의 흔적들을 줄 수까지 세어 가며 발굴했다.
그 발굴 작업이 끝났을 때, 나는 발표자료보다 먼저 이 글을 쓰고 싶어졌다. 발표는 사십 분이지만, 사십 분 안에 눌러 담느라 접어 둔 감정이 너무 많았기 때문이다.
부고를 쓰는 기분
이 블로그에는 비영리 스타트업의 MSA 여정이라는 글이 있다. 2025년 12월의 나는 그 글에서 서비스 간 인증 전략과 토큰 설계, 분산 트랜잭션을 향한 로드맵을 제법 진지하게 논했다. JWT를 어떻게 나눠 서명할지, Observability를 어떻게 쌓아 올릴지. 그 글의 모든 문장은 진심이었고, 대부분은 기술적으로 틀리지 않았다.
그리고 지금 나는 그 아키텍처의 부고를 쓴다.
부고라고 해서 고인을 욕보이려는 것은 아니다. 오히려 반대다. 그 시스템은 죽는 날까지 성실하게 일했다. 공식 성능 테스트 12개 항목을 전부 통과했고, API p95 레이턴시 1,340ms, 30일 가동률 99.87%, 동시 250세션에서 에러율 0.4%를 기록했다. 대시보드는 언제나 초록불이었다. 문제는 대시보드에 나오지 않는 지표가 하나 있었다는 것이다.
나였다.
두 개의 통념 위에서
먼저 고백할 것이 있다. 이 이야기의 모든 결정은 내가 했다. 원망할 전임자도, 설득에 실패한 아키텍트도 없다. 1인 개발 조직의 CTO에게 아키텍처 회고란 결국 자기 자신과의 대질 심문이다.

2025년의 나는 두 개의 통념 위에 서 있었다.
하나. FastAPI는 이름 그대로 'Fast하게 API를 서빙하는' 도구다. 실제로 FastAPI는 그 강점을 전면에 내세우며 등장했고, 파이썬 커뮤니티의 시선도 오랫동안 비슷했다. 빠른 프로토타이핑, 마이크로서비스의 한 조각, 가볍게 쓰기 좋은 물건.
둘. 수십 개의 도메인과 여러 계층(layer)을 가진 대규모 시스템은 Spring, 혹은 NestJS로 짓는다. Layered Architecture, Dependency Injection, 모듈 시스템 — 엔터프라이즈 설계의 어휘는 JVM과 TypeScript 진영의 것이라는 정설이 있었다.
나는 그 정설을 존중했고, 그래서 메인 백엔드를 NestJS로 지었다. NestJS는 Spring의 사상 — IoC 컨테이너, 데코레이터 기반 DI, 모듈 단위 구성 — 을 TypeScript로 옮겨 놓은, 말하자면 백엔드 아키텍처 교과서의 충실한 번역본이다. 교과서를 따르는 것은 부끄러운 일이 아니다. 문제는 교과서가 가정하는 독자와 나의 처지가 달랐다는 데 있었다.
내가 만든 나라의 지도
발표 준비를 하며 레거시 시스템의 전체 지도를 처음으로 한 장에 그렸다. 그리면서 알았다. 나는 이 지도를 머릿속에만 넣고 다녔을 뿐, 한 번도 눈으로 본 적이 없었다는 것을.

숫자로 요약하면 이렇다.
- 저장소 6개, 배포 유닛 9개 (PM2 프로세스 4벌 + ECS 태스크 5유닛)
- EC2 3대 + ECS, 논리 데이터베이스 4개 (PostgreSQL 15와 16 혼재)
- 언어 2개 — TypeScript 5.1과 Python 3.11 / 3.12 / 3.13 세 버전
- 패키지 매니저 3종 (yarn berry, npm, uv), CI 워크플로우 5벌
- JWT 인증 구현 4벌 — 전부 같은 시크릿으로 각자 서명
- 백엔드 코드 합계 약 132,000줄, 엔드포인트 약 350개
- 그리고 개발자, 1명
여기서 하나 짚어 둘 것이 있다. 흔히 이 이야기를 들으면 "NestJS가 문제였구나"라고 넘겨짚기 쉬운데, 실상은 더 흥미롭다. 여섯 저장소 중 NestJS는 메인 백엔드 하나뿐이었고, 나머지 백엔드 셋은 이미 FastAPI였다. AI 추천 서비스도, LLM 챗봇도, 후속 시도였던 멀티테넌트 서비스도 전부 파이썬이었다. 그런데도 힘들었다. 그때 처음으로 의심이 생겼다. 문제는 프레임워크가 아니라 경계(boundary)의 개수 아닐까.
금요일 저녁 여섯 시의 체크리스트
아키텍처의 성패는 다이어그램이 아니라 새벽 배포에서 판정된다. 그 시절의 배포가 어땠는지, 발표자료를 만들며 실제 인프라 문서와 CI 스크립트에서 항목을 추려 재구성해 보았다.

과장이 하나도 없다. EC2 세 대에 각각 SSH로 들어가야 했고, PM2 앱 네 벌과 ECS 다섯 유닛의 상태를 따로 확인해야 했으며, 서비스 기동 순서 — postgres → yeirin-ai → yeirin-backend → soul-e — 를 외우고 있어야 했다. .env 키는 네 서비스에 걸쳐 총 121개였고, 메인 백엔드의 CI는 .env 파일 하나를 만들기 위해 AWS SSM Parameter Store를 스물세 번 순차 호출해서 한 줄씩 echo로 이어 붙였다. 빌드는 2vCPU짜리 t3.small 인스턴스 위에서 yarn build로 돌았고, 단일 인스턴스였으므로 빌드하는 동안은 그대로 다운타임이었다. 롤백 플랜은 없었다. CI에 테스트 스텝도 없었다.
한 서비스는 공식 배포 절차가 "M1 맥북에서 --platform linux/amd64로 크로스 빌드해서 ECR에 푸시"였다. 문서에 그렇게 적혀 있었다. 내가 적었다.
그런데 발표를 준비하면서 깨달은 진짜 리스크는 다운타임이 아니었다. 이 체크리스트 전체를 아는 사람이 조직에 나 하나뿐이라는 사실이었다. 버스 팩터(bus factor)가 1이라는 말은 흔히 농담처럼 쓰이지만, 그 1이 자기 자신일 때 그것은 농담이 아니라 매주 금요일 저녁의 물성 있는 무게가 된다.
결합은 어디에 숨어 있었나
MSA의 약속은 격리(isolation)다. 서비스는 각자의 데이터를 소유하고, 잘 정의된 API로만 대화한다. 이 약속이 우리 시스템에서 어떻게 지켜졌는지 실측해 보았다.

메인 백엔드에는 이웃 서비스를 호출하기 위한 HTTP 클라이언트가 있었다. soul-e.client.ts, 593줄. 그중 130여 줄은 상대 서비스의 Pydantic 스키마를 TypeScript 인터페이스로 손으로 옮겨 적은 것이었다. 주석에는 이렇게 적혀 있었다. "Soul-E의 스키마와 동일." 아무것도 강제하지 않는, 오직 성실함에만 기대는 약속이었다. 반대 방향으로는 soul-e 쪽에 yeirin을 호출하는 클라이언트가 957줄 있었고, 내부 인증을 위한 시크릿 헤더는 서비스마다 이름이 달라서 X-Internal-Secret, X-Internal-Api-Key, X-Soul-E-Secret 세 종류가 공존했다.
그러나 진짜 무서운 결합은 HTTP가 아니었다.
soul-e 서비스의 코드베이스 안에는 infrastructure/yeirin_db/라는 디렉토리가 있었다. 내용물은 727줄짜리 읽기 전용 미러 모델(read model) — 이웃 서비스인 yeirin-backend가 TypeORM으로 관리하는 테이블들을, 파이썬 SQLAlchemy 모델로 손으로 복사해 둔 것이다. API를 우회해 남의 데이터베이스를 직접 읽기 위해서였다. camelCase 컬럼명은 물론이고 child_profiles_gender_enum 같은 PostgreSQL 네이티브 enum의 타입 이름까지 그대로 베껴야 했다. 그리고 원본 쪽 TypeORM 설정은 synchronize: true — 개발 중 엔티티가 바뀌면 스키마가 자동으로 바뀌는 모드였다. 즉, 한쪽이 필드 하나를 고치는 순간 다른 서비스가 런타임에 소리 없이 무너지는 구조였다.
정리하면 이렇다. 서비스들이 서로에게 말을 걸기 위한 배관 코드 — 클라이언트, 웹훅 수신부, 미러 모델 — 를 전부 합치면 약 3,185줄. 마이크로서비스의 격리라는 약속은, 그 약속을 지킬 인원이 없는 조직에서 이렇게 스스로 무너져 있었다. 나는 발표자료에 이 대목을 한 문장으로 요약했다.
우리 회사 안에서, 우리 회사한테, HTTP를 쳤습니다.
성능은 한 번도 문제였던 적이 없다
여기까지 읽으면 이 시스템이 형편없었다고 오해하기 쉬운데, 다시 강조하고 싶다. 성능 지표는 전부 좋았다. 그게 이 회고에서 가장 중요한 반전이다.

24시간 안정성 테스트에서 메모리 누수는 없었고, 크래시 복구는 12~21초 안에 이루어졌으며, 부하 테스트도 목표치를 전부 상회했다. 만약 내가 이 시스템을 성능 지표로만 평가했다면 바꿀 이유가 하나도 없었다.
문제는 다른 곳에 있었다. 2GiB 메모리의 서버에서 세 개의 런타임이 각자 200MB씩, 합쳐서 약 660MB를 그저 세 개의 프로세스로 존재하기 위해 유휴 점유하고 있었다. Auto Scaling은 적용되어 있지 않았다. 그러니까 우리는 마이크로서비스의 운영 비용 — 경계마다의 배포, 계약 관리, 장애 추적, 인지 부하 — 을 전부 지불하면서, 마이크로서비스의 핵심 이점인 독립적 확장성은 하나도 누리지 못하고 있었던 것이다.
기술부채(technical debt)라는 말을 우리는 흔히 "지저분한 코드"의 동의어로 쓴다. 이 일 년이 내게 가르쳐 준 정의는 다르다. 성능 지표가 좋은데 삶이 나쁘다면, 그것이 기술부채다. 부채의 이자는 레이턴시가 아니라 금요일 밤으로 청구되고 있었다.
세 갈래 길
2026년 3월, 나는 스스로에게 물었다. 이 시스템을, 이 인원으로, 계속 운영할 수 있는가.

선택지는 셋이었다.
A안 — MSA를 제대로 재정비한다. 반쯤 하다 만 컨테이너화를 완성하고, Observability를 쌓고, Auto Scaling Group을 붙이는 길. 큰 조직이라면 정답이었을 것이다. 하지만 이 길은 운영 비용의 원인 — 경계의 개수 — 을 하나도 줄이지 못한다. 1인 팀에게 경계 하나하나는 매달 나가는 월세다. 월세가 버거운 사람에게 필요한 것은 더 좋은 집기가 아니라 방 개수를 줄이는 일이다.
B안 — NestJS 단일 모놀리스로 합친다. 경계를 줄인다는 점에서는 옳은 방향이었다. 하지만 이미 코드의 절반이 파이썬이었고, 결정적으로 — 이건 2부에서 자세히 쓰겠지만 — 이 시스템의 가장 큰 흉터들이 TypeScript 진영 쪽에 나 있었다. 데코레이터로 두 번 쓰는 타입, 문자열 토큰으로 등록하는 의존성, 순환 참조를 피하기 위한 forwardRef 열 곳.
C안 — FastAPI 기반의 모듈러 모놀리스(Modular Monolith)로 다시 짓는다. 경계는 없애는 것이 아니라 패키지로 옮긴다. 배포와 운영은 하나로 합치되, 도메인 사이의 선은 코드 레벨에서 유지한다. 그리고 언젠가 조직이 커져서 정말로 서비스를 쪼개야 하는 날이 오면, 그때 패키지째 떼어낼 수 있는 구조를 남겨 둔다.
나는 C를 골랐다. 고르면서 얻은 문장이 이 발표 전체의 요지가 되었다.
모놀리스는 MSA의 반대말이 아니라, MSA를 나중에 저렴하게 시작하는 방법이다.
Modular Monolith는 새로운 개념이 아니다. 단일 배포 단위 안에서 모듈 경계를 엄격하게 유지하는 이 접근은 JVM 진영에서 Spring Modulith라는 이름으로 이미 정식화되어 있었다. 내가 던진 질문은 하나였다. 그 구조를, 파이썬으로, 지금 지을 수 있는가?
2026년의 파이썬 생태계는 "그렇다"라고 답할 준비가 되어 있었다. uv의 workspace가 있었고, Python 3.12의 성숙한 타입 시스템이 있었고, Pydantic v2 위에 올라선 FastAPI가 있었다. 그 대답을 확인해 가는 14일의 기록 — Spring의 어휘를 파이썬으로 옮기는 '번역'의 시간 — 이 다음 글의 내용이다.
2부에서는 NestJS의 @Module과 DI 컨테이너, class-validator와 TypeORM이 각각 파이썬의 무엇으로 번역되었는지, 그리고 스트랭글러 패턴(Strangler Fig pattern)으로 13일 만에 도메인을 옮긴 과정을 다룬다. — 2부 읽기