마이크로서비스의 도메인 기반 설계(DDD)
조직이 모놀리식 아키텍처에서 마이크로서비스로 전환할 때 다음과 같은 매우 중요하고 중요한 질문에 직면하게 됩니다. 서비스의 경계를 어떻게 그어야 할까요?
이론적으로 마이크로서비스는 독립적으로 개발, 배포 및 확장할 수 있는 느슨하고 분리된 단위여야 합니다. 그러나 실제로는 많은 팀이 서비스가 너무 긴밀하게 결합되어 단일 비즈니스 변경으로 인해 여러 서비스를 동시에 수정 및 배포해야 하는 시스템인 분산 모놀리스를 구축하게 되어 네트워크 대기 시간과 배포 정체 현상이 가중됩니다.
이러한 함정을 피하기 위해 소프트웨어 설계자는 **도메인 중심 설계(DDD)**로 전환합니다. 2003년 Eric Evans가 처음 소개한 DDD는 코드 구조를 코드 구조가 나타내는 복잡한 비즈니스 도메인에 맞추는 소프트웨어 개발 방법론입니다.
이 기사에서는 DDD가 깨끗하고 분리되었으며 유지 관리 가능성이 높은 마이크로서비스를 설계하기 위한 전략적, 전술적 청사진을 제공하는 방법을 살펴보겠습니다.
1. 전략적 청사진: 서비스 경계 그리기
DDD는 전략적 설계와 전술적 설계라는 두 가지 기본 단계로 구분됩니다. 전략적 설계는 비즈니스 컨텍스트를 모델링하고 서비스 경계를 정의하는 것입니다. 이는 아키텍처의 “매크로” 보기를 제공합니다.
A. 유비쿼터스 언어
비즈니스 내에서는 서로 다른 부서에서 동일한 단어를 사용하여 완전히 다른 의미를 갖습니다. 예를 들면:
- 영업팀에서 “사용자"는 리드 또는 잠재 고객을 의미합니다.
- 보안팀에게 “사용자"는 로그인 자격 증명의 집합입니다.
- 배송팀에게 있어 “사용자"는 실제 수령인 주소입니다.
모든 사람을 만족시키는 “사용자"를 위한 단일 통합 데이터베이스 모델을 구축하려고 하면 방대하고 얽힌 코드베이스가 생성됩니다. DDD는 특정 경계 내에서 비즈니스 도메인 전문가와 개발자 모두가 정의하고 사용하는 공유 어휘인 유비쿼터스 언어를 구축하여 이 문제를 해결합니다.
B. 제한된 컨텍스트
바운드 컨텍스트는 도메인 모델이 적용되는 명시적인 경계입니다. 경계 내에서 유비쿼터스 언어의 모든 용어는 하나의 명확한 의미를 갖습니다.
단일 “사용자” 모델 대신 각각의 바인딩된 컨텍스트 내에 별도의 모델을 정의합니다.
- 주문 컨텍스트에는 고객 연락처 정보가 포함된
Order모델이 있습니다. - ID 컨텍스트에는
Credentials모델이 있습니다. - 배송 컨텍스트에는
DeliveryAddress모델이 있습니다.
마이크로서비스의 황금률: 하나의 제한된 컨텍스트는 하나의 마이크로서비스에 직접 매핑됩니다.
시스템을 제한된 컨텍스트로 분할함으로써 마이크로서비스가 느슨하게 결합되고 고유한 비즈니스 기능을 나타내도록 보장합니다.
C. 컨텍스트 매핑: 서비스가 통신하는 방법
바인딩된 컨텍스트는 단독으로 존재하지 않습니다. 그들은 상호작용해야 합니다. 컨텍스트 맵은 컨텍스트 간의 관계 및 번역 메커니즘을 정의합니다. 주요 패턴은 다음과 같습니다.
- 공유 커널: 두 컨텍스트가 도메인 모델과 데이터베이스의 작은 하위 집합을 공유합니다(일반적으로 마이크로서비스에서는 결합으로 인해 권장되지 않음).
- 고객-공급자: 한 컨텍스트(공급업체)는 다른 컨텍스트(고객)에게 데이터를 제공해야 합니다. 공급업체는 고객과 함께 출시 일정을 조정해야 합니다.
- ACL(부패 방지 계층): 외부 시스템에서 들어오는 데이터를 클라이언트의 내부 도메인 모델로 변환하여 외부 모델이 내부 아키텍처를 오염시키는 것을 방지하는 변환 계층입니다.
2. 전술적 청사진: 마이크로서비스 코드베이스 구조화
전략적 설계를 사용하여 서비스 경계가 그려지면 전술적 설계는 단일 마이크로서비스 내에서 코드를 구조화하기 위한 일련의 설계 패턴을 제공합니다.
A. 엔터티 및 값 개체
마이크로서비스 내에서 모델은 두 가지 범주로 나뉩니다.
- 엔티티: 속성이 변경되더라도 시간이 지나도 지속되는 고유한 ID를 갖는 객체입니다. 예를 들면
Order또는Product이 있습니다. - 값 개체: 고유한 ID가 없고 해당 속성에 의해 전적으로 정의되는 개체입니다. 그것들은 불변입니다. 예를 들면
ShippingAddress또는ProductDimensions이 있습니다. 두 개의 값 개체가 동일한 속성을 갖는 경우 동일한 것으로 간주됩니다.
B. 집계 및 집계 루트
집계는 데이터 변경을 위한 단일 단위로 처리되는 연관된 엔터티 및 값 개체의 클러스터입니다. 모든 집계에는 외부 개체가 집계와 상호 작용할 수 있는 유일한 진입점인 집계 루트가 있습니다. 루트는 모든 비즈니스 불변성(규칙)이 적용되도록 보장합니다.
예를 들어, 위 다이어그램에서:
- 주문 서비스에서
Order은 Aggregate Root입니다. 여기에는OrderItem(엔티티) 및ShippingAddress(값 개체)가 포함됩니다. 외부 서비스는OrderItem을(를) 직접 수정할 수 없습니다. 주문이 아직 배송되지 않았는지 확인하는Order루트(예:order.AddItem())에서 메서드를 호출해야 합니다.
C. 도메인 이벤트
도메인 이벤트는 비즈니스 전문가가 관심을 갖는 도메인에서 발생한 일입니다. 바인딩된 컨텍스트 전체에서 변경 사항을 비동기적으로 전달하는 데 사용됩니다.
Aggregate는 명령을 실행하면 도메인 이벤트(예: OrderCreated)를 게시합니다. 다른 서비스는 이 이벤트를 수신하고 이에 따라 상태를 업데이트하여 동기식 API 종속성 없이 최종 일관성을 보장합니다.
3. 구체적인 예: 주문 서비스 vs. 재고 서비스
Aggregate Root가 비즈니스 규칙을 조정하는 방법을 보여주는 주문 서비스를 위한 Go의 전술적 DDD의 단순화된 구현을 살펴보겠습니다.
package domain
import (
"errors"
"time"
)
// Value Object (Immutable, identity-less)
type ShippingAddress struct {
Street string
City string
ZipCode string
}
// Entity (Has identity, mutable)
type OrderItem struct {
ProductID string
Quantity int
Price float64
}
// Aggregate Root (Enforces transactional boundaries)
type Order struct {
ID string
Items []OrderItem
Address ShippingAddress
Status string
CreatedAt time.Time
}
// NewOrder creates a new Order Aggregate Root
func NewOrder(id string, address ShippingAddress) *Order {
return &Order{
ID: id,
Items: []OrderItem{},
Address: address,
Status: "PENDING",
CreatedAt: time.Now(),
}
}
// AddItem enforces business rules before mutating state
func (o *Order) AddItem(productID string, qty int, price float64) error {
if o.Status != "PENDING" {
return errors.New("cannot add items to a finalized or cancelled order")
}
if qty <= 0 {
return errors.New("quantity must be greater than zero")
}
o.Items = append(o.Items, OrderItem{
ProductID: productID,
Quantity: qty,
Price: price,
})
return nil
}
주문이 성공적으로 저장되면 서비스는 메시지 브로커에 이벤트를 게시합니다.
{
"event_id": "evt_98231",
"event_type": "OrderCreated",
"timestamp": "2026-06-26T00:15:00Z",
"payload": {
"order_id": "ord_5521",
"items": [
{ "product_id": "prod_88", "quantity": 2 }
]
}
}
Inventory Service는 이 이벤트를 수신하고 로컬 StockLevel 엔터티를 업데이트하고 흐름을 비동기적으로 완료합니다.
4. 마이크로서비스의 전략적 DDD와 전술적 DDD
| 위상/측면 | 전략적 DDD | 전술 DDD |
|---|---|---|
| 범위 | 글로벌 시스템 아키텍처(매크로) | 단일 서비스 코드베이스 내부(마이크로) |
| 주요 목표 | 깨끗하고 분리된 서비스 경계 그리기 | 풍부한 비즈니스 로직 모델링 및 불변성 적용 |
| 주요 개념 | 제한된 컨텍스트, 유비쿼터스 언어, 컨텍스트 맵 | 엔터티, 값 개체, 집계, 도메인 이벤트 |
| 청중 | 설계자, 제품 관리자, 개발자 | 소프트웨어 개발자, 코드 검토자 |
| 마이크로서비스에 미치는 영향 | 서비스 수 및 범위 결정 | 데이터베이스 트랜잭션 및 디렉터리 구조 결정 |
결론: 전략적 DDD부터 먼저 시작하세요
마이크로서비스에 도메인 중심 설계를 적용하는 것은 아키텍처가 비즈니스 구조와 일치하도록 하는 강력한 방법입니다. 그러나 팀은 전략적 설계를 무시하면서 전술적 패턴(저장소 인터페이스 및 값 개체 작성 등)에 너무 집중하는 실수를 저지르는 경우가 많습니다.
경계를 올바르게 설정하지 않으면 아무리 깔끔한 전술 코드를 사용해도 시스템이 분산형 단일체로 변하는 것을 막을 수 없습니다. 항상 전략적 매핑부터 시작하십시오. 유비쿼터스 언어를 정의하고, 논리를 제한된 컨텍스트로 그룹화하고, 통신 흐름을 매핑하고, 이러한 정의에서 마이크로서비스 경계가 자연스럽게 드러나도록 하십시오.