본문 바로가기
programming/DDD

[DDD] 2장 아키텍처 개요

by yhsim98 2022. 11. 21.

네 개의 영역

  • 표현, 응용, 도메인, 인프라스트럭처는 아키텍처를 설계할 때 출현하는 전형적인 네 가지 영역이다
  • 표현 영역
    • 네 영역 중 표현(또는 UI영역)은 사용자의 요청을 받아 응용 영역에 전달하고, 응용 영역의 처리 결과를 다시 사용자에게 보여주는 역할을 한다
    • 스프링 MVC 프레임워크가 표현 영역을 위한 기술이다, 표현 영역의 사용자는 웹 브라우저 이용자일 수 있고, REST API를 호출하는 외부 시스템일 수 있다
    • HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환하여 응용 영역에 전달하고 응용 영역의 응답을 HTTP 응답으로 변환하여 전송한다
    • 예를 들어 웹 브라우저가 보낸 HTTP 요청을 응용 영역이 필요로 하는 형식의 객체 타입으로 변환하고,
    • 응용 영역이 리턴한 결과를 JSON 형식으로 변환하여 HTTP 응답으로 웹 브라우저에 전송한다
  • 응용 영역
    • 시스템이 사용자에게 제공해야 할 기능을 구현
    • '주문 등록', '상품 상세 조회'와 같은 기능 구현
    • 응용 영역은 기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다
    • 주문 취소 기능을 제공하는 응용 서비스를 예로 살펴보면 다음과 같이 주문 도메인 모델을 사용해서 기능을 구현한다
public class CancelOrderService {

    @Transactional
    public void cancelOrder(String orderId) {
        Order order = findOrderById(orderId);
        if (order == null) throw new Exception();
        order.cancel();
    }
    ...
}
  • 응용 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다
  • 위 코드도 주문 취소 로직을 직접 구현하지 않고 Order 객체에 취소 처리를 위임하고 있다
  • 인프라스트럭처
    • 인프라스트럭처 영역은 구현 기술에 대한 것을 다룬다
    • 이 영역은 RDBMS 연동을 처리하고, 메시지 큐에 메시지를 전송하거나 수신하는 기능을 구현하고
    • 몽고 DB나 레디스와의 데이터 연동을 처리한다
    • 이 영역은 SMTP를 이용한 메일 발송 기능을 구현하거나, HTTP 클라이언트를 이용해서 REST API를 호출하는 것도 처리한다
    • 인프라스트럭처 영역은 논리적인 개념을 표현하기보다 실제 구현을 다룬다
  • 도메인, 응용, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다
  • 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다
  • 예를 들어 응용 영역에서 DB에 보관된 데이터가 필요하면 인프라스트럭처 영역의 DB 모듈을 사용하여 데이터를 읽어오고
  • 비슷하게 외부에 메일을 발송해야 한다면 인프라스트럭처가 제공하는 SMTP 연동 모듈을 이용해서 메일을 발송한다

계층 구조 아키텍처

  • 네 영역을 구성할때는 계층 구조를 많이 사용한다
    • 표현 -> 응용 -> 도메인 -> 인프라스트럭처
  • 표현과 응용은 도메인 영역을 사용하고, 도메인 영역은 인프라 영역을 사용하므로 계층 구조를 적용하기에 적당해 보인다
  • 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고, 하위 계층은 상위 계층에 의존하지 않는다
  • 예를 들어 표현은 응용에, 응용은 도메인에 의존하지만, 인프라스트럭처 계층이 도메인에 의존하거나 응용 계층에 의존하지 않는다
  • 구현의 편리함을 위해 계층 구조를 유연하게 적용하기도 한다
  • 예를 들어 응용은 도메인에 의존하지만 인프라 계층에 의존하기도 한다

  • 이러한 계층 구조를 사용할 때 짚고 넘어가야 할 것은, 표현, 응용, 도메인 계층이
  • 상세한 구현 기술을 다루는 인프라 계층에 종속된다는 점이다
  • 종속되게 됨으로써 2가지 문제가 발생한다
  • 첫번째는 인프라 계층에 종속되게 되면 특정 계층만 테스트하기 쉽지 않다는 것이다
  • 인프라가 완벽하게 동작해야만 서비스를 테스트할 수 있다
public class CalculateDiscountService {
    private DroolsRuleEngine ruleEngine;

    public CalculateDiscountService() {
        ruleEngine = new DroolsRuleEngine();
    }

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

        MutableMoney money = new MutableMoney(0);
        List<?> facts = Arrays.asList(customer, money);
        facts.addAll(orderLines);
        ruleEngine.evaluate("discountCalculation", facts);
        return money.toImmutableMoney();
    }
    ...
}
  • 두 번째는 구현 방식을 변경하기 어렵다는 점이다
  • 위 코드를 보면 Drools가 제공하는 타입을 직접 사용하지 않으므로 Drools 자체에 의존하지 않는다고 생각할 수 있다
  • 하지만 discountCalculation 문자열은 Drools의 세션 이름을 의미한다
  • 따라서 Drools의 세션 이름을 변경하면 CaculateDiscountService의 코드도 함께 변경해야 한다
  • 또 MutableMoney는 룰 적용 결괏값을 보관하기 위해 추가한 타입인데 다른 방식을 사용헀다면 필요 없는 타입이다

 

  • 이처럼 CalculateDiscountService가 겉으로는 인프라의 기술에 직접적인 의존을 하지 않는 것처럼 보여도 실제로는 의존하고 있다
  • 이런 상황에서 Drools를 다른 구현 기술로 변경하면 코드의 많은 부분을 고쳐야 한다
  • 인프라스트럭처에 의존하면 '테스트 어려움'과 '기능 확장의 어려움'이라는 두 가지 문제가 발생한다는 것을 알게 되었다
  • 그렇다면 어떻게 해야 이 두 문제를 해소할 수 있을까?
  • 해답은 DIP에 있다

DIP

  • 가격 할인 계산을 하려면 고객 정보를 구하고, 고객 정보와 주문 정보를 이용하여 룰을 실행해야 한다
  • 고수준 모듈
    • 고객 정보를 구한다
    • 룰을 이용해서 할인 금액을 구한다
  • 저수준 모듈
    • RDBMS에서 JPA로 구한다
    • Drools로 룰을 적용한다
  • 여기서 CaclulateDiscountService는 고수준 모듈이다
  • 고수준 모듈은 의미 있는 단일 기능을 제공하는 모듈로 가격 할인 계산이라는 기능을 구현한다
  • 고수준 모듈의 기능을 구현하려면 여러 하위 기능이 필요하다
  • 저수준 모듈은 하위 기능을 실제로 구현한 것이다
  • JPA를 이용하여 고객 정보를 읽어오는 모듈과 Drools로 룰을 실행하는 모듈이 저수준 모듈이 된다
  • 고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야 한다
  • 그런데 고수준 모듈이 저수준 모듈을 사용하면 앞서 계층 구조 아키텍처에서 언급했던 두 가지 문제, 즉 구현 변경과 테스트가 어렵다는 문제가 발생한다
  • DIP는 이 문제를 해결하기 위해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다
  • 고수준 모듈을 구현하려면 저수준 모듈을 사용해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존하도록 하려면 어떻게 해야 할까?
  • 비밀은 추상화한 인터페이스에 있다
  • CalculateDiscountService 입장에서는 룰 적용을 Drools로 했는지 자바로 직접 구현했는지 중요하지 않다
  • 고객 정보와 구매 정보에 룰을 적용하여 할인 금액을 구한다 라는 것만 중요할 뿐이다
  • CaclulateDiscountService에는 Drools에 의존하는 코드가 없다. 단지 RuleDiscounter가 룰을 적용한다는 사실만 알뿐이다
  • 실제 RuleDiscounter 구현 객체는 생성자를 통해 전달받는다

  • service는 더 이상 구현 기술인 Drools에 의존하지 않는다
  • 룰을 이용한 할인 금액 계산을 추상화한 RuleDiscounter 인터페이스에 의존할 뿐이다
  • 룰을 이용한 할인 금액 계산은 고수준 모듈의 개념이므로 RuleDiscounter 인터페이스는 고수준 모듈에 속한다
  • DroolsRuleDiscounter는 고수준의 하위 기능인 RuleDiscounter를 구현한 것이므로 저수준 모듈에 속한다
  • DIP를 적용하면 위의 그림처럼 저수준 모듈이 고수준 모듈에 의존하게 된다
    • 상속은 의존의 다른 형태
  • 고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데,
  • 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 DIP, 의존 역전 원칙이라 부른다
  • DIP를 적용하면 인프라 영역에 의존할 때 발생했던 두 가지 문제인 구현 교체가 어렵다는 것과 테스트가 어려운 문제를 해소할 수 있다
  • 먼저 구현 기술 교체 문제를 보자
  • 고수준 모듈은 더 이상 저수준 모듈에 의존하지 않고 구현을 추상화한 인터페이스에 의존한다
  • 실제 사용할 저수준 구현 객체는 다음 코드처럼 의존 주입을 이용해서 전달받을 수 있다
// 사용할 저수준 객체 생성
RuleDiscounter ruleDiscounter = new DrollsRuleDiscounter();

// 생성자 방식으로 주입
CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);
  • 구현 기술을 변경하더라도 사용할 저수준 구현 객체를 생성하는 코드만 변경하면 된다
  • 스프링과 같은 의존 주입을 지원하는 프레임워크를 사용하면 설정 코드를 수정해서 쉽게 구현체를 변경할 수 있다
  • 테스트 또한 해당 인터페이스에 Mock 같은 대용 객체를 주입하면 되므로 실제 저수준 객체가 존재하지 않아도 테스트 가능하다

DIP 주의사항

  • DIP를 잘못 생각하면 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아들일 수 있다
  • DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함인데 DIP를 적용한 결과 구조만 보고
  • 저수준 모듈에서 인터페이스를 추출하는 경우가 있다

  • 위는 잘못된 구조이다
  • 도메인 영역은 구현 기술을 다루는 인프라스트럭처 영역에 의존하고 있다
    • 여전히 고수준 모듈이 저수준 모듈에 의존하는 것이다
  • 인터페이스를 고수준 모듈인 도메인 관점이 아니라 룰 엔진이라는 저수준 모듈 관점에서 도출한 것이다
  • DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해라

DIP와 아키텍처

  • 인프라 영역은 구현 기술을 다루는 저수준 모듈이고, 응용과 도메인 영역은 고수준 모듈이다
  • 인프라 계층에 DIP를 적용하면 인프라 영역이 응용과 도메인 영역에 의존(상속)하는 구조가 된다

  • 인프라에 위치한 클래스가 도메인이나 응용 영역에 정의한 인터페이스를 상속받아 구현하는 구조가 되므로
  • 도메인과 응용 영역에 대한 영향을 주지 않거나 최소화하며 구현 기술을 변경하는 것이 가능하다

  • 인프라 영역의 EmailNotifier 클래스는 응용 영역의 Notifier 인터페이스를 상속받고 있다
  • 주문 시 통지 방식에 SMS를 추가해야 한다는 요구사항이 들어왔을 때 응용 영역의 OrderService는 변경할 필요가 없다
  • 두 통지 방식을 함께 제공하는 Notifier 구현 클래스를 인프라 영역에 추가하면 된다
  • 비슷하게 mybatis 대신 jpa를 구현 기술로 사용하고 싶다면 JPA를 이용한 OrderRepository 구현 클래스를 인프라 영역에 추가하면 된다

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

  • 앞에서 도메인 영역은 도메인의 핵심 모델을 구현한다고 설명했다
  • 도메인 영역의 모델은 도메인의 주요 개념을 표현하며 핵심 로직을 구현한다
  • 1장의 엔티티와 밸류 타입은 도메인 영역의 주요 구성요소이다
  • 이 두 요소와 함께 도메인 영역을 구성하는 요소는 다음과 같다

 

  • 엔티티
    • 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다
    • 주문, 회원, 상품과 같이 도메인의 고유한 개념을 표현한다
    • 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다
  • 밸류
    • 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현할 때 사용된다
    • 배송지 주소를 표현하기 위한 '주소'나 구매 금액을 위한 '금액'과 같은 타입이 밸류 타입이다
    • 엔티티의 속성으로 사용할 뿐만 아니라 다른 밸류 타입의 속성으로도 사용할 수 있다
  • 애그리거트
    • 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것
    • Order 엔티티, OrderLine 밸류, Orderer 밸류 객체를 '주문' 애그리거트로 묶을 수 있다
  • 리포지터리
    • 도메인 모델의 영속성을 처리한다
    • 예를 들어 DBMS 테이블에서 엔티티 객체를 로딩하거나 저장하는 기능을 제공한다
  • 도메인 서비스
    • 특정 엔티티에 속하지 않은 도메인 로직을 제공한다
    • '할인 금액 계산'은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하는데,
    • 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현한다

엔티티와 밸류

  • 도메인 모델의 엔티티와 DB 모델의 엔티티는 같지 않다
  • 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다
    • 예를 들어 주문 엔티티는 주문과 관련된 데이터 뿐만 아니라 배송지 주소 변경을 위한 기능 등등
  • 도메인 모델의 엔티티는 단순히 데이터를 담는 데이터 구조를 넘어 기능을 함께 제공하는 객체이다
  • 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화하여 데이터가 임의로 변경되는 것을 막는다

 

  • 또 다른 차이점은 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다는 것이다
class Order {
	...
    Private Orderer orderer;
    ...
}

class Orderer {
	private String name;
    private String email;
    ...
}
  • RDBMS와 같은 관계형 데이터베이스는 밸류 타입을 제대로 표현하기 힘들다
  • 데이터를 개별로 저장하거나 별도 테이블로 분리해야 한다
    • 개별로 저장하면 데이터가 같은 개념에 속한다는 것을 드러낼 수 없다
    • 별도 테이블로 저장하면 엔티티에 가까워지며 벨류 타입의 의미가 드러나지 않는다
  • 반면 도메인 모델의 Orderer는 주문자라는 개념을 잘 반영하므로 도메인을 보다 잘 이해할 수 있도록 돕는다
  • 1장에서 설명한 것처럼 밸류는 불변으로 구현할 것을 권장하며,
  • 이는 엔티티의 밸류 타입 데이터를 변경할 때는 객체 자체를 완전히 교체한다는 것을 의미한다

 

애그리거트

  • 도메인 모델이 복잡해지면 전체 구조가 아닌 한 개 엔티티와 벨류에만 집중하는 상황이 발생하고
  • 이때 상위 수준에서 모델을 관리하지 않고 개별 요소에만 초점을 맞추면, 큰 수준에서 모델을 이해하지 못해 큰 틀에서 모델을 관리할 수 없는 상황에 빠진다

 

  • 도메인 모델을 상위 수준에서 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해하는데 도움이 되고,
  • 도메인 모델에서 전체 구조를 이해하는데 도움이 되는 것이 애그리거트 이다

 

  • 애그리거트
    • 관련 객체를 하나로 묶은 군집
    • 주문 -> 주문, 배송지 정보, 주문자, 주문 목록, 총 결제 금액의 하위 모델로 구성된다
    • 이 하위 개념을 표현한 모델을 하나로 묶어서 '주문' 이라는 상위 개념으로 표현
  • 애그리거트를 사용하면 개별 객체가 아닌 관련 객체를 묶어서 군집 단위로 모델을 바라볼 수 있게 되고
  • 개별 객체가 아닌 애그리거트 간의 관계로 도메인 모델을 이해하고 구현할 수 있다
  • 애그리거트는 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다
    • 루트엔티티는 애그리거트에 속한 엔티티와 벨류 객체를 이용하여 애그리거트가 구현해야 할 기능을 제공
    • 애그리거트를 사용하는 코드는 루트를 통해서만 다른 엔티티나 벨류 객체에 접근이 가능하다
    • 이를 통해 내부 구현을 숨겨 애그리거트 단위로 구현을 캡슐화할 수 있도록 돕는다

 

리포지터리

  • 도메인 객체를 지속적으로 사용하려면 RDBMS, NoSQL 같은 물리적인 저장소가 필요하다
  • 이를 위한 도메인 모델이 Repository
  • 엔티티나 벨류가 요구사항에서 도출되는 도메인 모델이라면 리포지터리는 구현을 위한 모델
  • 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 제공한다

 

인프라스트럭처 개요

  • 꼭 @Transactional 같은 인프라의 기술에 직접 의존하는 것이 나쁜 것은 아니다
  • 구현의 편리함 또한 변경의 유연함이나 테스트가 쉬움 만큼 중요하기 때문에 DIP 의 장점을 해치지 않는 범위에서 응용 영역과 도메인 영역에서 구현 기술에 대한 의존을 가져가는 것은 나쁘지 않다고 한다
  • trade off를 잘 생각해서 하자

 

모듈 구성

  • 아키텍처의 각 영역은 별도 패키지에 위치한다
  • 도메인이 크면 하위 도메인으로 나누고 각 하위 도메인마다 별도 패키지를 구성한다
  • 도메인 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성한다
  • 예를 들어 카탈로그 하위 도메인이 상품 애그리거트와 카테고리 애그리거트로 구성될 경우 도메인 패키지를 두 개의 하위 패키지로 구성할 수 있다
  • 모듈 구조를 얼마나 세분화해야 하는지에 대해 정해진 규칙은 없다지만,
  • 책의 저자는 한 패키지에 10~15개 미만으로 타입 개수를 유지하려 노력한다고 한다

'programming > DDD' 카테고리의 다른 글

[DDD] 4장 리포지터리와 모델 구현  (0) 2022.12.21
[DDD] 3장 애그리거트  (0) 2022.12.10
DDD-Light  (0) 2022.10.21