--- id: arch-modular-monolith title: Modular Monolith — microservice 의 대안 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [architecture, modular-monolith, vibe-coding] tech_stack: { language: "TS / generic", applicable_to: ["Architecture"] } applied_in: [] aliases: [modular monolith, modulith, single deployable, package-based, well-defined modules] --- # Modular Monolith > Microservice 가 default 가 아님. **모듈화 + 단일 deploy = simple + scalable**. Shopify, Basecamp, GitHub 가 거대한 모놀리스. "Microservice premium" 회피. ## 📖 핵심 개념 - 1 deploy unit, 여러 module. - 모듈 간 명시적 boundary. - 같은 process, 다른 namespace. - DB 가 module 별 schema. ## 💻 코드 패턴 ### 폴더 구조 ``` src/ ├── modules/ │ ├── orders/ │ │ ├── domain/ │ │ ├── application/ │ │ ├── infrastructure/ │ │ ├── api/ # HTTP handler │ │ └── index.ts # module 의 public │ ├── users/ │ ├── inventory/ │ └── billing/ ├── shared/ # 진짜 공유 └── main.ts ``` ### Module index (public API) ```ts // modules/orders/index.ts export { OrderService } from './application/order.service'; export { Order } from './domain/order'; // 기타 = private (export X) // modules/users/application/user.service.ts import { OrderService } from '../../orders'; // ✅ public import { Order } from '../../orders'; // ✅ public import { internal } from '../../orders/domain/secret'; // ❌ ``` ### TypeScript module boundary ```ts // nx / turbo + tsconfig path { "compilerOptions": { "paths": { "@/orders": ["src/modules/orders"], "@/users": ["src/modules/users"] } } } // eslint-plugin-boundaries { "rules": { "boundaries/element-types": ["error", { "default": "disallow", "rules": [ { "from": "users", "allow": ["orders/index"] } ] }] } } ``` ### DB schema 분리 ```sql -- Postgres schema CREATE SCHEMA orders; CREATE SCHEMA users; CREATE SCHEMA inventory; CREATE TABLE orders.orders (...); CREATE TABLE users.users (...); -- Cross-schema query 거의 X -- 직접 query 가 module 내부만 ``` ### Module 간 통신: 직접 호출 ```ts // orders 의 service 가 users 호출 class OrderService { constructor(private userService: UserService) {} async place(orderData) { const user = await this.userService.get(orderData.userId); if (!user.canOrder()) throw ...; // ... } } ``` → Function call. Network 없음. 빠름. ### Module 간 event (decouple) ```ts // orders 가 event publish class OrderService { async place(...) { const order = await this.repo.save(...); eventBus.emit('order.placed', { orderId: order.id, userId: ... }); return order; } } // users 가 listen eventBus.on('order.placed', async (e) => { await usersService.recordActivity(e.userId); }); ``` → In-process event bus. Microservice 와 같은 pattern, network 없음. ### Transaction (cross-module) ```ts // 같은 DB transaction 가능 (microservice 와 다른 큰 강점) await db.transaction(async (tx) => { await ordersService.place(tx, orderData); await inventoryService.reserve(tx, items); await billingService.charge(tx, payment); }); ``` → Microservice = saga 필요. Modular monolith = 1 transaction. ### 분리 layer ``` Strict (compile-time): - Module index 만 export - Linter rule - Folder structure Loose: - Convention 만 - Code review → Strict = 큰 팀. ``` ### Spring (Java) Modulith ```java // org.springframework.modulith @Modulith(systemName = "MyApp") @SpringBootApplication public class Application { ... } // modules/orders/Order.java (public) public class Order { ... } // modules/orders/internal/OrderRepo.java (internal) package modules.orders.internal; class OrderRepo { ... } ``` → Spring 가 module 의 first-class. ### NestJS module ```ts @Module({ imports: [TypeOrmModule.forFeature([Order])], providers: [OrderService], controllers: [OrderController], exports: [OrderService], // 다른 module 가 import 가능 }) export class OrdersModule {} // AppModule @Module({ imports: [OrdersModule, UsersModule], }) export class AppModule {} ``` ### .NET / C# class library ``` Solution ├── MyApp.Orders/ # class library ├── MyApp.Users/ ├── MyApp.Inventory/ └── MyApp.Web/ # entry ``` → Csproj 가 dependency 정의 — circular X. ### Migration to microservices (later) ``` Modular monolith 의 큰 장점: "필요 시" 1 module → service 분리 가능. 순서: 1. Module 가 명확 2. 그 module 만 별 process 3. In-process event → message queue 4. DB schema → 분리 DB 5. Service 가 됨 거꾸로 안 됨 (microservice → monolith 어려움). ``` ### 단점 인지 ``` - Scale 가 process 단위 (1 module 만 scale X) - Deploy 가 1 모놀리스 (작은 변경 도 전체 deploy) - 1 bug 가 전체 down 가능 - 큰 codebase = build / test 시간 ↑ → 100 dev 이상 = microservice 고려. < 100 dev = modular monolith 유리. ``` ### "Microservice premium" ``` Microservice 의 cost: - Network latency - Distributed tracing - Saga / eventual consistency - Service discovery - Independent deploy pipeline - Multiple DB - Multi-team coordination → 작은 팀 = 큰 cost. 큰 가치 안 옴. ``` → Sam Newman 의 "Building Microservices" 도 modular first 권장. ### Independent deployable (큰 팀) ``` 주 release schedule + emergency hotfix. Modular monolith = 1 deploy. 긴 release cycle = 큰 변경 누적 = risk. → 매일 deploy 가능 = OK. 주 1회 = bottleneck. ``` ### Test ``` Module 별 test (unit + integration). Cross-module test = E2E (전체 app). → Microservice 의 contract test 불필요. ``` ### Build / CI ``` Nx / Turbo 가 affected build. └─ orders 변경 → orders test 만. Cache 친화 + 작은 PR 빠름. ``` ### Logger / monitoring ``` log.info('order.placed', { module: 'orders', orderId }); → Module field 가 filter 친화. Datadog / Grafana. ``` ### Rate limit / circuit breaker ``` Microservice 에서 와는 달리 module 간 직접 call. 하지만 외부 API 호출 시 circuit breaker. ``` ### Famous 예 - **Shopify**: Rails monolith + 모듈 (component) — engine. - **Basecamp**: Rails monolith. - **GitHub**: Rails monolith + 일부 service. - **StackOverflow**: ASP.NET monolith (전 세계 traffic). ### When go microservice ``` - 100+ dev (조직) - 매우 다른 scaling 필요 (1 part 가 100x traffic) - 다른 stack (legacy + new + ML) - 매 일 100+ deploy → Default 는 modular monolith. ``` ### Hybrid: Citadel ``` 1 큰 monolith (대부분 logic) + 1-2 special service. 예: Monolith + ML inference service (GPU 필요). → Best of both. ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | < 100 dev | Modular monolith | | 1 codebase OK | Modular monolith | | 다른 scaling 필요 | 1 service 분리 (citadel) | | 100+ dev | Microservice | | Different stack | Microservice | | Strict module 가 어려움 | Strict tooling (nx + lint) | | Migration 가능성 | Modular first | ## ❌ 안티패턴 - **Big ball of mud** (no module): 분리 안 됨. - **너무 많은 module** (모든 file = module): 의미 X. - **Cross-module DB query**: schema 분리 위반. - **Circular dep**: build 깨짐. - **모든 모듈 = service 자동**: not always. - **Linter 없음**: 시간 따라 boundary 흐림. - **Module 별 stack**: 큰 monolith 가 망함. ## 🤖 LLM 활용 힌트 - Modular monolith 가 default. Microservice 가 last resort. - Module 의 명시적 boundary (linter / tsconfig). - DB schema 별 module. - 큰 팀 = 1 module → 1 service 분리 길. ## 🔗 관련 문서 - [[Arch_Module_Boundaries]] - [[Arch_Hexagonal_Clean]] - [[Backend_Multi_Tenant_Architecture]]