--- id: arch-module-boundaries title: Module Boundaries — Public API / 의존 관리 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [architecture, module, package, vibe-coding] tech_stack: { language: "TS", applicable_to: ["Backend", "Frontend"] } applied_in: [] aliases: [module boundary, public API, package, internal, dependency cruiser, layered] --- # Module Boundaries > Folder ≠ module. **Public API (index.ts) 만 export, 내부 implementation 가림**. ESLint / dependency-cruiser / project references 로 강제. Modular monolith → 미래 microservice 분리 쉬움. ## 📖 핵심 개념 - Public API: index.ts / barrel — 외부 사용 허용. - Internal: 외부 import 금지. - Acyclic: 순환 의존 X. - Layered: domain → app → infra (한 방향). ## 💻 코드 패턴 ### 폴더 구조 ``` src/ modules/ ordering/ index.ts # public API domain/ # internal application/ # internal infrastructure/ # internal catalog/ index.ts ... ``` ```ts // modules/ordering/index.ts export { CreateOrderUseCase } from './application/createOrder'; export { OrderRepository } from './ports/orderRepository'; export type { Order, OrderId } from './domain/order'; // internal 은 export X ``` ```ts // modules/billing/... import { CreateOrderUseCase, type Order } from '../ordering'; // ✅ public API 만 import { OrderEntity } from '../ordering/domain/order'; // ❌ internal — 차단 ``` ### ESLint — no-restricted-imports ```js // .eslintrc { "rules": { "no-restricted-imports": ["error", { "patterns": [ "*/modules/*/domain/*", "*/modules/*/application/*", "*/modules/*/infrastructure/*", ] }] } } ``` → 내부 import 시도 = lint 에러. ### dependency-cruiser ```js // .dependency-cruiser.cjs module.exports = { forbidden: [ { name: 'no-circular', severity: 'error', from: {}, to: { circular: true }, }, { name: 'no-cross-module-internals', severity: 'error', from: { path: '^src/modules/([^/]+)' }, to: { path: '^src/modules/(?!\\1)([^/]+)/(domain|application|infrastructure)' }, }, { name: 'domain-no-deps', severity: 'error', from: { path: '^src/modules/[^/]+/domain' }, to: { path: '^src/modules/[^/]+/(application|infrastructure)' }, }, ], }; ``` ```bash depcruise src --output-type err ``` ### TS project references ```jsonc // modules/ordering/tsconfig.json { "compilerOptions": { "composite": true, "rootDir": "src", "outDir": "dist" } } ``` ```jsonc // modules/billing/tsconfig.json { "references": [{ "path": "../ordering" }] } ``` → ordering 의 public API 만 type 노출. ### pnpm workspace (작은 monorepo) ```yaml # pnpm-workspace.yaml packages: - 'modules/*' ``` ```jsonc // modules/ordering/package.json { "name": "@app/ordering", "exports": { ".": "./src/index.ts" } } ``` ```ts // 다른 module import { CreateOrderUseCase } from '@app/ordering'; ``` → Module = npm package. 강제 boundary. ### Internal package (같은 monorepo, 외부 publish X) ```jsonc { "name": "@app/ordering", "private": true, "version": "0.0.0" } ``` ### Cross-module 통신 = event / interface ```ts // ordering 이 billing 직접 호출 금지 // 대신: // 1. Event // modules/ordering/domain/events.ts export class OrderPlaced { ... } // modules/billing/handlers.ts import { OrderPlaced } from '@app/ordering'; on(OrderPlaced, async (ev) => createInvoice(ev)); // 2. Or: ordering 이 BillingPort interface 정의 → composition root 가 wire export interface BillingPort { createInvoice(orderId: OrderId): Promise; } ``` ### Public API 변경 = breaking ```ts // 명시적 versioning 또는 careful change // Library처럼 semver ``` ### Visibility 강제 안 되는 언어 (TS) TS 는 internal 키워드 X. eslint / depcruise 가 대안. Java/Rust 는 `package` / `crate` 가 native. ### Modular monolith → microservice 추출 쉬움 ``` 1. Module 이 명확 boundary 2. 통신 = interface / event 3. Module 의 DB 분리 4. Module 을 별 service 로 분리 ``` → 시작은 monolith, 필요 시 분리. ### 단계 ``` Phase 1: 한 폴더 (chaos) Phase 2: layer 분리 (web, services, repositories) Phase 3: bounded context 분리 (modules/) Phase 4: package 별 (pnpm workspace) Phase 5: 별 service (microservice) ``` → 대부분 phase 3 으로 충분. ### Anemic boundary 함정 ```ts // boundary 약함 — 모두가 모두를 import import { db } from '../../db'; import { logger } from '../../logger'; import { config } from '../../config'; ``` → Shared kernel 작게 + 명시. ## 🤔 의사결정 기준 | 규모 | 추천 | |---|---| | <5 파일 | 한 폴더 OK | | <20 파일 | layer 분리 | | 큰 도메인 / 팀 | bounded context modules/ | | 큰 monorepo | package per module | | 분리 service 후보 | package + interface | | Internal lib | semver + private package | ## ❌ 안티패턴 - **Barrel index.ts 가 거의 모두 export**: 내부 노출 = boundary 의미 없음. - **순환 의존**: A → B → A. depcruise 로 차단. - **Cross-module deep import**: 내부 변경 시 깨짐. - **Shared 폴더 거대**: 모두가 의존 — module 분리 의미 없음. - **공유 DB schema**: data coupling. module 별 schema 또는 view. - **Strong type sharing**: type 도 boundary. shared types 작게. ## 🤖 LLM 활용 힌트 - index.ts barrel + 내부 차단 (eslint / depcruise). - 순환 의존 없음 항상. - Module = future service 후보. ## 🔗 관련 문서 - [[Arch_Hexagonal_Clean]] - [[Arch_DDD_Bounded_Context]] - [[TS_Monorepo_Patterns]]