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() # 레코드 삭제
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}")
- 애그리게이트 루트 (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)
그래서 이를 아래처럼 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)
'책 & 스터디' 카테고리의 다른 글
[자바/스프링 개발자를 위한 실용주의 프로그래밍] - 1부 객체 지향(2) SOLID (0) | 2024.11.04 |
---|---|
[자바/스프링 개발자를 위한 실용주의 프로그래밍] - 1부 객체 지향(1) (1) | 2024.10.29 |
도메인 주도 설계 첫걸음 Part 1. 전략적 설계 (0) | 2024.08.26 |
object 스터디를 마치며.. (1) | 2024.05.31 |
[파이썬으로 살펴보는 아키텍처 패턴] 챕터 12 명령-질의 책임 분리(CQRS) (0) | 2024.05.19 |