[ddd] 아키텍쳐

4개의 영역

아키텍쳐를 설계할 때 출현하는 전형적인 영역은 아래와 같다

  • 표현

    HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고, 응용 영역의 응답을 HTTP 응답으로 변환해서 전송한다
    e.g. 요청 파라미터를 객체로 받고 결과를 JSON으로 리턴

  • 응용

    시스템이 사용자에게 제공해야 할 기능을 구현한다

    • 응용 영역은 기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다
    • 응용 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다
  • 도메인

    도메인의 핵심 로직을 구현한다
    e.g. 주문 도메인의 경우 ‘배송지 변경’, ‘결제 완료’ 같은 핵심 로직을 도메인 모델에서 구현한다

  • 인프라스트럭쳐

    구현 기술에 대한 것을 다룬다
    e.g. RDBMS 연동, 몽고 DB, 메시지 큐 전송 등

계층 구조 아키텍쳐

1
2
3
4
5
6
7
   표현

응용

도메인

인프라스트럭쳐
  • 계층 구조는 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층에서 상위 계층에 의존하지는 않는다
  • 계층 구조를 엄격하게 적용하면 상위 계층은 바로 아래 계층에만 의존을 가져야하지만, 구현의 편리함을 위해 계층 구조를 유연하게 적용한다

이 말인 즉, 표현, 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭쳐에 의존할 수 있다는 점이다
예를 들면 아래처럼 될 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CalculateDiscountService { 
private DroolsRuleEngine ruleEngine = new DroolsRuleEngine();

public Money calculateDiscount(List<OrderLine> orderLines, String customerId) {
Customer customer = findCustomer(customerId);

// 초기 돈
MutableMoney money = new MutableMoney(0);

// 조건들 추가하고
List<?> facts = Arrays.asList(customer, money);
facts.addAll(orderLines);

// DroolsRulsEngine을 이용해 할인율 적용
ruleEngine.evaluate("discountCalculation", facts);

return money.toImmutableMoney();
}
}

위 처럼 도메인에 메시지를 보내는 것 외에 특정 엔진을 사용해야하는 상황이다
(특정 엔진을 사용하는 것이 더 나은 상황)

하지만 위 코드는 아래 2가지 문제점을 가지고 있다

  • 테스트하기 어렵다
    • DroolsRuleEngine이 완벽하게 동작해야만 CalculateDiscountService를 테스트할 수 있다
  • 구현 방식을 변경하기 어렵다
    • DroolsRuleEngine이 아니라 다른 엔진을 사용하도록 변경하고자 한다면 많은 부분이 변경되어야 할 것이다

고수준 모둘이 제대로 동작하려면 저수준 모듈을 사용해야 하는데, 인프라스트럭쳐의 경우 특정 기술을 직접 구현하므로 이런 문제점이 발생하게 된다.
이를 어떻게 처리할 수 있을까?

DIP

정답은 저수준 모델이 고수준 모델에 의존하도록 바꾸는 것이다
다시 한번 CalculateDiscountService를 살펴보면, discount를 얻는데 어떤 엔진을 사용했느냐는 중요하지 않다
단지 고객정보와 구매정보에 룰을 적용해서 할인 금액을 구한다는 것이 중요할 뿐이다
이 부분을 추상화해서 인터페이스로 만들 수 있다

1
2
3
interface RuleDiscounter {
public Money applyRules(Customer customer, List<OrderLine> orderLines);
}

이 인터페이스를 사용하여 CalculateDiscountService에서 DroolsRuleEngine을 제거할 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CalculateDiscountService { 
private RuleDiscounter ruleDiscounter;

public CalculateDiscountService(RuleDiscounter ruleDiscounter) {
this.ruleDiscounter = ruleDiscounter;
}

public Money calculateDiscount(List<OrderLine> orderLines, String customerId) {
Customer customer = findCustomer(customerId);
return ruleDiscounter.applyRules(customer, orderLines);
}
}

class DroolsRuleDiscounter implements RuleDiscounter {
// ...
}

CalculateDiscountService는 더 이상 구현기술인 Drools(저수준)에 의존하지 않고,
룰을 이용한 할인 금액 계산 을 표현하는 RuleDiscounter 인터페이스(고수준)에 의존한다

저수준이 고수준에 의존

그림에서 보이다시피 고수준 모듈이 저수준 모듈을 사용함에도 불구하고 저수준 모듈이 고수준 모듈에 의존하고 있다
이를 DIP(Dependency Inversion Principle, 의존 역전 원칙) 이라고 부른다

그리고 인터페이스를 구현한 저수준 모듈은 외부에서 생성해 주입(Dependency Injection) 해주게 된다

이와 같이 DIP를 적용함으로써 기존의 고수준 모듈에서 저수준 모듈 사용에서 오던 문제점들을 해결할 수 있게 된다

  1. 테스트하기 쉬워진다
    • 특정 클래스가 아니라 인터페이스에 의존하므로 mockito를 사용한 stub 등을 사용한다면 직접 구현체를 구현하지 않고도 테스트를 진행할 수 있게 된다
  2. 구현 방식을 변경하기 숴워진다
    • 저수준 모듈에 강하게 결합되어 있는 구조가 아니기 때문에, 구현 방식을 변경하고 싶다면 인터페이스를 구현한 구현체를 하나 더 만들어서 DI 해주면 CalculateDiscountService의 코드변경 없이 구현 방식을 변경할 수 있다(OCP)

DIP 주의사항

잘못된 DIP

DIP 결과 구조만 보고 인터페이스를 잘못 추출한 결과이다
RuleEngine은 고수준 모델인 도메인 관점이 아니라 엔진이라는 저수준 모듈 관점에서 도출된 것이다
즉, 여전히 고수준 모듈이 저수준 모듈에 의존하고 있는 셈이다

DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해야 한다

도메인 영역의 주요 구성요소

  1. 엔티티

    고유의 식별자를 가지고 자신의 라이프 사이클을 갖는 객체
    데이터와 데이터와 관련된 기능을 함께 제공한다

  2. 벨류

    고유의 식별자를 갖지 않고 주로 도메인 객체의 속성을 표현할 떄 사용되는 객체
    다른 벨류 타입의 속성으로도 사용될 수 있다

  3. 애그리거트

    관련된 엔티티와 벨류 객체를 개념적으로 하나로 묶은 것

  4. 리포지터리

    도메인 모델의 영속성을 처리함

  5. 도메인 서비스

    특정 엔티티에 속하지 않은 도메인 로직을 제공함
    도메인 로직이 여러 엔티티와 벨류를 필요로 할 경우 여기에서 로직을 구현한다

엔티티와 벨류

도메인 모델의 엔티티와 DB 모델의 엔티티는 다르다

  • 도메인 모델의 엔티티는 단순히 데이터를 담고 있는 데이터 구조라기보다는 데이터와 함께 기능을 제공하는 객체이다
    • 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화해서 데이터가 임의로 변경되는 것을 막는다
  • 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 벨류 타입을 이용해서 표현할 수 있다
    • RDBMS는 벨류를 제대로 표현하기 힘들다

      ORDER_NAME, ORDER_EMAIL 필드로 표시하거나 또는 ORDER_ORDERER 테이블로 표시하더라도 딱히 벨류 타입의 느낌을 주지 못한다

애그리거트

도메인이 커질수록 개발할 도메인 모델도 커지게 되고, 많은 엔티티와 벨류가 생기면서 모델이 점점 더 복잡해진다
이렇게 도메인 모델이 복잡해지면 개발자가 전체 구조가 아닌 한개 엔티티와 벨류에 집중하게 되는 경우가 발생한다

지도를 볼 때 매우 상세하게 나온 대축적 지도를 보면 큰 수준에서 어디에 위치하고 있는지 이해하기 어려우므로 큰 수준에서 보여주는 소축적 지도를 함꼐 봐야 현재 위치를 보다 더 정확하게 이해할 수 있다
이와 비슷하게 도메인 모델도 개별 객체가 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델과 개별 모델을 이해하는데 도움이 된다
이게 바로 애그리거트(AGGREGATE) 이다

주문 애그리거트는 주문, 주문자, 배송정보 등을 포함한다

애그리거트는 군집에 속한 객체들을 관리하는 루트 엔티티를 가진다

  • 루트 엔티티는 애그리거트에 속해 있는 엔티티와 벨류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다
  • 애그리거트를 사용하는 코드는 애그리거트가 제공하는 기능을 실행하고 애그리거트 루트를 통해서 간접적으로 애그리거트 내의 엔티티나 벨류 객체에 접근하게 된다

애그리거트를 구현할 때는 고려할 것이 많다

리포지터리

도메인 객체를 지속적으로 사용하려면 RDBMS 같은 물리적 저장소에 도메인 객체를 보관해야 하고, 이를 위한 도메인 모델이 리포지터리 이다

리포지토리는 애그리거트 단위로 도메인 객체를 조회하고 저장하는 기능을 제공한다

리포지터리는 도메인 객체를 영속화하는데 필요한 기능을 추상화 한 것이기 떄문에 고수준 모듈에 속하고, 실제 구현 클래스는 인프라스트럭쳐 영역에 속한다

리포지터리의 사용 주체는 응용 서비스이다

  • 응용 서비스는 필요한 도메인 객체를 구하거나 저장할 때 리포지터리를 이용한다
  • 그러므로 응용 서비스가 필요로 하는 메서드를 제공한다
    • 기본은 저장식별자로 조회 메서드이다

요청 처리 흐름

웹어플리케이션 요청처리 흐름

응용 서비스는 도메인 모델을 이용해서 기능을 구현한다
도메인 객체를 조회해야하거나 새로 생성해야 할 경우 리포지터리를 이용한다

인프라스트럭쳐

표현, 응용, 도메인 영역을 모두 지원하는 영역이다
DIP에서 언급했듯이 인프라스트럭쳐를 직접 사용하는 것 보단 각자의 영역에서 정의한 인터페이스를 인프라스트럭쳐 영역에서 구현하는 것이 시스템을 더 유연하고 테스트하기 쉽게 만들어준다

하지만 무조건 인프라스트럭쳐에 대한 의존을 없애는 것이 좋은 것은 아니다
예를 들면 스프링의 @Transactional이 있다
코드에서 스프링에 대한 의존을 없애려면 복잡한 스프링 설정을 사용해야하는데, 이럴때는 굳이 의존을 없애지 않는 것이 좋다
구현의 편리함 또한 다른 장점들만큼 중요한 부분이기 때문이다

표현 영역 또한 인프라스트럭쳐와 쌍을 이룬다. 그것도 항상.

모듈 구성

기본적으로 우리는 항상 아키텍쳐가 각 영역의 별도 패키지에 위치하는 형태로 구성한다

1
2
3
4
com.bookstore.ui
com.bookstore.application
com.bookstore.domain
com.bookstore.infrastructure

애그리거트의 모델과 리포지터리는 같은 패키지에 위치시킨다

하지만 패키지 구성 방식에 정답이 있는것은 아니다

도메인이 크면 하위 도메인마다 별도 패키지를 구성할 수 있다

1
2
3
4
5
6
7
8
9
10
11
com.bookstore.catelog.ui
com.bookstore.catelog.application
com.bookstore.catelog.domain
com.bookstore.catelog.infrastructure

com.bookstore.order.ui
com.bookstore.order.application
com.bookstore.order.domain
com.bookstore.order.infrastructure

...

만약 카탈로그 도메인이 상품 애그리거트와 카테고리 애그리거트로 구성된다면 domain에서 패키지를 나눌 수 있다

1
2
3
4
5
com.bookstore.catelog.ui
com.bookstore.catelog.application
com.bookstore.catelog.domain.product
com.bookstore.catelog.domain.category
com.bookstore.catelog.infrastructure

만약 도메인 서비스 계층이 따로 있으면 아래와 같이 나눌수도 있다

1
2
3
com.bookstore.catelog.domain.product
com.bookstore.catelog.domain.category
com.bookstore.catelog.domain.service

service는 product와 category 의 도메인 서비스 계층이다

이러한 룰들은 domain 뿐만이 아니라 다른 계층에도 적용될 수 있다
그리고 알다시피, 모듈 구성에 정답은 없다
하지만 가능하면 한 패키지내에 10개 미만으로 타입 개수를 유지하는 것이 좋다

참고 : 최범균, 『DDD Start!』, 지앤선(2016)