Files
2nd/10_Wiki/Topics/Coding/Arch_Modular_Monolith.md
T
2026-05-09 22:47:42 +09:00

7.8 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
arch-modular-monolith Modular Monolith — microservice 의 대안 Coding draft B conceptual 2026-05-09 2026-05-09
architecture
modular-monolith
vibe-coding
language applicable_to
TS / generic
Architecture
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)

// 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

// 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 분리

-- 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 간 통신: 직접 호출

// 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)

// 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)

// 같은 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

// 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

@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 분리 길.

🔗 관련 문서