-
도메인 주도 설계의 적용 - 2. 애그리게이트와 리포지토리 1부[옛날 글들] 도메인 주도 설계 2024. 4. 11. 01:19728x90
이전글 : 도메인 주도 설계의 적용 - 1. 값 객체와 참조 객체 4부
이 글은 제가 2008년 6월부터 10월까지 5개월간 마이크로소프트웨어에 연재했던 "도메인 주도 설계의 적용"이라는 원고의 원글입니다. 잡지에 맞추어 편집을 하는 과정에서 지면 상의 제약으로 인해 수정되거나 삭제된 부분이 있어 제 블로그에 원글을 올립니다. 도메인 주도 설계(Domain-Driven Design)에 관심 있는 분들에게 도움이 되었으면 합니다.
시너지를 생각하라. 전체는 부분의 합보다 크다
- Stephen R. Covey흔하디 흔한 주문 도메인
다음은 모델링의 단골 주제인 상품 주문에 관한 도메인 모델이다.
그림 1 주문 도메인 모델
고객(Customer)은 시스템을 사용해서 상품을 주문(Order)한다. 한 번 주문 시 다수의 상품(OrderLineItem)을 구매할 수 있으며 상품에 대한 이름(name), 가격(price)과 같은 기본 정보는 별도의 상품(Product) 클래스에 정의되어 있다. 고객은 고객 등급에 따라 일 회 주문 시 구매 가능한 금액에 제한(limitPrice 프로퍼티)을 받는다.
앞의 도메인 모델에서 Customer 클래스와 Money 클래스를 표기한 방법에 주목하자. 일반적으로 참조 객체는 독립적인 클래스로 표기하는 반면, 값 객체는 클래스의 속성으로 표기한다. 좋은 모델이란 충분한 정보를 다루면서도 세부 사항에 집착하지 않고 핵심 주제를 효과적으로 전달하는 모델이다. 주문 도메인의 핵심 주제는 도메인을 구성하는 고객(Customer), 주문(Order), 주문 항목(OrderLineItem), 상품(Product)이라는 개념과 이들 간의 상호 관계이다. 상대적으로 중요도가 낮은 금액(Money)을 프로퍼티로 표현함으로써 경제적이면서도 의미 전달이 명확한 모델을 만들 수 있다.
앞의 모델과 다음 모델을 효율성 측면에서 비교해 보자.
그림2 비경제적인 모델. 핵심 주제를 파악하기 힘들다.
주문 시 고객은 정해진 한도 내에서 상품을 구매할 수 있다. 이것은 일종의 비즈니스 룰로 볼 수 있다. 그렇다면 한도 초과 여부를 검증하는 책임을 어떤 도메인 객체에게 할당해야 할까?
한도액 검증 책임을 고객 객체에게 할당한다고 가정해 보자. 얼핏 보면 고객 객체가 자신의 주문 한도(limitPrice 프로퍼티)를 알고 있기 때문에 초과 한도를 검증하는 것이 논리적으로 타당해 보인다. 그러나 한도액을 초과했는지 여부를 검증하기 위해서는 고객 객체가 주문 객체의 내부 상태를 알고 있어야 한다. 따라서 고객과 주문 간에 양방향 연관 관계가 발생한다. 양방향 연과 관계는 도메인 모델 간의 결합도를 높이고 관계 간의 일관성을 유지하기 위해 필요한 구현 복잡도를 증가시키기 때문에 가능하다면 양방향 연관 관계를 사용하지 않는 것이 좋다.
나아가 고객 객체에게 한도액 검증 책임을 할당하는 것은 기능에 대한 욕심(Feature Envy)이라는 아주 고약한 냄새가 난다. 기능에 대한 욕심(Feature Envy)은 메소드가 자신의 내용보다 다른 클래스의 내용에 더 관심을 가질 경우 나타나는 코드 속의 나쁜 냄새로, 결과적으로 고객 객체가 자신이 몰라도 상관없는 주문의 상세 정보까지 눈독을 들임으로써 객체 간의 결합도가 높아지는 문제를 낳는다. 이것은 정보를 가지고 있는 클래스에 책임을 할당하라는 INFORMATIN EXPERT 패턴에 위반된다.
이제 한도액 검증 책임을 주문(Order) 객체에게 할당해 보자. 한도액 검증을 위해서는 고객 객체의 주문 한도액(limitPrice 프로퍼티)을 알아야 한다. 이 경우 이미 주문 객체에서 고객 객체로의 연관 관계가 설정되어 있기 때문에, 양방향 연관 관계를 추가하지 않고도 주문 한도액을 검증하는 것이 가능하다. 이것은 객체의 책임 할당과 관련하여 LOW COUPLING과 HIGH COHESION 패턴을 준수한다. 또한 주문에 포함된 정보를 사용하여 한도를 검증하기 때문에 기능에 대한 욕심(Feature Envy)이나 INFORMATIN EXPERT 패턴 위반이 발생하지 않는다. 따라서 한도액 검증 책임은 주문 객체에게 할당하는 것이 적절하다.
주문에 주문 항목을 추가하는 시나리오를 생각해 보자. 고객은 상품을 선택하고 상품의 개수를 입력한다. 시스템은 상품과 개수를 가진 주문 항목을 생성하고 주문에 추가한다. 주문은 새로 추가된 주문 항목의 가격을 더한 주문 총액과 구매 고객의 한도액을 비교한다. 만약 한도를 초과했다면 예외를 발생시키고 주문 프로세스를 중단한다.
이 시나리오로부터 주문 시 한도액을 검증하기 위해서는 주문 객체와 주문 항목 객체 간의 긴밀한 협력이 필요하며, 상태 변경 시 이들이 하나의 단위로 취급되어야 한다는 것을 알 수 있다. 즉, 주문 객체와 주문 항목 객체들은 구매액이 고객의 주문 한도액을 초과할 수 없다는 불변식(invariant)을 공유하는 하나의 논리적 단위라고 할 수 있다.
주문과 주문 항목 간의 불변식을 유지하기 위해서는 주문에 주문 항목이 추가된 이후에도 외부에서 직접적으로 주문 항목을 수정할 수 없도록 해야 한다. 외부에서 주문을 우회해서 주문 항목의 상태를 임의로 변경할 수 있다면 한도액에 관한 불변식을 유지하는 것이 불가능해진다. 따라서 주문 항목은 외부에 노출되지 않아야 하며 주문 항목의 추가, 수정, 삭제는 반드시 주문의 제어 하에 수행되어야 한다. 간단하게 말해서 주문은 주문 항목을 캡슐화해야 한다.
주문과 주문 항목을 하나의 클러스터로 취급하기로 결정했다면 이제 상품(Product)에 관해 고민해 보자. 주문-주문 항목 클러스터에 상품 객체를 포함시킬 수 있을까? 아니면 상품 객체 역시 고객 객체처럼 주문-주문 항목 클러스터 외부의 객체로 보아야 할까?
이 문제를 논의하기 위해 실행 콘텍스트를 다중 사용자 환경(또는 다중 쓰레드 환경)으로 확장해 보자. 다중 사용자 환경에서 운영되는 주문 처리 시스템에서는 한 명 이상의 사용자가 동시에 시스템을 사용할 수 있다. 따라서 동일한 주문을 두 명의 사용자가 동시에 수정할 수 있으며, 이것은 골치 아픈 일관성 문제를 야기한다.
150,000원의 구매 한도를 가진 고객이 “Refactoring” 한 권, “Design Pattern” 두 권, ”Code Complete” 한 권을 구입했다고 하자. 다음 그림은 앞에서 제시한 도메인 모델을 기반으로 주문이 완료된 상태를 오브젝트 다이어그램으로 나타낸 것이다.
그림 3 주문의 초기 상태
여기에 동시성이라는 양념을 뿌려 보자. 여기 주문을 사이에 두고 암투를 벌이는 두 명의 사용자가 있다. 첫 번째 사용자는 “Design Pattern”을 한 권 줄이고 “Refactoring”을 한 권 더 구매하는 것으로 주문 정보를 수정한다. 전체 구매액은 110,000원으로 고객의 주문 한도액을 초과하지 않으므로 수정이 가능하다.
그림 4 첫 번째 사용자 관점에서 본 주문 수정 결과
이때 두 번째 사용자가 첫 번째 사용자와 거의 동시에 “Refactoring”을 주문 항목에서 제외하고 “Code Complete”을 두 권 구입하는 것으로 수정한다고 하자. 전체 구매 금액은 120,000원으로 이 역시 주문 한도액을 초과하지 않으므로 수정 사항이 반영된다.
그림 5 두 번째 사용자 관점에서 본 주문 수정 결과그러나 어느 날 갑자기 평화롭던 두 사용자의 인생에 동시성이라는 불길한 그림자가 드리워지기 시작한다. 두 사용자의 요청이 단일 프로세서를 탑재한 시스템 상에서 별도의 스레드로 처리된다고 가정하고 다음 시나리오를 살펴보자..
첫 번째 사용자의 스레드가 수정 사항에 대한 불변식 검증을 통과하자마자 콘텍스트 스위칭이 일어나 두 번째 사용자의 수정 요청에 대한 불변식 검증 역시 통과된다. 다시 컨텍스트 스위칭이 일어나 첫 번째 사용자의 수정 내역이 주문에 반영되고, 다시 컨텍스트 스위칭이 일어나 두 번째 사용자의 수정 내역도 주문에 반영된다. 따라서 각각의 사용자 입장에서는 주문에 대한 불변식이 지켜졌으나 미묘한 동시성 문제로 인해 전체 시스템의 관점에서 보면 무결성이 깨져 버렸다.
그림 6 미묘한 동시성 문제로 인한 불변식 위반
이처럼 미묘한 동시성 문제로 인한 불변식 위반을 방지하기 위해서는 주문-주문 항목 클러스터에 대한 배타적인 접근이 가능해야 한다. 이를 위해 주문에 대한 잠금(lock) 설정이 가능해야 한다. 첫 번째 사용자가 주문을 얻을 때 다른 사용자가 동일한 주문을 얻을 수 없도록 이를 잠근다. 두 번째 사용자가 주문에 접근할 때는 이미 첫 번째 사용자가 주문을 소유하고 있으므로 첫 번째 사용자가 주문에 대한 잠금을 해제할 때까지 대기하고 있게 된다. 주문-주문 항목 전체가 잠기기 위해서는 두 번째 사용자가 주문을 우회해서 주문 항목에 접근하는 경우는 없어야 한다. 따라서 주문 항목은 항상 주문을 통해서만 접근 가능해야 함을 알 수 있다.
이 경우 주문 항목과 연관된 상품(Product) 객체는 어떻게 처리해야 할까? 주문, 주문 항목과 함께 상품 역시 잠금을 설정하여 배타적인 접근이 가능하도록 해야 할까? 주문 항목은 각 주문의 일부이며 하나의 주문에 의해서만 참조되기 때문에 주문과 함께 잠기더라도 시스템의 성능에 영향을 미치지 않는다. 그러나 상품의 경우는 이야기가 다르다. 상품은 하나 이상의 주문에 의해 참조된다. 따라서 주문을 잠글 때마다 연결된 모든 상품을 함께 잠근다면 해당 상품에 접근하려는 모든 주문 객체가 동시에 대기 상태로 빠지는 결과를 낳는다. 상품은 주문과 주문 항목과 달리 높은 빈도의 경쟁(high contention)이 발생하는 객체이다.
그림 7 orderId가 1인 주문을 잠글 때 orderId가 2인 주문이 잠겨서는 안 됨
또한 주문과 주문 항목이 변경되는 빈도에 비해 상품의 변경 빈도(the frequency of change)는 상대적으로 매우 낮다. 즉, 주문과 주문 항목이 끊임없는 비즈니스의 흐름 속에서 매우 빈번하게 생성, 수정, 삭제되는데 비해 상품의 명칭 및 가격의 수정, 신규 상품의 추가 및 상품의 삭제는 빈번하게 발생하지 않는다. 주문과 주문 항목의 수정이 거의 비슷한 시점에 발생하는데 비해 상품의 수정 시점은 주문, 주문 항목의 수정 시점과는 무관하다.
따라서 주문, 주문 항목은 하나의 객체 클러스터를 구성하며, 고객, 상품은 주문 클러스터에 속하지 않는 독립적인 객체로 존재한다. 이처럼 변경에 대한 불변식을 유지하기 위해 하나의 단위로 취급되면서 변경의 빈도가 비슷하고, 동시 접근에 대한 잠금의 단위가 되는 객체의 집합을 애그리게이트(AGGREGATE)라고 한다.
그림 8 주문 애그리게이트728x90'[옛날 글들] 도메인 주도 설계' 카테고리의 다른 글
도메인 주도 설계의 적용 - 2. 애그리게이트와 리포지토리 3부 (0) 2024.04.12 도메인 주도 설계의 적용 - 2. 애그리게이트와 리포지토리 2부 (0) 2024.04.11 도메인 주도 설계의 적용 - 1. 값 객체와 참조 객체 4부 (0) 2024.04.10 도메인 주도 설계의 적용 - 1. 값 객체와 참조 객체 3부 (0) 2024.04.10 도메인 주도 설계의 적용 - 1. 값 객체와 참조 객체 2부 (0) 2024.04.10