소프트웨어 작성은 단순히 코드 작성이 아니라 특정 문제를 해결하는 것이라고 말하는 것은 혁신적이지 않습니다. 결국 솔루션을 구현하는 사람은 개발자이지만, 애초에 문제가 무엇인지 정의하는 사람은 개발자가 아닙니다. 해당 작업은 문제가 무엇인지, 문제가 존재하는 이유 및 해결 방법을 설명하기 위해 프로세스, 위험 및 결과를 고려하는 다양한 비즈니스 담당자에 의해 수행됩니다. 도메인 기반 컨텍스트에서는 이러한 비즈니스 인력을 도메인 전문가 라고 합니다.
엔지니어링 관점에서 볼 때 도메인 전문가는 귀중한 자산, 즉 도메인에 대한 지식을 보유하고 있는 것으로 보입니다. 그러나 이러한 지식은 원시 형태로 공유되는 경우가 거의 없습니다. 대신 개발자가 이해하고 구현할 수 있도록 일반적으로 요구 사항으로 변환됩니다. 이 접근 방식의 문제는 비즈니스 담당자와 개발자의 도메인 지식이 다를 수 있다는 것입니다. 이는 문제를 정의하는 사람과 문제를 해결하기 위해 노력하는 사람의 관점이 일치하지 않아 오해와 갈등이 발생할 수 있음을 의미합니다.
그렇다면 탈출구는 무엇입니까? 비즈니스 담당자와 기술 담당자가 동일한 언어 및 용어를 사용하는지 확인하세요.
DDD(도메인 중심 설계)는 도메인 전문가와 기술 이해관계자 간의 공유된 이해를 창출하고 소프트웨어 솔루션을 기본 비즈니스 요구 사항에 맞게 조정하는 것의 중요성을 강조하는 방법론입니다 . 이는 높은 수준의 비기술적 정의인 것처럼 보이지만 보다 개발자 친화적인 정의로 나눌 수도 있습니다.
DDD는 비즈니스 도메인을 모델링하여 구축한 유비쿼터스 언어를 육성하고 사용하여 코드와 그 구조에 실제 개념을 표현하고 있습니다.
아직 소개해야 할 용어가 있어서 당장은 100% 명확하지 않을 수도 있습니다. 가장 중요한 것은 DDD가 비즈니스 비전에 맞는 코드를 작성하고 구조화할 수 있는 도구와 활동을 제공한다는 것입니다. 그러면 의사소통뿐만 아니라 실제로 공통 언어를 형성하는 디자인 결정을 내리는 것도 중요합니다.
DDD 세계에서 가장 중요한 용어가 도메인 이라는 것은 놀라운 일이 아닙니다. "Learning Domain-Driven Design"의 저자인 Vlad Khononov는 다음과 같이 설명합니다.
비즈니스 도메인은 회사의 주요 활동 영역을 정의합니다.
이는 비즈니스 도메인이 다음과 같이 간주될 수도 있음을 의미합니다.
도메인은 보다 구체적인 활동 범위인 하위 도메인 으로 나눌 수도 있습니다. 하위 도메인에는 세 가지 유형이 있지만 가장 중요한 하위 도메인은 핵심 입니다. 회사가 비즈니스 이점을 달성하는 방법을 설명합니다. 나머지 두 가지는 인증 시스템이나 내부 관리 패널과 같은 보다 일반적이고 일반적인 문제에 관한 것입니다.
도메인 중심 디자인의 이점을 최대한 활용하려면 회사의 비즈니스 도메인에 대한 깊은 이해가 절대적으로 중요합니다. 이러한 이해의 가장 좋은 원천은 바로 도메인 전문가 입니다. 이들은 이해관계자, 다양한 사업가, 심지어 사용자까지 소프트웨어로 문제를 해결하는 개인입니다. 엔지니어가 자신이 작업 중인 영역에 대해 지식이 없다는 것이 아니라 전문가가 해당 영역 지식의 진실의 원천이라는 것입니다. 개발자는 도메인 전문가와 협력하여 도메인 모델이 정확하고 최신 상태로 유지되도록 할 수 있습니다.
이는 또 다른 중요하지만 잠재적으로 모호한 용어인 model 로 이어집니다. Eric Evans는 DDD에 관한 그의 저서에서 이 모델을 다음과 같이 설명합니다.
이는 당면한 문제 해결과 관련된 측면을 추상화하고 불필요한 세부 사항을 무시하는 현실에 대한 해석입니다.
Vlad Khononov는 계속해서 이 아이디어를 보다 관련성이 높은 용어로 설명합니다.
모델은 실제 세계의 복사본이 아니라 실제 시스템을 이해하는 데 도움이 되는 인간의 구성물입니다.
결론적으로, 모델은 해당 도메인의 기본 복잡성에 대한 이해와 전달을 용이하게 하는 비즈니스 개념 또는 프로세스를 표현한 것입니다.
Vlad는 도메인 모델의 개념을 효과적으로 설명하기 위해 지도를 사용했습니다. 지도는 지형, 도로, 국경 등 지도 유형과 관련된 정보만 표시하는 방법을 보여주는 완벽한 예입니다. 모든 세부정보를 한 번에 표시하는 지도는 부담스럽고 거의 쓸모가 없습니다. 도메인 모델은 다음과 같은 다른 형태로도 찾을 수 있습니다.
DDD(Domain-Driven Design) 용어 퍼즐의 마지막 조각은 유비쿼터스 언어입니다. 이는 프로젝트에서 기술 및 비즈니스 이해관계자 모두가 사용하는 공유 언어를 나타냅니다. 도메인 모델에서 파생된 비즈니스 도메인을 설명하는 공통 언어를 갖는 것은 DDD에서 매우 중요합니다. 이는 모든 팀 구성원이 문제 공간, 개념 및 관계를 명확하게 이해하는 데 도움이 됩니다. 이를 통해 더 나은 정렬이 이루어지고 오해의 위험이 줄어듭니다. 유비쿼터스 언어를 사용함으로써 소프트웨어 솔루션은 기본 비즈니스 요구 사항을 정확하게 반영할 수 있으므로 DDD의 중요한 구성 요소가 됩니다.
대부분의 용어를 다루었으므로 도메인 중심 디자인이 무엇인지 이해하는 것이 더 쉬울 것입니다. 이제 DDD의 구성 요소인 실제 방법을 자세히 알아볼 차례입니다.
DDD 빌딩 블록은 효과적이고 효율적인 도메인 모델을 만들기 위한 기반 역할을 합니다. Vlad Khononov는 다음과 같은 방식으로 도메인 모델을 정의합니다.
도메인 모델은 동작과 데이터를 모두 통합하는 도메인의 개체 모델입니다.
도메인 모델은 다양한 빌딩 블록과 구조로 구성됩니다. 가장 중요한 것들은 다음과 같습니다:
값 개체는 사용 가능한 가장 기본적인 구성 요소입니다. 이는 일련의 속성 및 값으로 정의되는 객체입니다. 고유한 식별자가 없습니다. 값이 ID를 정의합니다. 다른 값이 이미 다른 값 개체를 나타내고 있다는 점에서 이는 변경할 수 없습니다. 값 개체의 예는 다음과 같습니다.
다음은 Python에서 간단한 값 개체를 구현하는 방법입니다.
from pydantic import BaseModel class Address(BaseModel): """Customer address.""" country: str city: str street: str house_number: str class Config: frozen = True
이는 두 주소를 비교하기 위해 항등 연산자( ==
)를 사용하면 두 개체에 정확히 동일한 값이 할당된 경우에만 True
반환한다는 의미입니다.
엔터티는 다음 유형의 빌딩 블록입니다. 엔터티는 사람이나 주문과 같은 고유한 ID를 가진 도메인의 개별 개체를 나타냅니다. 데이터를 저장한다는 점에서는 값 개체와 유사하지만 해당 속성은 변경될 수 있고 변경될 것으로 예상되므로 고유 식별자가 필요합니다. 주문 및 개인 정보는 엔터티의 두 가지 간단한 인스턴스입니다.
import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.value_objects import Address class Person(BaseModel): """Personal data.""" id: uuid.UUID = Field(default_factory=uuid.uuid4) first_name: str last_name: str address: Address class Order(BaseModel): """Customer order.""" id: uuid.UUID = Field(default_factory=uuid.uuid4) description: str value: float
두 경우 모두 인스턴스 값을 수정할 수 있으므로 UUID일 수 있는 식별이 필요합니다. 더 중요한 것은 대부분의 경우 엔터티가 직접 관리되지 않고 집계를 통해 관리된다는 것입니다.
Aggregate는 변경 가능하고 고유 식별자가 필요하기 때문에 엔터티 유형입니다. 그러나 주요 책임은 데이터를 저장하는 것이 아니라 관련 개체 집합(엔티티 및 값 개체)을 단일 일관성 단위로 그룹화하는 것입니다. Aggregate는 내부 상태를 캡슐화하고 전체 그룹의 일관성을 보장하기 위해 불변성을 적용하는 잘 정의된 경계가 있는 루트 개체입니다. Aggregate를 사용하면 개체 자체보다는 개체 간의 관계에 초점을 맞춰 보다 자연스럽고 직관적인 방식으로 도메인에 대한 추론이 가능합니다.
이전 예에 이어 집계는 고객으로 표시될 수 있습니다.
import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.entities import Person, Order from practical_ddd.building_blocks.value_objects import Address class Customer(BaseModel): """Customer aggregate. Manages personal information as well as orders. """ id: uuid.UUID = Field(default_factory=uuid.uuid4) person: Person orders: list[Order] = Field(default_factory=list) def change_address(self, new_address: Address) -> None: self.person.address = new_address def add_order(self, order: Order) -> None: if self.total_value + order.value > 10000: raise ValueError("Order cannot have value higher than 10000") self.orders.append(order) def remove_order(self, order_id: uuid.UUID) -> None: order = next((order for order in self.orders if order.id == order_id), None) if order is None: raise IndexError("Order not found") self.orders.remove(order) @property def total_value(self) -> float: return sum(order.value for order in self.orders)
고객은 개인 데이터와 직접 연결되며 모든 주문을 저장합니다. 게다가 집계는 개인의 주소를 관리하고 주문을 추가 및 제거하기 위한 인터페이스를 노출합니다. 이는 해당 메소드를 실행해야만 집계의 상태를 변경할 수 있기 때문입니다.
이전 예는 제약 조건이 하나만 있는 비교적 간단하지만(주문 값은 10000보다 클 수 없음) DDD 구성 요소와 해당 관계의 사용을 보여 줍니다. 실제 시스템에서 집계는 더 많은 제약 조건, 경계 및 더 많은 관계로 인해 더 복잡한 경우가 많습니다. 결국 그들의 존재 자체가 이러한 복잡성을 관리하는 것입니다. 또한 실제 세계에서 집계는 일반적으로 데이터베이스와 같은 데이터 저장소에 유지됩니다. 이것이 저장소 패턴이 작동하는 곳입니다.
집계의 상태 변경은 모두 단일 원자성 작업으로 트랜잭션 방식으로 커밋되어야 합니다. 그러나 "지속"하는 것은 집합체의 책임이 아닙니다. 리포지토리 패턴을 사용하면 데이터 저장 및 검색 의 세부 사항을 추상화하고 대신 더 높은 수준의 추상화에서 집계 작업을 수행할 수 있습니다. 간단히 말해서 리포지토리는 집계와 데이터 저장 사이의 계층으로 간주될 수 있습니다. JSON 파일은 상당히 이러한 저장소의 간단한 예 고객 집계에는 JSON 파일에서 작동하는 저장소가 있을 수 있습니다.
import json import uuid from practical_ddd.building_blocks.aggregates import Customer class CustomerJSONRepository: """Customer repository operating on JSON files.""" def __init__(self, path: str) -> None: self.path = path def get(self, customer_id: uuid.UUID) -> Customer: with open(self.path, "r") as file: database = json.load(file) customer = database["customers"].get(str(customer_id)) if customer is None: raise IndexError("Customer not found") person = database["persons"][str(customer["person"])] orders = [database["orders"][order_id] for order_id in customer["orders"]] return Customer( id=customer["id"], person=person, orders=orders, ) def save(self, customer: Customer) -> None: with open(self.path, "r+") as file: database = json.load(file) # Save customer database["customers"][str(customer.id)] = { "id": customer.id, "person": customer.person.id, "orders": [o.id for o in customer.orders], } # Save person database["persons"][str(customer.person.id)] = customer.person.dict() # Save orders for order in customer.orders: database["orders"][str(order.id)] = order.dict() file.seek(0) json.dump(database, file, indent=4, default=str)
물론 이 클래스는 더 많은 일을 할 수 있고 아마도 해야 하지만 완벽하고 다기능적인 ORM이 되도록 의도된 것은 아닙니다. 이는 저장소 책임에 대한 아이디어를 제공해야 하며, 이 경우 JSON 파일 에서 고객 집계를 저장하고 검색하는 것입니다. 저장소가 집계와 관련된 엔터티를 처리하는 방법도 주목할 가치가 있습니다. 개인 데이터와 주문은 고객 라이프사이클과 긴밀하게 연결되어 있으므로 집계가 처리되는 동안 정확하게 관리되어야 합니다.
고려해야 할 또 다른 경우는 집계나 해당 엔터티 또는 값 개체에 맞지 않는 비즈니스 논리가 있는 경우입니다. 이는 여러 집계 또는 데이터 저장소 상태에 따라 달라지는 논리일 수 있습니다. 이러한 경우 도메인 서비스 라는 구조가 유용할 수 있습니다. 도메인 서비스는 예를 들어 리포지토리를 사용하여 집계를 관리할 수 있어야 하며 그런 다음 집계에 속하지 않는 도메인 논리를 저장할 수 있습니다. 예를 들어 고객은 너무 많은 주문을 잃지 않도록 논리를 요구할 수 있습니다.
import uuid from typing import Protocol from practical_ddd.building_blocks.aggregates import Customer class CustomerRepository(Protocol): """Customer repository interface.""" def get(self, customer_id: uuid.UUID) -> Customer: ... def save(self, customer: Customer) -> None: ... class CustomerService: """Customer service.""" def __init__(self, repository: CustomerRepository) -> None: self.repository = repository def get_customer(self, customer_id: uuid.UUID) -> Customer | None: try: return self.repository.get(customer_id) except IndexError: return None def save_customer(self, customer: Customer) -> None: existing_customer = self.get_customer(customer.id) # If customer is already in the database and has more than 2 orders, # he cannot end up with half of them after a single save. if ( existing_customer is not None and len(existing_customer.orders) > 2 and len(customer.orders) < (len(existing_customer.orders) / 2) ): raise ValueError( "Customer cannot lose more than half of his orders upon single save!" ) self.repository.save(customer)
Aggregate는 애초에 JSON 파일에 대한 지식이 없기 때문에 상태가 JSON 파일의 상태와 어떻게 다른지 확인할 수 없습니다. 그렇기 때문에 도메인 서비스에는 비교 논리가 포함되어야 합니다. 도메인 서비스가 저장소 추상화와 함께 작동해야 한다는 점에 유의하는 것도 중요합니다. 이를 통해 종속성 주입을 사용하여 구체적인 구현을 대체 구현으로 간단하게 교체할 수 있습니다.
이제 모든 부분이 다루어졌으므로 이제 작업 프로그램으로 볼 수 있습니다.
import uuid from practical_ddd.building_blocks import aggregates, entities, value_objects from practical_ddd.database.repository import CustomerJSONRepository from practical_ddd.service import CustomerService # Initialize domain service with json repository srv = CustomerService(repository=CustomerJSONRepository("test.json")) # Create a new customer customer = aggregates.Customer( person=entities.Person( first_name="Peter", last_name="Tobias", address=value_objects.Address( country="Germany", city="Berlin", street="Postdamer Platz", house_number="2/3", ), ), ) srv.save_customer(customer) # Add orders to existing customer customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.add_order(entities.Order(description="Order 1", value=10)) customer.add_order(entities.Order(description="Order 2", value=210)) customer.add_order(entities.Order(description="Order 3", value=3210)) srv.save_customer(customer) # Remove orders from existing customer # If there are only 3 orders, it's gonna fail customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.remove_order(uuid.UUID("0f3c0a7f-67fd-4309-8ca2-d007ac003b69")) customer.remove_order(uuid.UUID("a4fd7648-4ea3-414a-a344-56082e00d2f9")) srv.save_customer(customer)
모든 것에는 책임과 경계가 있습니다. Aggregate는 엔터티와 값 개체를 관리하고 제약 조건을 적용하는 일을 담당합니다. 도메인 서비스는 삽입된 JSON 리포지토리를 사용하여 JSON 파일의 데이터를 유지하고 추가 도메인 경계를 적용합니다. 결국 각 구성 요소는 지정된 영역 내에서 고유한 기능과 의미를 갖습니다.
도메인 기반 디자인은 의심할 여지 없이 이해하기 복잡한 아이디어입니다. 소프트웨어 팀이 비즈니스 영역에 중점을 두어 가장 어려운 비즈니스 문제를 해결하는 데 도움이 되는 사례, 패턴 및 도구를 제공합니다. 그러나 DDD는 단순한 빌딩 블록 세트 그 이상입니다. 이는 기술 이해관계자와 비즈니스 이해관계자 간의 협업과 의사소통이 필요한 사고방식입니다. 유비쿼터스 언어를 통해 표현되는 도메인에 대한 공유된 이해는 DDD 프로젝트의 성공에 매우 중요합니다. 잘 수행되면 DDD는 비즈니스 요구 사항에 더 잘 부합하고 복잡한 문제를 해결하는 데 더 효과적인 소프트웨어로 이어질 수 있습니다.
이 기사는 "DDD: From Zero To Hero"와 같은 내용을 의도한 것이 아니라 DDD 세계에 대한 소개를 제공하기 위한 것입니다. 저는 도메인 기반 디자인의 가장 중요한 개념을 매우 간단하고 실용적인 방식으로 보여주고 싶었습니다. 저는 도메인 중심 디자인을 배우는 것이 프로그래밍 전문성을 높이는 훌륭한 방법이라고 믿습니다. 그러나 이에 대해 너무 자주 듣지는 않습니다. 적어도 "11가지 미친 JavaScript 팁과 요령 - 스레드 🧵" 만큼은 아닙니다.
어쨌든, 이 내용이 흥미로웠다면 처음에 제가 이 글을 쓰도록 영감을 준 책과 기사의 출처 섹션을 살펴보세요. 이 소개의 범위를 벗어나는 것으로 생각되어 다루지 않은 몇 가지 개념이 있지만 조사해 볼 가치가 있습니다.
의심할 여지없이 아래 나열된 소스에서 찾을 수 있습니다.
기사에 사용된 코드 예제는 여기( 링크) 에서 찾을 수 있습니다.
Vlad Khononov의 도메인 중심 설계 학습 . 나에게 큰 영감의 원천이 된 놀라운 책. 이 문서에서 논의된 모든 개념을 더 자세히 설명합니다.
Harry Percival과 Bob Gregory가 작성한 Python의 아키텍처 패턴 . 저는 거의 2년 전에 이 책을 읽었고, 개발자로서 제게 큰 영향을 미쳤습니다. 이 글을 쓰면서 다시 찾아봤는데, 또 한 번 도움이 되었습니다.
Przemysław Górecki가 작성한 Python의 DDD입니다 . 나는 이 블로그를 기사 작성이 끝날 무렵에 발견했지만, 그것이 얼마나 전문적인지 때문에 내 관심을 불러일으켰습니다. 재미있는 사실: 저는 Przemysław와 같은 회사에서 일했는데, 전혀 몰랐습니다.
여기에도 게시되었습니다.