Heeveloper

[DDD Start]2장. 아키텍처 개요

2020-07-02
Heeveloper
DDD

1. 네 개의 영역

아키텍처의 전형적인 영역으로 ‘표현’, ‘응용’, ‘도메인’, ‘인프라스트럭처’를 들 수 있다.

  • 표현 영역(Presentation Layer) : 사용자의 요청을 받아 응용 영역에 전달하고 처리 결과를 다시 사용자에게 보여주는 역할 (스프링 MVC 프레임워크가 표현 영역을 위한 기술에 해당)
  • 응용 영역(Application Layer) : 시스템의 기능을 도메인 영역의 도메인 모델을 사용하여 구현한다. 로직을 직접 수행하기 보다 도메인 모델에 로직 수행을 위임한다.
  • 도메인 영역(Domain Layer) : 도메인 모델 구현
  • 인프라스트럭처 영역(Infrastructure Layer) : 논리적인 개념을 표현하기보다 실제 구현 기술을 다룬다. (RDBMS 연동, 메시징 큐의 메세지 전송 및 수신 기능 구현, 몽공DB나 HBase를 사용해 DB 연동 등)

2. 계층 구조 아키텍처

표현 -> 응용 -> 도메인 -> 인프라스트럭처

일반적으로 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않지만 구현의 편리함을 위해 계층 구조를 유연하게 적용 한다.
예를 들어, 외부 시스템과의 연동을 위해 응용 계층에서 바로 인프라스트럭처 계층에 의존하기도 한다.

이처럼 표현, 응용, 도메인 계층이 구현기술을 다루는 인프라스트럭처 계층에 종속하게 되면,
첫 번째 문제는 테스트하기 어렵다는 것이고 두 번째 문제는 구현 방식을 변경하기 어렵다는 것이다.

// 응용 계층의 응용 서비스
public class CalculateDiscountService {
  private DroolsRuleEngine ruleEngine; // 외부 룰 엔진
  
  public CalculateDiscountService() {
    ruleEngine = new DroolsRuleEngine();
  }
  
  public Money calculateDiscount(List<OrderLine> orderLines, String customerId) {
    Customer customer = findCustomer(customerId);
    
    MutableMoney money = new MutableMoney(0); // Drools에 특화된 코드 : 연산결과를 받기 위해 추가한 타입
    /** Drools에 특화된 코드 : 룰에 필요한 데이터(지식) **/
    List<?> facts = Arrays.asList(customer, money);
    facts.addAll(orderLines);
    
    ruleEngine.evalute("discountCalculation", facts); // "discountCalculation" : Drools에 특화된 코드(Drools의 세션 이름)
    return money.toImmutableMoney();
    ...
  }
}
  • Drools 변경시 위 응용서비스 코드도 함께 수정해야 한다.
  • 인프라스터럭처 계층의 구현이 완료되기 전까지 테스트를 진행할 수 없다.

3. DIP (Dependency Inversion Principle)

위에서 언급한 2가지 문제(구현 변경과 테스트 어려움)를 해결하기 위해 DIP는 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다. CalculateDiscountService 입장에서 봤을 때 룰 적용을 Drools로 했는지 자바로 직접 했는지는 중요하지 않다. 단지 ‘고객 정보와 구매 정보에 룰을 적용해서 할인 금액을 구한다’는 것이 중요할 뿐이다. 이를 추상화한 인터페이스를 통해 DIP을 적용한다.

public interface RuleDiscounter {
  public Money applyRules(Customer customer, List<OrderLine> orderLines);
}
public class CalculateDiscountService {
  private RuleDiscounter ruleDiscounter;
  
  public CalculateDiscountService(Rulediscounter ruleDiscounter) {
    private RuleDiscounter ruleDiscounter;
    
    public Money calculateDiscount(List<OrderLine> orderLines, String customerId) {
      Customer customer = findCustomer(customerId);
      return ruleDiscounter.applyRules(customer, orderLines);
    }
  }
}
  • 더이상 CalculateDiscountServiceDrools에 의존하는 코드 포함하지 않는다.
public class DroolsRuleDiscounter implements RuleDiscounter {
  ...
    
  @Override
  public Money applyRule(Customer customer, List<OrderLine> orderLines) {
    ...
  }
}
  • RuleDiscounter를 상속받아 구현

위의 구조를 통해 구현 기술을 변경하더라도 CalculateDiscountService를 수정할 필요가 없다.

// 사용할 저수준 구현 객체 변경
RuleDiscounter ruleDiscounter = new SimpleRuleDiscounter();

// 사용할 저수준 모듈 변경해도 고수준 모듈을 수정할 필요가 없다.
CalculateDiscountService discountService = new CalculateDiscountService(ruleDiscounter);
  • 의존 주입을 지원하는 Spring 같은 프레임워크를 사용하면 설정 코드를 수정해 쉽게 구현체를 변경할 수 있다.

또한 테스트도 간편해졌다. 기존에는 CalculateDiscountService 가 저수준 모듈에 직접 의존했다면 저수준 모듈(CustomerRepository, RuleDiscounter)이 만들어지기 전까지 테스트를 할 수 없었지만 이제 CustomerRepositoryRuleDiscounter는 인터페이스이므로 대용 객체(mock)를 사용해 테스트를 진행할 수 있다.

DIP 주의 사항

DIP는 단순히 인터페이스와 구현 클래스를 분리하는 것이 아니다. 인터페이스를 저수준 모듈에 두지 않도록 한다.

DIP와 아키텍처

아키텍처 수준에서 DIP를 적용하면 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존하는 구조가 된다.

  • Email알림이 아닌 SMS알림으로 바꿀 경우 EmailNotifier -> SMSNotifier로, MyBatis가 아니라 JPA로 바꿀 경우 MyBatisOrderRepository -> JpaOrderRepository로만 변경해주면 된다.

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

  • 엔티티(Entity) : 고유의 식별자 갖음. 주문(Order), 회원(Member), 상품(Product)과 같이 도메인 고유한 개념을 표현. 도메인 모델의 데이터와 관련 기능을 제공
  • 밸류(Value) : 고유 식별자 없고 개념적으로 하나의 도메인 객체의 속성을 표현할 때 사용(Address, Money, ..)
  • 애그리거트(Aggregate) : 관련 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것. Order 엔티티, OrderLine 밸류, Orderer 밸류 객체를 ‘주문’ 애그리거트로 묶을 수 있다.
  • 레포지토리(Repository) : 도메인 모델의 영속성 처리. DBMS -> 엔티티 객체로 로딩하거나 저장하는 기능 제공
  • 도메인서비스(DomainService) : 특정 엔티티에 속하지 않는 도메인 로직을 제공. ‘할인 금액 계산’은 상품, 쿠폰, 회원 등급 등 다양한 조건을 사용해 구현하게 되는데, 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 할 경우 도메인 서비스에서 로직을 구현한다.

엔티티와 밸류

도메인 모델의 엔티티와 DB테이블 엔티티의 차이

  • 도메인 모델의 Entity는 데이터 뿐만 아니라 관련된 기능까지 제공한다.
  • 도메인 모델의 경우 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해 표현할 수 있다. RDBMS는 풀어서 저장하거나 별도 테이블로 저장해야 한다

애그리거트

도메인이 커질수록 엔티티와 밸류가 점점 많아지면서 복잡해지는데, 관련 객체들을 묶어 객체 군집 단위인 하나의 애그리거트로 볼 수 있다.
예를 들어 주문이라는 도메인 개념은 주문, 배송지정보, 주문자, 주문 목록, 총 결제금액의 하위 모델로 구성되는데, 이 때 이 하위 개념을 표현한 모델을 하나로 묶어 ‘주문’이라는 상위 개념으로 표현할 수 있다.

루트 엔티티

  • 애그리거트는 군집에 속한 객체들을 관리하는 루트 엔티티를 갖는다
  • 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류를 이용해서 애그리거트가 구현해야 할 기능을 제공한다.

  • 위의 Order가 Root Entity

레포지터리

엔티티나 밸류가 요구사항에서 도출되는 도메인 모델이라면 리포지터리는 구현을 위한 도메인 모델 이다.

레포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.

public interface OrderRepository {
  public Order findByNumber(OrderNumber orderNumber);
  public void save(Order order);
}

도메인 모델을 사용해야 하는 코드는 리포지터리를 이용하여 도메인 객체를 구한 뒤에 도메인 객체의 기능을 실행하게 된다.

도메인 모델 관점에서 OrderRepository는 도메인 객체를 영속화하는데 필요한 기능을 추상화한 것으로 고수준 모듈에 속한다. 기반 기술을 이용해 OrderRepository를 구현한 클래스(JpaOrderRepository는 인프라스트럭처 영역에 속한 저수준 모듈이다.

응용 서비스(ApplicationService)는 의존 주입과 같은 방식을 통해 실제 레포지터리 구현 객체에 접근한다.

@Configuration
public class OrderServiceConfig { // 응용 서비스 영역 설정
  @Autowired
  private OrderRepository orderRepository;
  
  @Bean
  public CancelOrderService cancelOrderService() {
    return new CancelOrderService(orderRepository)
  }
}
@Configuration
public class RepositoryConfig { // 인프라스트럭처 영역 설정
  @Bean
  public JpaOrderRepository orderRepository() {
    return new JpaOrderRepository();
  }
  
  ...
}

응용 서비스와 리포지터리는 밀접한 연관이 있다

  • 응용 서비스는 필요한 도메인 객체를 구하거나 저장할 때 리포지터리를 사용한다.
  • 응용 서비스는 트랜잭션을 관리하는데, 트랜잭션 처리는 리포지터리 구현 기술에 영향을 받는다.

5. 요청 처리 흐름

예매하기나 예매 취소와 같은 기능을 제공하는 응용 서비스는 도메인 상태를 변경하므로 변경 상태가 물리저장소에 올바르게 반영되도록 트랜잭션을 관리해야 한다. 스프링 프레임워크를 사용하면 @Transactional 을 통해 트랜잭션을 처리할 수 있다.

기능 구현에 필요한 도메인 객체를 Repository에서 가져와 실행하거나 신규 도메인 객체를 생성하여 Repository에 저장한다.

6. 인프라스트럭처 개요

무조건 인프라스트럭처에 대한 의존을 없애는 것이 좋은 것이 아니다.

  • 스프링의 @Transactional, 영속성을 위한 JPA의 @EntityTable 과 같은 애노테이션을 사용하는 것이 XML 매핑 설정을 이용하는 것보다 편리하다.**
  • DIP 장점을 해치지 않는 범위에서 응용 영역과 도메인 영역에서 구현 기술에 대한 의존을 가져가는 것이 현명하다.
  • 의존을 완전히 갖지 않도록 시도하는 것은 구현을 더 복잡하고 어렵게 만들 수 있다.

7. 모듈 구성

패키지 구성 규칙에 한 가지 정답만 존재하는 것은 아니므로 상황에 맞게 영역별로 모듈이 위치할 패키지를 구성할 수 있다.

  • 아키텍처의 각 영역은 별도 패키지에 위치한다. (ui, application, domain, infrastructure)
  • 도메인이 크면 하위 도메인으로 나누고 각 하위 도메인 마다 별도 패키지를 구성한다.
  • domain 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성한다.
  • 도메인 모델과 도메인 서비스를 별도 패키지에 위치시킬 수 있다.
  • 응용 서비스도 도메인 별로 패키지를 구분할 수 있다.
  • 한 패키지에 가능하면 10개 미만으로 타입 개수를 유지하려고 노력한다. 넘어가면 모듈 분리 시도한다.



Similar Posts

Comments

-->