Eternity's Chit-Chat

aeternum.egloos.com



Domain-Driven Design의 적용-2.AGGREGATE와 REPOSITORY 1부 Domain-Driven Design

PART 2. AGGREGATE와 REPOSITORY
 

시너지를 생각하라. 전체는 부분의 합보다 크다

- Stephen R. Covey

흔하디 흔한 주문 도메인

다음은 모델링의 단골 주제인 상품 주문에 관한 도메인 모델이다.

 

그림 1 주문 도메인 모델

 

고객(Customer)은 시스템을 사용해서 상품을 주문(Order)한다. 한 번 주문 시 다수의 상품(OrderLineItem)을 구매할 수 있으며 상품에 대한 이름(name), 가격(price)과 같은 기본 정보는 별도의 상품(Product) 클래스에 정의되어 있다. 고객은 고객 등급에 따라 일 회 주문 시 구매 가능한 금액에 제한(limitPrice 프로퍼티)을 받는다.

 

앞의 도메인 모델에서 Customer 클래스와 Money 클래스를 표기한 방법에 주목하자. 일반적으로 REFERENCE OBJECT는 독립적인 클래스로 표기하는 반면, VALUE OBJECT는 클래스의 속성으로 표기한다. 좋은 모델이란 충분한 정보를 다루면서도 세부 사항에 집착하지 않고 핵심 주제를 효과적으로 전달하는 모델이다. 주문 도메인의 핵심 주제는 도메인을 구성하는 고객(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 주문 AGGREGATE

핑백

  • Domain-Driven Design | Jongmin Kim's Blog 2014-09-02 01:18:35 #

    ... main-Driven Design의 적용-2.AGGREGATE와 REPOSITORY 2부 2008/11/20 Domain-Driven Design의 적용-2.AGGREGATE와 REPOSITORY 1부 2008/11/17 Domain-Driven Design의 적용-1.VALUE OBJECT와 REFE ... more

덧글

  • KKK 2015/10/14 04:17 # 삭제

    좋은글 잘보고 갑니다. 그런데..내용이 블루북에서 고대로 가져오신거 같은데 언급이 없으시네요. 블루북에 나온 예제인데;;;
  • 이터너티 2015/10/14 10:56 #

    이 글 전체가 대부분 블루북을 참고로 해서 작성된 거라서요
  • KKK 2015/10/22 02:56 # 삭제

    예 그건 알겠는데 완전 고대로 가져오시고 여기저기 한글로 변경만 하신거 같은데...원글에 대한 언급이 없으시네요.
  • 이터너티 2015/10/23 21:26 #

    어떤 부분을 완전 고대로 가져왔다고 말씀 하시는지 이해를 못 하겠네요.
    주문 모델링은 가장 잘 알려져 있고 일반적인 모델인데 주문이라는 예제를 동일하게 사용했다고 해서 단순하게 한글로 번역만 했다고 말씀하시는 것은 이해할 수 없습니다.
    제 연재가 단순하게 블루북만 참고한게 아니고 많은 무수한 책들을 참고했고 코드 자체는 완전히 새로 작성한 것인데도 블루북을 그대로 베꼈다고 생각하시나요?
    블루북과 제 글을 비교해서 어떤 부분이 "완전 고대로 가져와서 한글로 번역만했다고 하시는지" 알려 주시면 납득하겠습니다.
    책에 대해 언급한 부분이 필요하시다면 아래 링크 참고하세요.
    http://aeternum.egloos.com/1165089

    제 글뿐만 아니라 다른 분들이 쓰신 글들 모두 힘들고 고생스러운 과정을 참고 견디면서 애정을 쏟아 부어서 탄생한 글입니다.
    이런 식의 비난은 예의가 아닌 것 같네요.
  • KKK 2015/10/24 08:50 # 삭제

    고대로 가져왔다는 말은 정정하겠습니다. 책을 읽은지 오래되서 예제를 착각했네요. 이 점 사과드립니다.

    일단 그림 예제에 사용된 오브젝트들은 다르게 표현됬습니다만 글의 진행 과정...오더에 관련된 예제로 시작해서 한도액이 불변식으로 나오는 전개는 똑같습니다. 또한 DDD 컨텍스에서 Agreegate를 다룬다는 점에서도 똑같구요.

    챕터 6에 Aggregates에서 보고 말씀드리는 겁니다. DDD에 관해서 처음 나온 책이고 지금 이 블로그의 이 카테고리에서 DDD 에 대해서 다루고 있으면서 이렇게 설명의 전개가 똑같은데 여러 책에서 보고 여러명에서 같이 썼다고 하시면 할말이 없습니다.

    말씀드린데로 한글로 된 글중에서 아주 설명이 최고로 잘되어 있습니다만..글의 좋고 나쁨을 떠나서 전개 과정이 똑같은데 아무런 언급도 없으신점은 이해가 안되네요. 분명 블루북과 레드북에 많은 영향을 받으셨을텐데요...
  • 이터너티 2015/10/28 00:24 #

    당연히 Aggregate 관련 내용인데 책의 Aggregate 부분을 참고해서 썼겠죠. ㅡㅡ;;

    그리고 "여러 책에서 보고 여러명에서 같이 썼다고 하시면 할말이 없습니다."라고 하셨는데 제 답글을 다시 읽어 보시면 이 부분이 아니라 "연재 전체"라고 분명히 말씀 드렸습니다.

    하지만 책에서 Aggregate 예제를 설명하는 부분이 코드도 없고, 단순한 표 형식으로 되어 있어 이해하기 어렵다고 판단되서 UML 다이어그램과 코드를 이용해서 이해하기 쉽게 풀어서 설명한 부분입니다.

    이렇게 풀어서 쓴 부분을 모두 표시한다면 아마 이 연재의 모든 페이지마다 여러 책들을 인용했다는 말을 붙여야 했겠죠.

    책에서 난해하게 설명한 부분을 명확하게 풀어서 설명한 것도 "고대로 배낀거라고 생각하신다면" 저 역시 더 이상 할 말이 없네요.

    KKK님의 논리대로라면 어려운 책을 쉽게 풀어서 쓴 모든 글들이 표절이 될테니까요.

    글의 전체가 아니라 일부분의 전개만 보고 표절이라고 판단되신다면 어쩔 수 없네요.

    익명이셔서 KKK님의 블로그나 쓰신 글을 볼 수 없는데 링크 부탁드립니다.

    어떻게 글을 쓰시는지 참고하고 싶네요.
  • bluepoet 2016/01/25 13:04 # 삭제

    영호님 한가지 질문이 있는데요.

    글 중간에

    두 번째 사용자가 첫 번째 사용자와 거의 동시에 “Refactoring”을 주문 항목에서 제외하고 “Code Complete”을 세 권 구입하는 것으로 수정한다고 하자.

    그림을 보니까 “Code Complete”을 세 권이 아니고 "두 권"인 것 같은데 맞나요?

    좋은 글 항상 잘 보고 있습니다.

    감사합니다^^
  • 이터너티 2016/01/25 23:28 #

    네 두 권이 맞네요.

    수정했습니다 ^^

    오류를 발견해 주셔서 감사합니다
※ 로그인 사용자만 덧글을 남길 수 있습니다.