분산된 코드가 AI의 발목을 잡았다 — MSA를 유지한 채 모노레포로 간 4주
24 Jun 2026
멈춰 있던 제 사업 카오르를 다시 굴리기로 했습니다. 문제는 제 체력이 아니라, 매번 발목을 잡는 환경 설정이었습니다.

저녁 9시, 머리가 안 돌아간다
출퇴근 왕복 3시간에 회사 일까지 하고 집에 오면, 몸은 피곤하고 머리는 멈춥니다. 그 상태로 멈춰 있던 카오르를 다시 굴리려니 답은 하나뿐이었습니다. AI에게 많이 맡기는 것. 일을 잘 떼어 주면 개발이 반자동으로 굴러갑니다. 멈춘 머리로도 진도가 나갑니다.
그런데 매번 같은 데서 막혔습니다. 환경 설정. 서비스마다 레포가 다르고, 띄우는 절차가 다르고, AI한테 “지금 이 서비스는 저 서비스를 이렇게 부른다”를 설명하는 것부터가 일이었습니다. 정작 일을 시키기도 전에 제가 먼저 지쳤습니다.
이 글은 그 병목을 4주 동안 사부작사부작 걷어낸 기록입니다. 아직 현재 진행형입니다.
원래 왜 MSA였나 — 그땐 맞는 선택이었다
카오르는 마이크로서비스로 흩어져 있었습니다. main-api(PHP + Laravel)를 중심으로 여러 서비스가 각자의 레포에서 각자 배포되는 구조였습니다. 이유는 분명했습니다.
- 시간이 없었습니다. 서비스별로 독립적으로 짜고 따로 배포하는 게 당장 빨랐습니다.
- 관리가 편했습니다. 하나 고치면 하나만 올리면 됐습니다.
- 새 기술 도입이 쉬웠습니다. 서비스마다 다른 스택을 자유롭게 얹을 수 있었습니다.
분산은 합리적인 선택이었습니다. 단, 그 합리성은 “사람이 나눠 맡는다”를 전제로 한 계산이었습니다.
AI 시대가 바꾼 한 가지 — 맥락의 비용
최근 몇 달, 배포와 쿠버네티스 운영, UI/UX 개선은 예전보다 훨씬 편해졌습니다. AI를 붙이면서 혼자서도 손이 닿는 범위가 넓어졌습니다.
문제는 기술이 너무 빨리 바뀐다는 거였습니다. 새로 나온 걸 따라가 코드를 최신화하려는데, 코드가 N개의 레포로 흩어져 있으니 한 번에 손을 못 댔습니다. 한 군데 바꾸면 다른 군데가 따라오지 않았습니다.
더 본질적인 건 이겁니다. AI에게 개발을 맡기려면 맥락을 줘야 하는데, MSA는 그 맥락이 N조각으로 쪼개져 있습니다. 서비스 하나 띄우고, 옆 레포를 설명하고, 둘 사이 의존성을 알려주고… AI가 일을 시작하기도 전에 제 저녁이 다 갔습니다.
분산은 사람이 나눠 맡을 때 정답이었습니다. 협업자가 AI로 바뀌니, “한 맥락 안에 다 있는 것”의 가치가 역전됐습니다.
결정: 모노레포 ≠ 모놀리스
오해부터 막아두겠습니다. 마이크로서비스를 버린 게 아닙니다. 런타임은 여전히 분리돼 있습니다 — 서비스는 각자 독립적으로 배포됩니다. 합친 건 아키텍처가 아니라 레포입니다.
목표는 두 개였습니다.
- AI에게 단일 맥락을 준다. 한 레포 안에 전부 있으면, 의존성도 규칙도 한 번에 읽힙니다.
- 환경 설정을 원샷으로 만든다. 피곤한 저녁에도 두 줄이면 전체가 떠야 합니다.
한 줄로 줄이면 이렇습니다. “MSA는 유지하되, 코드는 한곳에 모은다.”
4주간 사부작사부작 한 것
모노레포 골격. pnpm workspace로 apps/ 아래 11개 서비스를 한 레포에 모았습니다. 프런트 3종(Next.js · Quasar/Vue · Vite/React)과 백엔드/워커가 한 곳에서 삽니다.
버전 단일 진실원. nvm을 버리고 mise로 갈아탔습니다. mise.toml이 Node 26과 pnpm 버전을 못 박습니다. “제 노트북에선 되는데”를 구조적으로 제거했습니다.
원샷 부트스트랩. pnpm bootstrap:<target>이 DB·마이그레이션·시드·.env를 멱등하게 깔고, pnpm up:<target>이 mprocs TUI 한 화면에 전체 스택을 띄웁니다. git clone 후 두 줄이면 끝입니다. 이게 사실상 이번 이주의 핵심 산출물입니다.
재현성 트릭. docker compose 프로젝트명을 caor-mono로 고정했습니다. 그러면 네트워크 이름이 클론한 폴더와 무관하게 항상 같아서, 문서에 박아둔 --network 명령이 깨지지 않습니다.
레거시는 죽이지 않습니다. main-api와 몇몇 서비스는 PHP + Laravel 그대로입니다. 모노레포에는 git 서브모듈로 매달았습니다. 한 번에 갈아엎는 대신 작은 것부터 옮기는 스트랭글러 방식입니다. 돌아가는 걸 굳이 멈춰 세울 이유가 없습니다.
그래도 옮길 건 옮깁니다. MSA로 흩어져 있던 Laravel 기반 API 서버 몇몇은 NestJS로 포팅했습니다. 이유는 DTO와 검증(validation)을 원소스로 두기 위해서입니다. PHP로는 프런트(TypeScript)와 타입·검증 규칙을 공유할 수 없지만, NestJS로 옮기면 DTO와 validation을 한 곳에서 한 번만 정의해 클라이언트까지 재사용할 수 있습니다. 모노레포가 ‘같은 언어, 같은 맥락’이 되는 또 하나의 이유입니다.
AI가 읽을 맥락을 코드 옆에 둡니다. AGENTS.md에 서비스 의존성 맵과 프록시 규칙을 박아뒀고, 의존성 그래프는 Graphify로 커밋 자산화해서 AI가 raw 소스를 뒤지기 전에 그래프에 먼저 질의하게 했습니다. MCP로는 운영 k8s 클러스터를 readonly로, DB는 로컬 개발 환경만 연결했습니다.
작은 서비스부터 포팅 → 재배포 → 터진 문제 수습. 큰 main-api는 그대로 두고, 옮기기 쉬운 것부터 모노레포 안으로 끌어들였습니다.
거기서 터진 문제들 — 실측의 묘미
포트 충돌. 서비스들이 죄다 같은 기본값(예: 3000, 8000)을 물고 있었습니다. 그래서 서비스마다 고유 포트를 하나씩 배정하는 컨벤션 표를 만들어 정리했습니다(예: 30xx 대역으로 서비스별 분리). 코드 fallback이 기본값이라, env에서 포트를 강제로 지정해야 했습니다.
프록시 함정. 같은 /api prefix가 앱마다 다른 서비스를 가리킵니다. partner/customer의 /api는 레거시 main-api인데, admin의 /api는 admin-api입니다. AI가 prefix만 보고 엉뚱한 서비스로 추론하길래, 이걸 AGENTS.md의 “가장 중요한 규칙”으로 못 박았습니다. AI에게 맥락을 준다는 건 결국 이런 함정을 글로 남겨두는 일입니다.
시드 멱등성. dev 시드를 몇 번을 돌려도 깨지지 않게 고쳤습니다. 반자동 루틴에선 멱등이 생명입니다 — 멱등하지 않으면 매번 사람이 들여다봐야 하니까요.
레거시 PHP 버전까지 mise로. 서브모듈로 매단 레거시 image-api는 mise로 PHP 버전까지 고정하고 자동 기동 스크립트를 붙였습니다. 레거시라고 환경이 제멋대로면 원샷이 깨집니다.
아직 안 끝났다, 그게 정상
main-api는 여전히 PHP/Laravel입니다. 전부 옮길 생각도 아직 없습니다. 특히 main-api처럼 의존하는 곳이 많은 API는 섣불리 포팅하면 터질 위험이 큽니다. 그래서 이런 API는 e2e 테스트와 시나리오 테스트를 먼저 깔아 동작을 고정한 뒤 포팅할 계획입니다. 모노레포는 ‘완성’이 아니라 ‘바닥’입니다.
달라진 건 이겁니다. 이제 AI에게 맥락을 한 번에 줄 수 있고, 환경은 두 줄로 뜹니다. 저녁 9시의 멈춘 머리로도 일이 굴러갈 조건이 생겼습니다. 거창한 리아키텍처가 아니라, 피곤한 사람이 계속 일할 수 있게 만드는 작업이었습니다.
비슷한 처지라면
소수 팀이고, 저녁마다 지치고, 그래도 AI에 위임해서 뭔가를 굴리고 싶은 분에게.
MSA를 버릴 필요는 없습니다. 흩어진 레포를 모으고, 환경을 원샷으로 만들고, AI가 읽을 맥락 문서를 코드 옆에 두는 것 — 거기서부터입니다.
분산의 비용은 사람을 기준으로 계산됐습니다. 협업자가 AI로 바뀌었다면, 그 계산을 처음부터 다시 해야 합니다.