https://pragprog.com/titles/swdddf/domain-modeling-made-functional/
Order 및 OrderLine 타입에 대해 자세히 살펴보겠습니다.
type OrderLine = {
OrderLineId : int
Price : float
// etc
}
type Order = {
OrderLines : OrderLine list
// etc
}
OrderLine을 변경하면 Order도 변경되나요?
이 경우 대답은 '예'임이 분명합니다.
OrderLine을 변경하면 전체 Order도 변경됩니다.
사실, 데이터 구조를 변경할 수 없기 때문에 이를 피할 수 없습니다.
변경할 수 없는 OrderLine을 포함하는 변경할 수 없는 주문이 있는 경우 주문 라인 중 하나의 복사본을 만드는 것만으로는 Order의 복사본을 만들 수 없습니다.
Order에 포함된 OrderLine을 변경하려면 OrderLine 수준이 아니라 Order 수준에서 변경해야 합니다.
예를 들어 주문 라인의 가격을 업데이트하기 위한 의사 코드는 다음과 같습니다.
/// We pass in three parameters:
/// * the top-level order
/// * the id of the order line we want to change
/// * the new price
let changeOrderLinePrice order orderLineId newPrice =
// 1. find the line to change using the orderLineId
let orderLine = order.OrderLines |> findOrderLine orderLineId
// 2. make a new version of the OrderLine with the new price
let newOrderLine = {orderLine with Price = newPrice}
// 3. create a new list of lines, replacing
// the old line with the new line
let newOrderLines =
order.OrderLines |> replaceOrderLine orderLineId newOrderLine
// 4. make a new version of the entire order, replacing
// all the old lines with the new lines
// 가장 중요합니다! 새로운 order를 리턴합니다!
let newOrder = {order with OrderLines = newOrderLines}
// 5. return the new order
newOrder
let findOrderLine orderLineId (lines:OrderLine list) =
lines |> List.find (fun ol -> ol.OrderLineId = orderLineId )
let replaceOrderLine orderLineId newOrderLine lines =
lines // no implementation!
따라서 "하위 항목"(OrderLine) 중 하나를 변경하더라도 항상 Order 수준에서 작업해야 합니다.
Aggregates 는 일관성과 불변 규칙을 강제합니다. (Consistency and Invariants)
Aggregate는 데이터가 업데이트될 때 중요한 역할을 합니다.
Aggregate는 일관성 경계 역할을 합니다. (일관성 - 여러 데이터 간의 일치, 정합성)
Aggregate의 한 부분이 업데이트되면 일관성을 보장하기 위해 다른 부분도 업데이트해야 할 수 있습니다.
예를 들어, 최상위 주문에 추가 필드인 "총 가격"이 저장되도록 이 디자인을 확장할 수 있습니다.
분명히 라인 중 하나가 가격을 변경하면 데이터 일관성을 유지하기 위해 Aggregate도 업데이트해야 합니다.
이것은 위의 changeOrderLinePrice 함수에서 수행됩니다.
일관성을 유지하는 방법을 "알고 있는" 유일한 구성 요소는 최상위 주문(Aggregate Root)뿐이므로 이것이 라인 수준이 아닌 주문 수준에서 모든 업데이트를 수행하는 또 다른 이유입니다.
Aggregate는 불변 규칙이 적용되는 곳이기도 합니다. (Invariant - 비즈니스 규칙)
모든 주문에 하나 이상의 주문 라인이 있다는 규칙이 있다고 가정해 보겠습니다.
그런 다음 여러 주문 라인을 삭제하려고 하면 집계에서 한 라인만 남아 있을 때 삭제 시도 시 오류가 발생하는지 확인합니다.
Aggregate 참조
type Order = {
OrderId : OrderId
Customer : Customer // info about associated customer
OrderLines : OrderLine list
// etc
}
훨씬 더 나은 디자인은 전체 고객 레코드 자체가 아니라 고객에 대한 참조를 저장하는 것입니다.
즉, 다음과 같이 주문 타입에 CustomerId를 저장합니다.
type Order = {
OrderId : OrderId
CustomerId : CustomerId // reference to associated customer
OrderLines : OrderLine list
// etc
}
이것은 Aggregates의 또 다른 중요한 측면으로 이어집니다.
Aggregates는 persistence의 기본 단위입니다.
- 최상위 엔터티 루트가 아니고
- 일관성 경계가 아니면 DDD "Aggregates"가 아닙니다.
- 집계는 최상위 엔터티가 "루트" 역할을 하는 단일 단위로 처리될 수 있는 도메인 개체의 모음입니다.
- 집계 내부의 개체에 대한 모든 변경 사항은 최상위 수준을 통해 루트에 적용되어야 하며
- 집계는 일관성 경계 역할을 하여 집계 내부의 모든 데이터가 동시에 올바르게 업데이트되도록 합니다.
- 집계는 지속성, 데이터베이스 트랜잭션 및 트랜잭션의 원자성 단위입니다.
지금까지 공부한 DDD 용어는 다음과 같습니다.
- Value Object는 ID가 없는 도메인 개체입니다. 동일한 데이터를 포함하는 두 개의 Value Object는 동일한 것으로 간주됩니다. Value Object는 변경 불가능해야 합니다. 일부가 변경되면 다른 Value Object가 됩니다. Value Object의 예로는 이름, 주소, 위치, 돈 및 날짜가 있습니다.
- Entity는 속성이 변경되어도 지속되는 고유한 ID를 가진 도메인 개체입니다. Entity 개체에는 일반적으로 ID 또는 키 필드가 있으며 동일한 ID/키를 가진 두 개체는 동일한 개체로 간주됩니다. Entity는 일반적으로 문서와 같이 수명과 변경 이력이 있는 도메인 개체를 나타냅니다. Entity의 예로는 고객, 주문, 제품 및 송장이 있습니다.
- Aggregate는 도메인의 일관성을 보장하고 데이터 트랜잭션의 원자성 단위로 사용되며, 단일 구성 요소로 처리되는 관련된 Entity의 모음입니다. 다른 Entity는 "Root"로 알려진 Aggregate의 "최상위" 구성원 ID인 식별자로만 Aggregate를 참조해야 합니다.
타입 정의를 도메인 정의 문서로 활용하기.
지금까지 코드로 어떻게 문서를 만들 것인지에 대해 배웠습니다.
답은 타입 시스템을 적절히 활용하는 것입니다.
타입 시스템을 활용하여 도메인 전문가 및 다른 협력자들이 도메인의 요구 사항을 캡처할 수 있나요?
개발자가 아니라면 AND(레코드 타입), OR(초이스 타입, | ), Process(함수 화살표) 세 가지를 배우면 되지만, 기존 프로그래밍 언어보다 훨씬 읽기 쉬울 것입니다.
(빠진 내용은 도메인의 무결성과 일관성, 상태에 대한 내용입니다. 이는 추후 다룹니다.)
총정리
module PuttingItAllTogether =
(*>PuttingItAllTogether1
// 네임스페이스로 바운디드 컨텍스트를 나타냅니다!
namespace OrderTaking.Domain
// types follow
<*)
//>PuttingItAllTogether2
// Product code related
// Simple type (도메인 VO)
// 이것들은 모두 값 개체이며 식별자가 필요하지 않습니다.
type WidgetCode = WidgetCode of string
// constraint: starting with "W" then 4 digits
type GizmoCode = GizmoCode of string
// constraint: starting with "G" then 3 digits
type ProductCode =
| Widget of WidgetCode
| Gizmo of GizmoCode
// Order Quantity related
type UnitQuantity = UnitQuantity of int
type KilogramQuantity = KilogramQuantity of decimal
type OrderQuantity =
| Unit of UnitQuantity
| Kilos of KilogramQuantity
//<
//>PuttingItAllTogether3
// 엔터티
// 주문은 변경될 때 유지되는 ID(식별자)가 있으므로 엔터티로 모델링해야 합니다.
// ID가 string인지 int인지 Guid인지 알 수 없지만 필요하다는 것을 알고 있으므로
// 지금은 Undefined를 사용합시다. 다른 식별자도 같은 방식으로 처리합니다.
type OrderId = Undefined
type OrderLineId = Undefined
type CustomerId = Undefined
//<
// 이제 주문과 해당 구성 요소를 스케치할 수 있습니다.
// VO는 엔터티의 필드 역할을 합니다.
//>PuttingItAllTogether4
type CustomerInfo = Undefined
type ShippingAddress = Undefined
type BillingAddress = Undefined
type Price = Undefined
type BillingAmount = Undefined
type Order = {
Id : OrderId // id for entity
CustomerId : CustomerId // customer reference
ShippingAddress : ShippingAddress
BillingAddress : BillingAddress
OrderLines : OrderLine list
AmountToBill : BillingAmount
}
// and는 아래 나오는 타입을 참조하기 위한 F# 키워드입니다.
and OrderLine = {
Id : OrderLineId // id for entity
OrderId : OrderId
ProductCode : ProductCode
OrderQuantity : OrderQuantity
Price : Price
}
//<
// 이제 워크플로로 마무리하겠습니다.
// 워크플로에 대한 입력인 UnvalidatedOrder는 "as is" 주문 양식에서 작성되므로
// int 및 string과 같은 primitive 타입만 포함됩니다.
//>PuttingItAllTogether5
type UnvalidatedOrder = {
OrderId : string
CustomerInfo : DotDotDot
ShippingAddress : DotDotDot
//...
}
//<
// 워크플로의 출력에는 두 가지 유형이 필요합니다.
// 첫 번째는 워크플로가 성공한 경우의 이벤트 유형입니다.
//>PuttingItAllTogether6a
type PlaceOrderEvents = {
AcknowledgmentSent : DotDotDot
OrderPlaced : DotDotDot
BillableOrderPlaced : DotDotDot
}
//<
// 두 번째는 워크플로가 실패할 때의 오류 유형입니다.
//>PuttingItAllTogether6b
type PlaceOrderError =
| ValidationError of ValidationError list
| DotDotDot // other errors
and ValidationError = {
FieldName : string
ErrorDescription : string
}
//<
//>PuttingItAllTogether7
// 마지막으로 주문 배치 워크플로를 나타내는 최상위 기능을 정의할 수 있습니다.
/// The "Place Order" process
type PlaceOrder =
UnvalidatedOrder -> Result<PlaceOrderEvents,PlaceOrderError>
//<
'BackEnd' 카테고리의 다른 글
파이프라인으로 워크플로 모델링하기 - 개요 및 상태 머신 (0) | 2022.03.19 |
---|---|
도메인의 무결성(Integrity)과 일관성(Consistency) 관리하기 - Make illegal states unrepresentable (0) | 2022.03.19 |
타입으로 도메인 모델링하기 with F# - Value Object, Entitiy (0) | 2022.03.18 |
타입으로 도메인 모델링하기 with F# - 단순, 복합 타입과 함수 (0) | 2022.03.18 |
타입으로 코드 문서화하기 With F# - 타입을 조합하여 도메인 모델링 (0) | 2022.03.18 |