[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user