웹서비스 내부 구조 아키텍처 가이드 Part 2: EDA, VSA, Modular Monolith
이 글은 Claude Opus 4.6 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
Part 1에서는 Layered Architecture, Ports & Adapters 계열(Hexagonal/Onion/Clean), CQRS를 다뤘습니다. Part 2에서는 나머지 세 아키텍처와 전체 비교를 다룹니다.
4. Event-Driven Architecture (EDA)#
개요#
Event-Driven Architecture는 시스템 구성 요소 간의 통신을 이벤트를 통해 수행 하는 설계 패턴입니다. 컴포넌트가 서로 직접 호출하는 대신, “무언가가 발생했다"는 사실을 이벤트로 발행하면, 관심 있는 컴포넌트가 이를 구독하여 반응합니다.
이 글의 전제가 Kafka/RabbitMQ 같은 메시지 브로커를 포함하는 환경이므로, EDA를 내부 구조 패턴으로 다루는 것은 매우 자연스럽습니다. 메시지 브로커가 있다는 것 자체가 이미 이벤트 기반 통신의 인프라를 갖추고 있다는 뜻이기 때문입니다.
핵심 개념#
EDA에는 세 가지 핵심 구성 요소가 있습니다.
- Event Producer: 상태 변화가 발생했을 때 이벤트를 발행하는 컴포넌트입니다. 예: 주문 서비스가
OrderCreated이벤트를 발행합니다. - Event Channel: 이벤트를 전달하는 중간 매개체입니다. Kafka topic, RabbitMQ exchange, 또는 애플리케이션 내부의 이벤트 버스가 될 수 있습니다.
- Event Consumer: 이벤트를 수신하여 반응하는 컴포넌트입니다. 예: 재고 서비스가
OrderCreated를 수신하여 재고를 차감합니다.
EDA의 주요 토폴로지는 두 가지입니다.
Mediator 토폴로지: 중앙 조정자가 이벤트를 받아 처리 단계를 조율합니다. 여러 단계를 거치는 복잡한 워크플로에 적합합니다. 예를 들어 주문 처리 시 “결제 확인 → 재고 차감 → 배송 요청"을 중앙에서 순서대로 조율합니다.
Broker 토폴로지: 중앙 조정자 없이, 이벤트가 메시지 브로커를 통해 관심 있는 소비자에게 전달됩니다. 각 소비자가 독립적으로 반응하므로 결합도가 낮습니다. 예를 들어 OrderCreated 이벤트에 대해 재고 서비스, 알림 서비스, 분석 서비스가 각각 독립적으로 반응합니다.
graph LR
subgraph "Event Producers"
OS["Order Service"]
PS["Payment Service"]
end
BROKER["Message Broker<br/>Kafka / RabbitMQ"]
subgraph "Event Consumers"
INV["Inventory Service"]
NOTI["Notification Service"]
ANAL["Analytics Service"]
end
OS -->|"OrderCreated"| BROKER
PS -->|"PaymentCompleted"| BROKER
BROKER --> INV
BROKER --> NOTI
BROKER --> ANAL
style BROKER fill:#FFD700,color:#000000
style OS fill:#90EE90,color:#000000
style PS fill:#90EE90,color:#000000
style INV fill:#87CEEB,color:#000000
style NOTI fill:#87CEEB,color:#000000
style ANAL fill:#87CEEB,color:#000000
Producer(초록색)는 이벤트를 발행만 하고, Consumer(푸른색)의 존재를 알 필요가 없습니다. 새로운 Consumer를 추가해도 Producer를 수정하지 않습니다.
Domain Event과의 관계#
DDD에서 말하는 Domain Event와 EDA의 이벤트는 같은 개념이 아닙니다. Domain Event는 도메인 모델 안에서 “비즈니스적으로 의미 있는 일이 발생했다"를 표현하는 것이고, EDA의 이벤트는 시스템 간 통신 메커니즘입니다.
하지만 실무에서 이 둘은 자연스럽게 연결됩니다. Domain Event가 발생하면 이를 메시지 브로커를 통해 다른 컴포넌트에 전파하는 패턴이 매우 흔합니다. 이때 주의할 점은 내부 Domain Event의 구조와 외부로 발행하는 Integration Event의 구조를 분리 하는 것입니다. 내부 이벤트는 도메인 모델의 세부사항을 포함할 수 있지만, 외부 이벤트는 소비자가 필요한 최소한의 정보만 담아야 합니다.
장점#
컴포넌트 간 결합도가 낮아집니다. Producer는 Consumer의 존재를 알 필요가 없습니다. 새로운 Consumer를 추가할 때 Producer를 수정하지 않아도 됩니다. 주문 생성 후 알림을 보내야 한다면, 주문 서비스를 수정하지 않고 알림 서비스를 새 Consumer로 추가하면 됩니다.
확장성이 좋습니다. Consumer를 독립적으로 스케일 아웃할 수 있습니다. 이벤트 처리량이 많은 Consumer만 인스턴스를 늘리면 됩니다.
비동기 처리가 자연스럽습니다. 즉시 완료되지 않아도 되는 작업(이메일 발송, 로그 기록, 통계 집계)을 이벤트로 위임하면, 사용자 응답 시간이 단축됩니다.
시간적 결합(temporal coupling)이 줄어듭니다. 동기 호출에서는 호출 대상이 다운되면 호출자도 실패합니다. 이벤트 기반에서는 메시지 브로커가 중간에서 버퍼 역할을 하므로, Consumer가 일시적으로 다운되어도 Producer는 영향을 받지 않습니다.
단점#
디버깅과 추적이 어렵습니다. 동기 호출에서는 콜 스택을 따라가면 흐름이 보입니다. 이벤트 기반에서는 이벤트가 어디로 전파되어 어떤 핸들러가 처리했는지 추적하기 어렵습니다. Correlation ID와 분산 트레이싱(OpenTelemetry 등)이 거의 필수적으로 필요합니다.
순서 보장이 복잡합니다. 이벤트의 발행 순서와 처리 순서가 일치하지 않을 수 있습니다. Kafka는 파티션 내 순서를 보장하지만, 파티션 간 순서는 보장하지 않습니다. 순서가 중요한 비즈니스 로직이라면 이를 명시적으로 설계해야 합니다.
Eventual consistency를 다뤄야 합니다. 이벤트 발행과 소비 사이에 시간차가 있으므로, 시스템의 모든 부분이 항상 일관된 상태를 보여주지 않습니다. 사용자 경험 측면에서 이를 어떻게 처리할지 설계해야 합니다.
이벤트 스키마 관리가 필요합니다. 이벤트 구조가 변경되면 모든 Consumer가 영향을 받을 수 있습니다. 이벤트 버전 관리 전략(스키마 레지스트리, 하위 호환 변경 규칙 등)이 필요합니다.
Choreography vs Orchestration#
이벤트 기반으로 여러 서비스를 조율할 때 두 가지 접근법이 있습니다.
Choreography: 각 서비스가 이벤트에 독립적으로 반응합니다. 중앙 조정자가 없으므로 결합도가 낮지만, 전체 워크플로를 파악하기 어렵습니다. 서비스 수가 적고 워크플로가 단순할 때 적합합니다.
Orchestration: 중앙 조정자(Saga orchestrator 등)가 워크플로를 관리합니다. 전체 흐름이 한 곳에서 보이지만, 조정자가 단일 장애점이 될 수 있습니다. 워크플로가 복잡하고 보상 트랜잭션(compensation)이 필요할 때 적합합니다.
실무에서는 둘을 혼합해서 사용하는 경우가 많습니다. 핵심 비즈니스 워크플로는 Orchestration으로, 부수적인 반응(알림, 로깅, 통계)은 Choreography로 처리하는 것이 흔한 패턴입니다.
적합한 상황#
- 여러 서비스/컴포넌트 간 비동기 통신이 필요한 시스템
- 하나의 비즈니스 이벤트에 여러 후속 처리가 연쇄적으로 필요한 경우
- 처리량이 불규칙하고 피크 시 버퍼링이 필요한 환경
- 새로운 Consumer를 Producer 변경 없이 추가해야 하는 시스템
부적합한 상황#
- 모든 처리가 동기적으로 즉시 완료되어야 하는 시스템
- 강한 일관성(strong consistency)이 모든 곳에 필수적인 경우
- 팀이 분산 트레이싱과 이벤트 디버깅 도구를 갖추지 못한 환경
- 이벤트 흐름이 2~3개 이하로 단순한 소규모 시스템
5. Vertical Slice Architecture (VSA)#
개요#
VSA는 앞의 아키텍처들과 결이 다릅니다. Jimmy Bogard는 VSA를 개별 요청(distinct request)을 중심으로 프런트엔드부터 백엔드까지의 관심사를 하나의 slice에 묶는 스타일 이라고 설명합니다. 핵심 문장은 이것입니다. 기술 계층을 따라 결합하지 말고, 변경 축(axis of change)을 따라 결합하라.
기존 아키텍처들이 “기술 책임별로 어떻게 나눌 것인가"를 묻는다면, VSA는 “이 기능을 변경할 때 어떤 코드가 함께 바뀌는가"를 묻습니다.
핵심 구조#
전통적 Layered Architecture의 폴더 구조는 기술 축으로 나뉩니다.
/Controllers
OrderController.cs
ProductController.cs
/Services
OrderService.cs
ProductService.cs
/Repositories
OrderRepository.cs
ProductRepository.cs
VSA는 기능 축으로 나뉩니다.
/Features
/Orders
/Create
CreateOrderCommand.cs
CreateOrderHandler.cs
CreateOrderValidator.cs
/Cancel
CancelOrderCommand.cs
CancelOrderHandler.cs
/Products
/Search
SearchProductsQuery.cs
SearchProductsHandler.cs
/UpdatePrice
UpdatePriceCommand.cs
UpdatePriceHandler.cs
각 slice에는 request/response 모델, 핸들러, validator, 매핑 설정 등 해당 기능에 필요한 모든 것이 가까이 모여 있습니다. Controller는 라우팅을 만족시키기 위한 얇은 껍데기일 뿐입니다.
graph TD
subgraph "Slice: 주문 생성"
E1["Endpoint"] --> H1["Handler"]
H1 --> V1["Validator"]
H1 --> D1["Domain Model + DB"]
end
subgraph "Slice: 상품 검색"
E2["Endpoint"] --> H2["Handler"]
H2 --> V2["Validator"]
H2 --> D2["Query + Cache"]
end
subgraph "Slice: 가격 변경"
E3["Endpoint"] --> H3["Handler"]
H3 --> V3["Validator"]
H3 --> D3["Domain + Event 발행"]
end
style E1 fill:#FFB6C1,color:#000000
style H1 fill:#FFB6C1,color:#000000
style V1 fill:#FFB6C1,color:#000000
style D1 fill:#FFB6C1,color:#000000
style E2 fill:#87CEEB,color:#000000
style H2 fill:#87CEEB,color:#000000
style V2 fill:#87CEEB,color:#000000
style D2 fill:#87CEEB,color:#000000
style E3 fill:#90EE90,color:#000000
style H3 fill:#90EE90,color:#000000
style V3 fill:#90EE90,color:#000000
style D3 fill:#90EE90,color:#000000
각 slice(색상별)가 독립적으로 Endpoint부터 데이터 접근까지 수직으로 관통합니다. Slice마다 내부 구현 방식이 다를 수 있습니다. 주문 생성은 Domain Model을 거치고, 상품 검색은 캐시를 직접 조회하며, 가격 변경은 이벤트를 발행합니다.
왜 이런 구조가 필요한가#
“상품 검색"과 “상품 가격 변경"을 예로 들면, 이 둘이 ProductService를 공유할 이유는 생각보다 많지 않습니다.
- 검색: 조인, 정렬, 페이징, 캐시, Elasticsearch 연동이 중요합니다.
- 가격 변경: 권한 검사, 도메인 규칙 검증, 이벤트 발행이 중요합니다.
전통적 layered 구조에서는 둘 다 ProductController → ProductService → ProductRepository 아래에 몰립니다. VSA는 이 강제적 공유를 의심합니다. Bogard는 전통적 계층 구조가 소수의 전형적 요청에만 잘 맞고, 현실에서는 많은 불필요한 추상화가 생긴다고 비판합니다.
CQRS와의 자연스러운 연결#
VSA로 구성하면 사실상 CQRS를 자연스럽게 얻게 됩니다. 각 slice가 Command 또는 Query 중 하나이기 때문입니다. CreateOrder는 Command slice이고, SearchProducts는 Query slice입니다. 각 slice가 자신에게 맞는 방식을 독립적으로 선택할 수 있으므로, Command slice는 도메인 모델을 거치고, Query slice는 DB를 직접 조회하는 것이 자연스럽습니다.
장점#
변경 비용이 국소화됩니다. 화면 필드 하나 추가, 검증 규칙 변경, 특정 유스케이스의 쿼리 최적화 같은 작업이 한 slice 근처에서 끝나는 경우가 많습니다. “공용 Service를 수정했더니 다른 화면이 깨졌다” 같은 부작용이 줄어듭니다.
slice마다 적합한 구현 방식을 선택할 수 있습니다. 어떤 slice는 transaction script에 가깝고, 어떤 slice는 rich domain model에 가깝게 구현할 수 있습니다. 전체 시스템에 하나의 패턴을 강제할 필요가 없습니다.
기능 간 의존성이 명시적입니다. Slice 간 공유가 기본값이 아니므로, 공유가 필요할 때 의식적으로 결정하게 됩니다.
단점과 실패 모드#
Bogard는 팀이 코드 스멜과 리팩터링을 이해하지 못하면 이 패턴은 적합하지 않다 고 분명히 말합니다. VSA는 공용 계층을 먼저 세우지 않는 대신, 언제 중복을 유지하고 언제 공통 도메인 개념으로 추출할지를 개발자가 스스로 판단해야 합니다.
Slice라는 이름만 붙이고 기존 구조를 복사하는 경우: Controller/Service/Repository를 폴더만 바꿔 넣으면 중복만 늘고 응집은 생기지 않습니다.
리팩터링 없이 복붙이 누적되는 경우: 같은 도메인 규칙이 여러 slice에 미묘하게 다르게 들어가게 됩니다. Bogard도 핸들러 로직이 복잡해지면 도메인 모델로 밀어 넣어야 한다고 말합니다.
모든 중복을 두려워해 너무 일찍 공용층을 만드는 경우: 그러면 VSA의 장점이 사라지고 기존 layered 구조로 회귀합니다.
VSA의 핵심은 폴더 구조가 아니라, 변경 단위에 맞춘 응집과 신중한 추출 입니다. “처음부터 모든 것을 공용 도메인 계층으로 만들지 말고, 필요가 드러날 때 추출하라"가 정확한 메시지입니다.
적합한 상황#
- 기능 변화가 빠르고 요청마다 구현 방식이 다른 제품 조직
- B2B SaaS, 내부 운영도구, 백오피스
- 조회와 변경의 성격이 크게 다른 시스템
- 팀이 리팩터링에 익숙한 조직
부적합한 상황#
- 도메인 개념이 매우 안정적이고 중앙 집중적 정책 모델이 핵심인 시스템
- 팀의 리팩터링 문화가 약한 조직 (공용 도메인 모델이 “늦게 생기는 것"이 아니라 “영원히 안 생기는 것"으로 끝날 위험)
- 코드 스멜을 읽고 적절히 추출할 역량이 부족한 팀
6. Modular Monolith#
개요#
Modular Monolith는 단일 배포 단위(monolith) 안에서 명확한 모듈 경계를 설정 하는 아키텍처입니다. 마이크로서비스의 모듈 경계와 독립성 이점을 원하지만, 분산 시스템의 운영 복잡성은 감수하고 싶지 않을 때 선택하는 구조입니다.
Shopify의 엔지니어링 팀은 Shopify의 핵심 모놀리스를 Modular Monolith로 전환하며 이 접근법을 공개적으로 공유했습니다. 그들은 마이크로서비스로의 분리가 항상 정답은 아니며, 모놀리스 안에서 모듈 경계를 강화하는 것만으로도 대부분의 문제를 해결할 수 있었다고 설명합니다.
핵심 구조#
단일 배포 단위 (Monolith)
├── Orders 모듈
│ ├── Public API (다른 모듈이 호출할 수 있는 인터페이스)
│ ├── Internal Domain (외부에 노출되지 않는 내부 구현)
│ ├── Internal Data Store (모듈 전용 테이블)
│ └── Event Publisher/Subscriber
├── Payments 모듈
│ ├── Public API
│ ├── Internal Domain
│ ├── Internal Data Store
│ └── Event Publisher/Subscriber
├── Inventory 모듈
│ └── ...
└── Shared Kernel (공용 Value Object, 인터페이스)
graph TD
subgraph "단일 배포 단위 (Monolith)"
subgraph "Orders Module"
OA["Public API"] --> OD["Internal Domain"]
OD --> ODB[("Data Store")]
end
subgraph "Payments Module"
PA["Public API"] --> PD["Internal Domain"]
PD --> PDB[("Data Store")]
end
subgraph "Inventory Module"
IA["Public API"] --> ID["Internal Domain"]
ID --> IDB[("Data Store")]
end
EB["Event Bus"]
SK["Shared Kernel"]
end
OA -.->|"API 호출"| PA
OA -.->|"API 호출"| IA
PA -.->|"이벤트 발행"| EB
EB -.->|"이벤트 구독"| OD
style OA fill:#FFB6C1,color:#000000
style OD fill:#FFB6C1,color:#000000
style ODB fill:#FFB6C1,color:#000000
style PA fill:#87CEEB,color:#000000
style PD fill:#87CEEB,color:#000000
style PDB fill:#87CEEB,color:#000000
style IA fill:#90EE90,color:#000000
style ID fill:#90EE90,color:#000000
style IDB fill:#90EE90,color:#000000
style EB fill:#DDA0DD,color:#000000
style SK fill:#FFD700,color:#000000
각 모듈(색상별)은 자신만의 Public API, Internal Domain, Data Store를 가집니다. 모듈 간 직접 호출은 단방향으로 유지하여 순환 의존을 방지하고, 역방향 통신이 필요할 때는 Event Bus를 통한 간접 통신을 사용합니다. 어떤 경우든 다른 모듈의 내부 구현이나 DB에 직접 접근하지 않습니다.
각 모듈은 다음 원칙을 따릅니다.
- 모듈 간 통신은 Public API를 통해서만 합니다. 다른 모듈의 내부 클래스나 DB 테이블에 직접 접근하지 않습니다.
- 각 모듈은 자신의 데이터를 소유합니다. 다른 모듈의 테이블을 직접 조인하지 않습니다.
- 모듈 내부 구조는 자유입니다. 어떤 모듈은 Layered Architecture를, 어떤 모듈은 Hexagonal을, 어떤 모듈은 VSA를 사용할 수 있습니다.
왜 마이크로서비스가 아닌가#
마이크로서비스는 모듈 경계뿐 아니라 배포 경계, 네트워크 경계, 데이터 저장 경계 까지 분리합니다. 이것은 강력한 독립성을 제공하지만, 대가도 큽니다.
- 서비스 간 통신이 네트워크 호출이므로 지연, 실패, 재시도를 처리해야 합니다.
- 분산 트랜잭션, eventual consistency, 데이터 동기화 문제가 생깁니다.
- 배포 파이프라인, 모니터링, 서비스 디스커버리 등 운영 인프라가 필요합니다.
Modular Monolith는 모듈 경계는 엄격하게 유지하되, 프로세스 내 함수 호출 로 통신하므로 이런 분산 시스템 복잡성을 피할 수 있습니다. 모듈 간 통신이 메서드 호출이므로 트랜잭션 관리도 단순합니다.
마이크로서비스로의 진화 경로#
Modular Monolith의 큰 이점 중 하나는 마이크로서비스로의 점진적 전환이 가능 하다는 것입니다. 모듈 간 경계가 이미 Public API로 정의되어 있으므로, 특정 모듈을 별도 서비스로 분리할 때 해당 Public API를 네트워크 호출(REST, gRPC)로 교체하면 됩니다.
반대 방향도 가능합니다. 성급하게 마이크로서비스로 분리했다가 운영 복잡성이 이점을 초과한다면, 다시 모듈로 합칠 수 있습니다. 이는 모듈 경계가 명확하게 정의되어 있기 때문에 가능합니다.
실무에서 권장되는 경로는 모놀리스 → Modular Monolith → (필요시) 마이크로서비스 입니다. Sam Newman도 “마이크로서비스는 목표가 아니라 수단"이며, 모놀리스에서 시작하는 것이 대부분의 경우 올바른 선택이라고 말합니다.
장점#
분산 시스템 복잡성 없이 모듈 독립성을 얻습니다. 네트워크 호출, 분산 트랜잭션, 서비스 디스커버리 같은 인프라 없이도 모듈 간 명확한 경계를 유지할 수 있습니다.
개발 속도가 빠릅니다. 단일 프로세스이므로 로컬 개발, 디버깅, 테스트가 마이크로서비스보다 훨씬 단순합니다. IDE에서 모든 코드를 한 번에 열고 탐색할 수 있습니다.
배포가 단순합니다. 하나의 아티팩트만 빌드하고 배포하면 됩니다. 배포 파이프라인, 서비스 메시, 컨테이너 오케스트레이션의 복잡성이 크게 줄어듭니다.
모듈 내부 구조를 자유롭게 선택할 수 있습니다. 주문 모듈은 도메인이 복잡하니 Hexagonal로, 관리자 모듈은 단순하니 Layered로, 검색 모듈은 CQRS로 구성하는 것이 가능합니다.
단점#
모듈 경계를 강제하기 어렵습니다. 같은 프로세스 안에 있으므로, 개발자가 “급하니까” 다른 모듈의 내부 클래스를 직접 참조하거나 테이블을 직접 조인하는 유혹이 있습니다. 이를 방지하려면 컴파일타임 경계 검사(예: Java의 모듈 시스템, .NET의 internal 접근 제한자, ArchUnit 같은 아키텍처 테스트 도구)가 필요합니다.
독립적 배포가 불가능합니다. 하나의 모듈만 수정해도 전체를 재배포해야 합니다. 모듈 간 배포 주기가 크게 다른 조직에서는 병목이 될 수 있습니다.
독립적 확장이 제한적입니다. 특정 모듈만 스케일 아웃할 수 없고, 전체 애플리케이션을 함께 스케일해야 합니다. 모듈별 리소스 요구사항이 극단적으로 다른 경우에는 한계가 있습니다.
모듈 간 DB 분리가 논리적 수준에 머물 수 있습니다. 같은 DB 안에서 스키마나 테이블 접두사로 분리하는 경우, 개발자가 직접 조인하는 것을 완전히 막기 어렵습니다.
적합한 상황#
- 마이크로서비스의 운영 복잡성을 감수하기 어려운 조직
- 모듈 간 경계는 필요하지만 독립 배포까지는 필요 없는 시스템
- 나중에 마이크로서비스로 전환할 가능성을 열어두고 싶은 경우
- 팀 규모가 중소규모(2~5팀)이고, 모듈별 독립 배포 주기가 필수적이지 않은 환경
부적합한 상황#
- 모듈별 독립 배포가 비즈니스적으로 필수적인 환경
- 모듈별 기술 스택이 완전히 달라야 하는 경우
- 모듈별 스케일링 요구사항이 극단적으로 다른 시스템
- 수십 개 팀이 동시에 개발하여 배포 병목이 심각한 대규모 조직
7. 비교와 선택 기준#
한눈에 보는 비교#
| 아키텍처 | 코드 조직 축 | 의존성 방향 | 주요 강점 | 주요 비용 |
|---|---|---|---|---|
| Layered | 기술 책임 (수평) | 위 → 아래 | 이해 용이, 빠른 합의 | 도메인 빈혈, pass-through |
| Ports & Adapters | 안/밖 경계 | 바깥 → 안쪽 | 도메인 보호, 테스트 격리 | 추상화 비용, 교리화 |
| CQRS | 읽기/쓰기 분리 | 경로별 독립 | 독립 최적화, 확장성 | 모델 이중화, 동기화 |
| EDA | 이벤트 흐름 | 발행 → 구독 | 낮은 결합, 비동기 처리 | 디버깅 난이도, 순서 보장 |
| VSA | 기능/요청 단위 (수직) | slice 내부 자유 | 변경 국소화, 유연성 | 리팩터링 역량 요구 |
| Modular Monolith | 비즈니스 도메인 (모듈) | 모듈 API 통해서만 | 경계 + 단순 운영 | 경계 강제, 독립 배포 불가 |
이것들은 상호배타적이지 않다#
가장 중요한 점은 이 아키텍처들이 서로 다른 관심사를 다룬다는 것입니다. 실무에서는 여러 패턴을 조합하는 것이 자연스럽습니다.
- Modular Monolith + 모듈 내부에 Hexagonal: 전체 시스템은 모듈로 나누고, 각 모듈 내부는 Ports & Adapters로 구성합니다.
- Hexagonal + CQRS: 쓰기 경로는 도메인 중심으로, 읽기 경로는 별도 모델로 분리합니다.
- VSA + 복잡한 slice에 Domain Model: 단순한 slice는 transaction script로, 복잡한 slice는 rich domain model로 구현합니다.
- Modular Monolith + EDA: 모듈 간 통신에 이벤트를 사용하여 결합도를 낮춥니다.
Bogard 자신도 “보통의 n-tier 또는 hexagonal 아키텍처에서 장벽을 제거하고 vertical slice로 응집을 재배치한다"고 설명합니다.
선택 기준#
아키텍처 선택은 “어느 이름이 더 좋은가"가 아니라, 다음 질문에 대한 답에서 나옵니다.
도메인 복잡도가 높은가? → 높다면 Ports & Adapters 계열(Hexagonal/Onion/Clean)로 도메인을 보호합니다. 낮다면 Layered Architecture로 시작해도 충분합니다.
읽기/쓰기 요구사항이 크게 다른가? → 다르다면 CQRS를 적용합니다. 같다면 굳이 모델을 나눌 필요가 없습니다.
비동기 처리와 컴포넌트 간 결합 해소가 필요한가? → 필요하다면 EDA를 도입합니다. 메시지 브로커가 이미 있다면 더 자연스럽습니다.
기능 변화가 빠르고 요청마다 구현 방식이 다른가? → 그렇다면 VSA가 변경 비용을 줄여줍니다. 팀의 리팩터링 역량이 전제 조건입니다.
모듈 간 독립성은 필요하지만 분산 시스템 복잡성은 피하고 싶은가? → Modular Monolith가 적합합니다. 나중에 마이크로서비스로 진화할 수 있는 발판이 됩니다.
팀의 역량과 습관은 어떤가? → 어떤 아키텍처든 팀이 그 구조를 유지할 의지와 역량이 없다면 퇴화합니다. 가장 좋은 아키텍처는 팀이 실제로 지킬 수 있는 아키텍처입니다.
결론#
여섯 가지 아키텍처를 요약하면 이렇습니다.
- Layered Architecture 는 기술 책임별 분리에 강한 전통적 기본형입니다.
- Ports & Adapters 계열 은 도메인 보호와 의존성 통제에 강한 한 가족입니다.
- CQRS 는 읽기/쓰기 비대칭을 해결하는 실용적 분리 패턴입니다.
- EDA 는 컴포넌트 간 결합을 낮추고 비동기 처리를 가능하게 하는 통신 패턴입니다.
- VSA 는 기능 변화 속도와 요청 단위 응집에 강한 실용형입니다.
- Modular Monolith 는 마이크로서비스의 경계 이점을 분산 복잡성 없이 얻는 구조형입니다.
무엇이 “최고"인지는 절대적이지 않습니다. 핵심은 내 시스템의 주된 복잡도가 도메인 규칙 에 있는지, 입출력 경계 에 있는지, 읽기/쓰기 비대칭 에 있는지, 컴포넌트 간 결합 에 있는지, 기능 변화의 속도 에 있는지를 먼저 진단하는 것입니다. 그 진단에 답하고 나면, 어떤 패턴을 선택하고 어떻게 조합할지는 생각보다 훨씬 명확해집니다.
References#
- Microsoft, “Common web application architectures,” Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/common-web-application-architectures
- Microsoft, “N-tier architecture style,” Azure Architecture Center. https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/n-tier
- Microsoft, “DDD-oriented microservice,” Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice
- Microsoft, “Infrastructure persistence layer design,” Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design
- Eric Evans, “Domain-Driven Design Reference,” Domain Language, 2015. https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf
- Martin Fowler, “CQRS,” martinfowler.com. https://martinfowler.com/bliki/CQRS.html
- Martin Fowler, “Event Sourcing,” martinfowler.com. https://martinfowler.com/eaaDev/EventSourcing.html
- Alistair Cockburn, “Hexagonal Architecture,” alistair.cockburn.us. https://alistair.cockburn.us/hexagonal-architecture
- Jeffrey Palermo, “The Onion Architecture: part 1,” Programming with Palermo, 2008. https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/
- Robert C. Martin, “The Clean Architecture,” Clean Coder Blog, 2012. https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
- Jimmy Bogard, “Vertical Slice Architecture,” jimmybogard.com. https://www.jimmybogard.com/vertical-slice-architecture/
- Mark Richards, “Software Architecture Patterns,” O’Reilly, 2015. https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/
- Shopify Engineering, “Deconstructing the Monolith,” Shopify Engineering Blog. https://shopify.engineering/deconstructing-the-monolith
- Sam Newman, “Monolith First,” samnewman.io. https://samnewman.io/blog/2015/04/07/microservices-and-monoliths-is-there-a-third-way/
- Greg Young, “CQRS Documents,” 2010. https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf