본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 08. 08:28

모노레포 시대의 디렉터리 아키텍처

요약

tacosDB 프로젝트를 사례로 AI 에이전트와 협업하기 최적화된 모노레포 디렉터리 구조를 소개합니다. 프로젝트의 사양, 설계, 태스크를 명확히 분리하여 관리함으로써 유지보수성과 AI 개발 효율을 높이는 아키텍처를 제안합니다.

핵심 포인트

  • AI 에이전트를 위한 AGENTS.md 및 설계 중심의 루트 디렉터리 구성
  • README는 입구 역할만 수행하고 상세 사양은 spec/ 폴더로 분리
  • 목적에 따라 Astro(Web)와 Next.js(CMS)를 분리한 TypeScript 앱 구조
  • Rust 기반 백엔드의 API와 모듈 간 명확한 책임 분리

여러분 「타코스(Tacos)」 드시고 계신가요?

안녕하세요, tacosDB 개발 책임자입니다.

타코스를 너무 좋아해서 타코스에 특화된 사이트 「tacos DB」를 개발하고 있습니다.

기존의 맛집 사이트에서는 찾기 어려운, 또띠아(Tortilla)나 속재료 등의 조건으로 타코스 가게를 찾을 수 있게 하고 싶다는 것이 개발의 계기입니다.

지난번에는 PR(Pull Request)마다 프리뷰 환경이 구축되는 CI/CD 구성에 대해 썼습니다.

이번에는 지난 기사에서 조금 언급했던 리포지토리·디렉터리 구성에 대해 정리합니다.

tacos DB는 공개 Web, CMS, API, DB migration, 인프라 정의까지 동일한 리포지토리에서 관리하고 있습니다.

다만 하나의 리포지토리에 전부를 두는 것이 아니라, 모노레포(Monorepo) 안에서 책임을 명확히 나누는 구성으로 하고 있습니다.

이 기사에서는 AI와 함께 개발하는 것을 전제로 한 모노레포 시대의 디렉터리 아키텍처에 대해 쓰겠습니다.

영역기술
BackendRust / Cloudflare Workers
...
현재의 큰 틀은 다음과 같습니다.
tacos/
├── AGENTS.md
├── DESIGN.md
...

루트(Root) 직하에는 프로젝트의 입구가 되는 것들만 배치하고 있습니다.

위치역할
README.md셋업 및 주요 커맨드
AGENTS.mdAI 에이전트용 작업 규칙
DESIGN.mdUI 디자인 방침의 source of truth
spec/사양, ER, API, 화면, 운용 방침
tasks/태스크 정의 및 이력
src/구현
terraform/Cloudflare resource 관리
.github/workflows/CI/CD

README는 입구 쪽으로 모아두었습니다. 사양이나 설계 판단까지 README에 넣으면 어떤 정보가 정답인지 알기 어려워지기 때문입니다.

상세 내용은 spec/에 두고, README에서 찾아갈 수 있는 형태로 만들었습니다.

TypeScript 측은 공개 Web과 CMS를 별도의 앱으로 분리했습니다.

src/ts/
├── tacos-web/
└── tacos-cms/
역할채택 기술
tacos-web점포 목록, 점포 상세, 에리어 페이지, JournalAstro / React island
tacos-cms점포 관리, 이미지 관리, member 관리, Blog 기사 관리Next.js / OpenNext

공개 Web은 검색 유입과 초기 표시를 중시합니다. 점포 목록이나 점포 상세는 가능한 한 가볍게 표시되는 것이 중요합니다. 그렇기 때문에 Astro를 채택하였고, 지도나 필터링 같은 동적 UI만 React island로서 구현하고 있습니다.

CMS는 로그인 후의 관리 UI입니다. 폼(Form), 목록, 편집, 이미지 upload, 권한 관리가 중심이 됩니다. 그렇기 때문에 Next.js를 채택하고 있습니다.

같은 TypeScript라도 사용자(User)와 목적이 다르기 때문에 앱을 나누었습니다. 이를 통해 공개용 UI와 관리용 UI가 동일한 components/에 섞이지 않도록 하고 있습니다.

백엔드는 src/rs/tacos-api-server에 모아두었습니다.

src/rs/tacos-api-server/
├── Cargo.toml
├── migrations/
...

크게 나누면 apimodules가 있습니다.

디렉터리역할
crates/api/client-api공개 Web용 Worker의 HTTP entrypoint
crates/api/cms-apiCMS용 Worker의 HTTP entrypoint
crates/modules/item매장 도메인
crates/modules/blogJournal 기사 도메인
crates/modules/media이미지 upload / R2 / metadata
crates/modules/membermember / session / RBAC
crates/modules/shared정말로 공유가 필요한 타입이나 보조 처리

api crate는 HTTP의 입구와 DI (Dependency Injection)에 집중되어 있습니다. 비즈니스 로직은 modules 측에 둡니다.

공개 API에서 호출되는지, CMS API에서 호출되는지는 외부의 사정입니다. 매장 정보의 정합성이나 공개 상태의 처리는 item module의 책임이며, 기사의 공개나 초안 관리(draft management)는 blog module의 책임입니다.

각 domain module은 기본적으로 동일한 레이어(layer) 구성으로 되어 있습니다.

crates/modules/item/src/
├── domain/
│ ├── model/
...
레이어역할
domainaggregate, entity, value object, repository trait
usecase애플리케이션 조작, 입력/출력 DTO, query service 계약
handlerHTTP request 파싱, response 렌더링, error mapping
infrastructureD1 / R2 / SQL / repository 구현

의존성 흐름은 다음과 같이 설정했습니다.

handler는 HTTP의 사정을 다룹니다. usecase는 애플리케이션의 흐름을 다룹니다. domain은 비즈니스 규칙을 다룹니다. infrastructure는 D1이나 R2와 같은 외부 서비스의 상세 내용을 다룹니다.

이렇게 구조를 잡아두면 AI에게도 "이번에는 usecase만", "SQL은 infrastructure에 둘 것", "domain에 HTTP payload를 넣지 말 것"과 같이 지시할 수 있습니다.

모듈러 모놀리스 (Modular Monolith)에서는 디렉터리를 나누는 것만으로는 불충분합니다.

module 간에 무엇이든 직접 import 할 수 있게 되면 경계가 금방 모호해집니다.

tacos DB에서는 백엔드 module 간 통신에 대해 다음과 같은 규칙을 적용하고 있습니다.

  • 동일 module 내에서는 handler -> usecase -> repository trait -> infrastructure 흐름을 따름
  • 동일 module 내의 호출에 client를 끼워 넣지 않음
  • 다른 module의 usecase를 직접 import 하지 않음
  • 다른 module과 연계할 때는 공개된 HTTP / client 계약을 경유함
  • shared에는 정말로 여러 module에서 공유가 필요해진 것만 배치함

예를 들어, blog module에서는 기사의 작성자(writer) 정보를 다룹니다. 따라서 향후에 member 정보가 필요하게 됩니다.

하지만 blog에서 member module의 usecase를 직접 import 하지 않는 방침을 유지하고 있습니다. 직접 import 하면 blog가 member의 내부 구조에 의존하게 되기 때문입니다.

또한, Journal 기사에서 매장을 참조하는 기능을 넣을 경우에는 blog에서 item으로의 의존도 발생합니다. 이 경우에도 item의 usecase를 직접 import 하는 것이 아니라, 공개된 계약을 통해 연계하는 것을 상정합니다.

module 간은 계약으로 연결하고 내부 구현에는 손대지 않는다. 이를 모노레포(monorepo) 안에서도 지키도록 하고 있습니다.

tacos DB에서는 Cloudflare D1을 사용하고 있습니다.

매장, 기사, 이미지, member 등의 테이블(table)은 동일한 D1에 들어 있습니다. 다만, SQL의 위치는 module별 infrastructure에 국한되어 있습니다.

crates/modules/item/src/infrastructure/d1_repo.rs
crates/modules/blog/src/infrastructure/d1_repo.rs
crates/modules/media/src/infrastructure/d1_repo.rs
...

동일한 DB를 사용하더라도, 모든 모듈 (module)이 자유롭게 모든 테이블 (table)을 건드리는 구성으로 만들지는 않았습니다.

DB 테이블 (table)을 경계로 삼아버리면, 도메인 (domain)의 언어가 아니라 테이블 (table)의 편의에 맞춰 코드를 작성하게 됩니다. 따라서 D1 row struct, SQL, 리포지토리 (repository) 구현은 infrastructure에 두고, 도메인 (domain)이나 유스케이스 (usecase)에서 SQL의 상세 내용을 보지 않도록 하고 있습니다.

이러한 분리 방식을 통해 DB는 공유하면서도, 애플리케이션 측의 책임은 모듈 (module) 단위로 유지할 수 있습니다.

모노레포 (monorepo)에서는 공통 처리를 shared에 모으고 싶어집니다.

하지만 너무 이른 단계에서 공통화하면, 의미가 다른 것들까지 동일한 타입 (type)으로 취급하게 됩니다.

예를 들어, 같은 String이라도 ShopIdArticleId는 의미가 다릅니다. tacos DB에서는 의미를 가진 식별자를 값 객체 (value object)로서 모듈 (module) 측에 가깝게 배치했습니다.

ShopId
ShopSlug
AreaId
...

shared는 여러 모듈 (module)이 동일한 계약 (contract)을 공유해야 할 필요가 생겼을 때만 사용합니다.

먼저 거대한 공통 계층을 만드는 것이 아니라, 중복되는 의미가 일치하게 되었을 때 공통화하는 방침입니다.

여기까지가 tacos DB의 Rust 백엔드 내부 구조입니다.

crates/api에 엔트리포인트 (entrypoint)를 두고, crates/modules에 도메인 모듈 (domain module)을 둡니다. 각 모듈 (module)의 내부는 domain / usecase / handler / infrastructure로 통일합니다.

이 구성을 채택한 이유는 서비스를 잘게 나누고 싶어서가 아닙니다.

tacos DB는 개인 개발 프로젝트입니다. 공개 웹 (Web), CMS, API, DB, 이미지 관리, 배포까지 포함되어 있지만, 현재 규모에서 마이크로서비스 (microservices)로 나눌 필요는 없습니다.

마이크로서비스 (microservices)로 전환하면 서비스 간 통신, 인증, 배포 단위, 모니터링, 장애 조사 등 애플리케이션 본체 이외의 설계 및 운영 비용이 커집니다.

현재 tacos DB에서 원하는 것은 서비스 분할이 아닙니다.

원하는 것은 다음과 같은 상태입니다.

  • 하나의 PR로 Web / CMS / API / DB / infra의 변경 사항을 추적할 수 있음
  • 하나의 리포지토리 (repository)에서 사양과 구현을 통합 관리할 수 있음
  • 로컬 확인이나 CI의 진입점을 통일할 수 있음
  • 단, 변경 범위나 책임은 모호하게 하지 않음

그렇기 때문에 리포지토리 (repository)는 하나로 합쳐두었습니다.

한편, 모노레포 (monorepo) 안에서 앱이나 도메인 모듈 (domain module)의 경계는 명확히 합니다. 서비스로 나누지는 않지만, 코드상의 책임은 나눕니다. 이것이 현재 tacos DB에서 채택하고 있는 모듈러 모놀리스 (modular monolith)의 위치 설정입니다.

모듈러 모놀리스 (modular monolith)를 사용하는 이유는 AI와 함께 개발할 때 궁합이 좋다고 느끼기 때문입니다.

이유는 크게 3가지가 있습니다.

  • 책임이 명확해짐
  • 리뷰가 용이해짐
  • AI에 대한 지시가 간단해짐

AI 에이전트 (AI agent)에게 "매장 목록 API를 수정해줘"라고 요청했을 때, item 모듈 (module)이 있다면 먼저 살펴봐야 할 곳을 좁힐 수 있습니다.

handler, usecase, domain, infrastructure가 나누어져 있다면, 어느 계층을 건드려야 하는지도 전달하기 쉬워집니다.

리뷰를 할 때도 마찬가지입니다.

SQL이 handler에서 나타나면 위치가 잘못되었다고 판단할 수 있습니다. HTTP 응답 (response)의 편의성이 domain에 들어오면, 외부의 사정이 유출되었다고 판단할 수 있습니다.

인간에게 이해하기 쉬운 경계는 AI에게도 이해하기 쉬운 경계가 됩니다.

이전 같았으면, 이 정도 규모의 개인 개발에서 클린 아키텍처 (Clean Architecture)나 모듈러 모놀리스 (modular monolith)를 도입하는 것은 다소 무거운 결정이었을 것이라고 생각합니다.

레이어를 나누면 파일 수가 늘어납니다. DTO, repository trait, infrastructure 구현, request / response 변환 등도 필요하게 됩니다.

매번 수작업으로 모든 것을 작성한다면 오버엔지니어링 (over-engineering)이 되기 쉽습니다.

하지만 AI 에이전트 (AI agent)를 전제로 한다면 이 비용 체감이 달라집니다.

AI는 정해진 형태의 구현을 빠르게 내놓는 데 능숙합니다.

예를 들어 다음과 같은 작업은 AI에게 맡기기 쉽습니다.

  • 기존 module과 동일한 레이어 구성으로 새로운 usecase 추가하기
  • request DTO와 response DTO를 기존 명명 규칙에 맞추기
  • repository trait에 method를 추가하고, D1 구현에 반영하기
  • 기존의 handler / usecase / infrastructure 흐름에 따라 endpoint 늘리기
  • spec과 구현의 차이점을 보며 리뷰하기

즉, 기존에는 도입 비용이 무겁게 느껴졌던 구성이라도, AI의 출력 속도 덕분에 부담이 줄어듭니다.

그 결과, 이전 같았으면 이 정도 규모에서는 채택하지 않았을 설계라도, 책임의 명확화, 리뷰의 용이성, AI에 대한 지시의 용이성 같은 장점만을 취하기 쉬워졌다고 느낍니다.

이 부분이 이번 글에서 가장 쓰고 싶은 내용입니다.

디렉터리 구성은 src/ 뿐만이 아닙니다.

tacos DB에서는 사양(specification)과 작업 이력도 리포지토리 내에서 관리하고 있습니다.

spec/
├── README.md
├── overview.md
...

spec/에는 사양이나 설계 판단을 둡니다. tasks/에는 태스크 이력을 남깁니다.

코드만으로는 왜 그런 설계를 했는지 남기기 어렵습니다. 예를 들어 "다른 module의 usecase를 직접 import하지 않는다"라는 규칙은 구현뿐만 아니라 spec/tech/backend_coding_rules.md에도 적어두고 있습니다.

완료된 태스크 본문은 나중에 다시 쓰지 않고, 추가 변경 사항이 있으면 새로운 태스크를 생성하는 방식으로 운영합니다.

tacos DB에서는 Codex 등의 AI 에이전트를 개발에 사용하고 있습니다.

AI 에이전트에게도 디렉터리 구성은 중요합니다. 어디에 무엇이 있는지, 어느 계층을 건드려야 하는지가 명확하면 변경 범위를 좁히기 쉬워집니다.

그렇기 때문에 repo-local skill을 .agents/skills/에 두고 있습니다.

.agents/skills/
├── tacos-backend-architecture/
│ └── SKILL.md
...

tacos-backend-architecture에는 이 리포지토리 고유의 module 경계나 Clean Architecture 규칙을 적어둡니다.

rust-programmer에는 Rust 일반의 API guideline, error handling, ownership, async 등을 적어둡니다.

프로젝트 고유의 규칙과 Rust 일반 규칙을 분리함으로써, AI 에이전트에게 전달할 전제 조건도 정리하기 쉬워졌습니다.

이 구성으로 바꾸어 좋았던 점은 다음과 같습니다.

  • README가 입구로서 사용하기 쉬워졌다
  • 사양의 정본(source of truth)을 spec/에서 찾아갈 수 있게 되었다
  • Worker crate를 얇게 유지할 수 있게 되었다
  • item / blog / media / member의 변경 범위를 나누기 쉬워졌다
  • SQL이나 HTTP response의 위치를 판단하기 쉬워졌다
  • shared로의 과도한 공통화를 피하기 쉬워졌다
  • AI 에이전트에게 "어디를 보고 어디를 건드리지 말아야 하는지" 지시하기 쉬워졌다

한편으로는 처음부터 이런 형태였던 것은 아닙니다.

README에 정보를 너무 많이 모으거나, 미래를 위한 module의 의도가 잘 전달되지 않거나, 공통화의 입도(granularity)로 고민하기도 했습니다.

그때마다 spec/이나 AGENTS.md, repo-local skill에 규칙을 반영하여 다음에는 헤매지 않도록 하고 있습니다.

이 글에서 쓰고 싶었던 것은 모노레포 (monorepo)와 AI 개발은 궁합이 좋지 않을까 하는 이야기입니다.

다만, 하나의 리포지토리에 모든 것을 두기만 해서는 변경 범위가 너무 넓어집니다.

그렇기에 모노레포 안에서 디렉터리를 통해 경계를 만듭니다.

서비스의 규모감 측면에서는 마이크로서비스 (Microservices)로 만들지는 않습니다. 반면, AI와 함께 개발하기 위해 책무 (Responsibility)는 명확하게 나눕니다.

이번에 소개한 방침을 정리하면 다음과 같습니다.

  • README는 입구 쪽에 배치한다
  • 사양 (Specification)은 spec/에 모은다
  • 공개 Web과 CMS는 별도의 앱으로 분리한다
  • 백엔드 (Backend)는 Rust Workspace로 통합한다
  • Worker crate는 HTTP entrypoint와 DI (Dependency Injection)에 가깝게 구성한다
  • 비즈니스 로직 (Business Logic)은 crates/modules/*에 둔다
  • module 내부는 domain / usecase / handler / infrastructure 구조로 통일한다
  • module 간에는 직접 usecase를 import 하지 않는다
  • DB는 공유하더라도 SQL은 module의 infrastructure 내로 한정한다
  • shared는 작게 유지한다
  • spec, tasks, AGENTS.md, repo-local skill까지 포함하여 구성을 설계한다

이전이라면 작은 프로덕트에는 무겁게 느껴졌을 구성이라도, AI의 구현 속도 덕분에 도입 비용이 낮아졌습니다.

그 결과, 책무의 명확화, 리뷰의 용이성, AI에 대한 지시의 용이성이라는 장점을 작은 규모에서도 누릴 수 있게 되었다고 느낍니다.

릴리스한 tacos DB는 아직 게재 점포 수를 늘려가는 중입니다.

점포 정보, 사진, 소개글을 제공해 주시는 분이나, 타코스를 좋아해서 함께 만들어가 주실 분들을 모집하고 있습니다.

알고 있는 가게가 있다면, 꼭 문의 양식을 통해 게시해 주세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0