Search

도메인 주도 설계 첫걸음 Part 2. 전술적 설계 5장, 6장

part 1 에서 전략적 설계 측면에 대해 알아보았다. part 2에서는 전술적 설계 측면에서 ‘방법’에 대해 알아본다.

05. 간단한 비즈니스 로직 구현

비교적 간단한 비즈니스 로직에 적합한 두 가지 패턴을 살펴보자

1. 트랜잭션 스크립트 패턴

각 기능별로 스크립트나 메서드 작성하고 비즈니스 로직을 순차적으로 처리하는 방법
복잡한 계층 구조 없이도 로직을 구현할 수 있어 단순하고 직관적
비즈니스 로직이 단순한 지원 하위 도메인에 적합(ex. ETL 작업)

잘못된 트랜잭션 구현 예시

트랜잭션 동작 구현 실패
전체를 아우르는 트랜잭션 없이 여러 업데이트를 하는 경우
→ 데이터 변경을 모두 포함하는 트랜잭션으로 묶어 해결
분산 트랜잭션
분산 시스템에서는 데이터베이스의 데이터를 변경한 다음 메시지 버스에 메시지를 발행해 다른 컴포넌트에 변경사항을 알림
변경사항 커밋후 메시지를 발행하기전 오류가 난다면 다른 컴포넌트는 메시지를 받지 못해서 문제가 됨
암시적 분산 트랜잭션
로직이 호출되어 데이터를 업데이트했지만 성공/실패 여부가 호출자에게 전달하는데 실패한 경우. 호출자는 실패를 가정해 로직을 다시 호출하게 되면 업데이트가 이미 되었지만 한번더 업데이트가 발생
같은 요청을 여러번하더라도 결과가 같도록 멱등성을 보장하게 작업해야함
예를 들어 사용자에게 현재 값을 전달받고 DB상의 현재 값과 같을 경우만 로직이 실행되도록하면 최종 결과는 변경되지 않음

액티브 레코드 패턴

객체 - 테이블간의 매핑(ORM) 방식 중 하나로 클래스가 데이터베이스의 테이블에 직접 매핑됨.
객체의 메서드를 통해 CRUD 작업을 수행. 객체 자체가 데이터베이스와 상호작용하며 상태와 동작을 함께 가짐. 즉, 데이터와 비즈니스 로직이 한 클래스에 포함됨
DB 접근을 최적화하는 트랜잭션 스크립트이기 때문에 지원 하위 도메인, 일반 하위 도메인과 외부 솔루션의 연동, 모델 변환 작업에 적합
user = User(name="Alice", email="alice@example.com")# 객체 생성 (데이터베이스 레코드 생성)user.save()# 데이터베이스에 레코드 저장 found_user = User.find(1)# ID로 레코드 조회 found_user.delete()# 레코드 삭제
Plain Text
복사

06. 복잡한 비즈니스 로직 다루기

앞서 간단한 비즈니스 로직을 다루는 패턴에 더해 복잡한 비즈니스 로직에 사용되는 도메인 모델 패턴을 소개

도메인 모델

복잡한 상태 전환, 항상 보호해야 하는 규칙과 불변성을 다루기 위한 패턴
행동과 데이터를 포함하는 도메인의 객체 모델
DDD 전술 패턴인 애그리게이트, 밸류 오브젝트, 도메인 이벤트, 도메인 서비스는 모두 객체 모델의 구성요소

밸류 오브젝트

도메인 내에서 값으로 취급되는 객체를 의미. 값 자체가 의미를 가지며 고유한 식별자(id)가 없는 객체
밸류 오브젝트는 생성된 이후로 상태가 변하지 않기 때문에 불변성을 띈다
식별자가 없고 값 자체로 식별 되기때문에 두 개의 밸류 오브젝트가 같은 속성을 가지고 있다면 이 둘은 동일한 것으로 간주됨
특정 도메인 개념을 표현하기 위한 작고 의미있는 단위로 사용됨(화폐, 주소 등)
코드의 표현력을 높여주고 분산되기 쉬운 비즈니스 로직을 한데 묶어주기 때문에 가능한 모든 경우에 사용하는 게 좋음

엔티티

밸류 오브젝트와 정반대로 엔티티는 다른 엔티티 인스턴스와 구별하기 위해 명시적인 필드(id)가 필요함
엔티티의 식별 필드 값은 엔티티의 생애주기 내내 불변이며 엔티티 상태가 변하더라도 식별자를 동일한 객체로 인식 가능
도메인 모델에서의 엔티티는 애그리게이트 패턴의 컨텍스트에서만 구현함

애그리게이트

도메인 모델 내에서 일관성을 유지해야 하는 객체들의 집합
도메인 모델에서 중요한 개념들을 캡슐화하고 객체들간의 복잡한 상호작용을 관리
엔티티는 애그리게이트 내부의 구성요소로 모든 엔티티는 하나의 애그리게이트에 속함
애그리게이트는 항상 하나의 루트 엔티티를 가지며 외부에서 애그리게이트에 접근할 때는 항상 루트 엔티티를 통해 접근
애그리게이트 경계
애그리게이트가 관리하는 모든 엔티티와 밸류 오브젝트를 포함
경계 내에서는 일관성을 유지해야 함. 같은 트랜잭션 경계 공유
좀 더 자세한 예시)
쇼핑몰 도메인 설명
하나의 주문 (Order) 에는 여러 개의 주문 항목(OrderItem) 포함 될 수 있음
각 주문 항목은 Product를 나타냄
주문 생성 후 상태가 확인, 배송중, 완료도 변경 될 수 있음
주문이 생성되면 고객의 Adress, Payment Information이 필요
애그리게이트 구성요소
Order (주문): 주문 애그리게이트의 루트 엔티티
OrderItem (주문 항목): Order 애그리게이트 내부의 엔티티로, 각각의 상품을 나타냄
Address (주소): 주문에 포함된 배송 주소를 나타내는 밸류 오브젝트.
PaymentInformation (결제 정보): 결제 정보를 나타내는 밸류 오브젝트.
class OrderItem: def __init__(self, product_id, quantity, price): self.product_id = product_id self.quantity = quantity self.price = price class Address: def __init__(self, street, city, zip_code): self.street = street self.city = city self.zip_code = zip_code class PaymentInformation: def __init__(self, payment_type, details): self.payment_type = payment_type self.details = details class Order: def __init__(self, order_id, customer_id, address, payment_info): self.order_id = order_id # 고유 식별자 self.customer_id = customer_id self.status = "Pending" self.items = [] self.address = address self.payment_info = payment_info def add_item(self, product_id, quantity, price): item = OrderItem(product_id, quantity, price) self.items.append(item) def confirm_order(self): if not self.items: raise Exception("Cannot confirm an empty order") self.status = "Confirmed" def ship_order(self): if self.status != "Confirmed": raise Exception("Order must be confirmed before shipping") self.status = "Shipped" # 사용 예시 address = Address(street="123 Main St", city="Seoul", zip_code="12345") payment_info = PaymentInformation(payment_type="Credit Card", details="VISA") order = Order(order_id=1, customer_id=123, address=address, payment_info=payment_info) order.add_item(product_id=101, quantity=2, price=50) order.add_item(product_id=102, quantity=1, price=100) order.confirm_order() order.ship_order() print(f"Order status: {order.status}")
Python
복사
애그리게이트 루트 (Order): Order 클래스가 애그리게이트 루트. Order 객체를 통해서만 OrderItem, Address, PaymentInformation 같은 내부 객체에 접근하고 상태를 변경할 수 있음
일관성 유지: 모든 상태 변경(예: 주문 항목 추가, 주문 확인, 주문 배송)은 Order 애그리게이트 루트에서만 가능. 외부에서는 OrderItem이나 Address 객체를 직접 변경할 수 없음
코드 예시 설명: Order 객체는 하나의 주문을 나타내며, 주문이 생성된 후 주문 항목을 추가하거나 주문을 확인하고 배송 상태로 변경할 수 있음. 모든 작업은 Order 애그리게이트 루트를 통해 이루어진다

도메인 이벤트

비즈니스 도메인에서 일어나는 중요한 이벤트를 설명하는 메시지
주문이 완료됨, 결제가 승인됨 등
애그리게이트는 도메인 이벤트를 발행하여 외부 엔티티와 커뮤니케이션 할 수 있음
주문이 완료됨 이벤트 > 이벤트를 구독하는 결제 시스템이 결제 처리하도록 트리거

도메인 서비스

도메인 모델에서 애그리게이트나 밸류 오브젝트에 속하지 않는 비즈니스 로직을 구현한 상태가 없는 객체
복잡한 비즈니스 로직을 캡슐화하여 도메인 모델내에서 일관성 있게 관리 할 수 있게 해줌
코드 예시)
주문시 배송비 계산(calculate_shipping_cost)을 함께 한다면 Order 클래스 역할이 모호해지고 재사용성이 감소함
class Customer: def __init__(self, customer_id, name, membership_level): self.customer_id = customer_id self.name = name self.membership_level = membership_level class Order: def __init__(self, order_id, customer, order_amount, shipping_address): self.order_id = order_id self.customer = customer self.order_amount = order_amount self.shipping_address = shipping_address def calculate_shipping_cost(self): base_cost = 5.0# 기본 배송비# 주문 금액에 따른 할인if self.order_amount > 100: base_cost -= 2.0 # 회원 등급에 따른 할인if self.customer.membership_level == 'Gold': base_cost -= 1.5 elif self.customer.membership_level == 'Silver': base_cost -= 1.0 # 배송지에 따른 추가 요금if self.shipping_address.startswith('Seoul'): base_cost += 3.0 elif self.shipping_address.startswith('Busan'): base_cost += 4.0 # 최소 배송비를 보장return max(base_cost, 0.0)
Python
복사
그래서 이를 아래처럼 calculate_shipping_cost 를 처리하는 서비스로 분리해서 책임을 나누고, 해당 서비스를 다른 곳에서 재사용 할 수 도 있게 되는 것
class ShippingCostService: def calculate_shipping_cost(self, order): base_cost = 5.0 if order.order_amount > 100: base_cost -= 2.0 if order.customer.membership_level == 'Gold': base_cost -= 1.5 elif order.customer.membership_level == 'Silver': base_cost -= 1.0 if order.shipping_address.startswith('Seoul'): base_cost += 3.0 elif order.shipping_address.startswith('Busan'): base_cost += 4.0 return max(base_cost, 0.0) # 고객과 주문 생성 customer = Customer(customer_id=1, name="Alice", membership_level="Gold") order = Order(order_id=101, customer=customer, order_amount=150, shipping_address="Seoul") # 도메인 서비스 인스턴스 생성 shipping_service = ShippingCostService() # 배송비 계산 shipping_cost = shipping_service.calculate_shipping_cost(order)
Python
복사