관리 메뉴

csct3434

[도메인 주도 개발 시작하기] 02. 아키텍처 개요 본문

개발 서적/도메인 주도 개발 시작하기

[도메인 주도 개발 시작하기] 02. 아키텍처 개요

csct3434 2024. 6. 12. 09:12

네 개의 영역

표현 영역 (Presentation)

  • 표현 영역은 사용자의 요청을 받아 응용 영역에 전달하고 응용 영역의 처리 결고를 다시 사용자에게 보여주는 역할을 한다.
  • 웹 애플리케이션의 표현 영역은 HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고, 응용 영역의 응답을 HTTP 응답으로 변환하여 전송한다.
  • 예를 들어 표현 영역은 웹 브라우저가 HTTP 요청 파라미터로 전송한 데이터를 응용 서비스가 요구하는 형식의 객체 타입으로 변환해서 전달하고, 응용 서비스가 리턴한 결과를 JSON 형식으로 변환해서 HTTP 응답으로 웹 브라우저에 전송한다.

응용 영역 (Application)

public class CancelOrderService {

    @Transactional
    public void cancelOrder(String orderId) {
        Order order = findOrderById(orderId);
        if (order == null) throw new OrderNotFoundException(orderId);
        order.cancel();
    }
}
  • 표현 영역을 통해 요청을 전달받는 응용 영역은 도메인 모델을 통해 시스템이 제공해야 할 기능을 구현한다.
  • 응용 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다. (실제 도메인 로직은 도메인 모델에서 구현한다)

도메인 영역

public class Order {

    private OrderState state;

    public void cancel() {
        verifyNotYetShipped();
        this.state = OrderState.CANCELED;
    }

    private void verifyNotYetShipped() {
        if (state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING) {
            throw new IllegalArgumentException("Already Shipped");
        }
    }

    public enum OrderState {
        PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED, CANCELED;
    }
}
  • 도메인 영역은 도메인 모델을 구현한다.
  • 도메인 모델은 도메인의 핵심 로직을 구현한다.

인프라스트럭쳐

  • 인프라스트럭쳐 영역은 구현 기술에 대한 것을 다룬다.
  • 인프라스트럭처 영역은 논리적인 개념을 표현하기보다는 실제 구현을 다룬다.
  • 도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.

계층 구조 아키텍처

  • 이러한 계층 구조는 직관적으로 이해하기는 쉽지만, 도메인 계층이 구현 기술을 다루는 인프라스트럭처 계층에 종속된다.
  • 도메인 계층이 구현 기술에 의존할 경우 테스트를 작성하기가 어려워지고 구현 기술의 변경에 많은 영향을 받아 기능 확장이 어려워진다는 단점이 존재한다.

DIP

  • 고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야 하지만, 이렇게 되면 구현 변경과 테스트가 어렵다는 문제가 발생한다.
public class CalculateDiscountService {
    
    private DroolsRuleEngine ruleEngine;
    
    public CalculateDiscountService() {
        ruleEngine = new DroolsRuleEngine();
    }

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

        MutableMoney money = new MutableMoney(0);
        List<?> facts = Arrays.asList(customer, money);
        facts.addAll(orderLines);
        ruleEngine.evaluate("discountCalculation", facts);
        return money.toImmutableMoney();
    }
}

  • DIP는 추상화한 인터페이스를 통해 저수준 모듈이 고수준 모듈에 의존하도록 하여 이러한 문제를 해결한다.
  • 고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 DIP(Dependency Inversion Pricinple, 의존성 역전 원칙)라고 부른다.
  • DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈의 관점에서 도출해야 한다. 따라서 해당 인터페이스는 저수준 모듈(인프라 계층)이 아닌 고수준 모듈(도메인 계층)에 위치해야 한다.
public interface RuleDiscounter {
    Money applyRules(Customer customer, List<OrderLine> orderLines);
}

public class DroolsRuleDiscounter implements RuleDiscounter {
    @Override
    public Money applyRules(Customer customer, List<OrderLine> orderLines) { ... }
}

public class CalculateDiscountService {

    private RuleDiscounter ruleDiscounter;

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

    public Money calculateDiscount(OrderLine orderLines, String customerId) {
        Customer customer = findCustomer(customerId);
        return ruleDiscounter.applyRules(customer, orderLines);
    }
}
  • CalculateDiscountService는 더 이상 구현 기술인 Drools에 의존하지 않는다.
  • '룰을 이용한 할인 금액 계산'은 고수준 모듈의 개념이므로 RuleDiscounter 인터페이스는 고수준 모듈에 속한다.
  • DroolsRuleDiscounter는 고수준의 하위 기능인 RuleDiscounter를 구현한 것이므로 저수준 모듈에 속한다.

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

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

엔티티와 밸류

  • 도메인 모델의 엔티티와 DB 테이블의 엔티티는 별개의 개념이다.
  • 두 모델의 가장 큰 차이점은 도메인 모델의 엔티티는 단순히 데이터를 담고 있는 데이터 구조라기보다는 데이터와 함께 기능을 제공한다는 점이다.
  • 또 다른 차이점은 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다는 것이다. 하지만 RDBMS와 같은 관계형 데이터베이스는 밸류 타입을 제대로 표현하기가 힘들다.

애그리거트

  • 도메인이 커질수록 개발할 도메인 모델도 커지면서 많은 엔티티와 밸류가 출현한다.
  • 지도를 볼 때 큰 수준에서 어디에 위치하고 있는지 파악하기 위해 대축적 지도 뿐만 아니라 소축적 지도가 필요한 것 처럼, 도메인 모델도 전체 모델의 관계와 개별 모델을 이해하기 위해 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있는 애그리거트가 필요하다.

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

리포지터리

  • 리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.
  • 도메인 모델 관점에서 OrderRepository는 도메인 객체를 영속화하는데 필요한 기능을 추상화한 것으로 고수준 모듈에 속한다.
  • 기반 기술을 이용해서 OrderRepository를 구현한 클래스는 저수준 모듈로 인프라스트럭처 영역에 속한다.

요청 처리 흐름

  • 표현 영역은 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 응용 서비스가 요구하는 형식으로 데이터를 변환하여 응용 서비스에 기능 실행을 위임한다.
  • 응용 서비스는 도메인 모델을 이용하여 기능을 구현하고, 도메인의 상태가 변경된 경우 물리 저장소에 반영되도록 트랜잭션을 관리한다.

모듈 구성

  • 패키지 구성 규칙에 정답이 존재하는 것은 아니지만 그림과 같이 영역별로 모듈이 위치할 패키지를 구성할 수 있다.
  • 도메인이 복잡하면 도메인 모델과 도메인 서비스를 다음과 같이 별도의 패키지에 위치시킬 수도 있다.
    • com.myshop.order.domain.order: 애그리거트 위치
    • com.myshop.order.domain.service: 도메인 서비스 위치
  • 응용 서비스도 다음과 같이 하위 도메인 별로 패키지를 구분할 수 있다.
    • com.myshop.catalog.application.product
    • com.myshop.catalog.application.category
  • 모듈 구조를 얼마나 세분화해야 하는지에 대해 정해진 규칙은 없다. 한 패키지에 너무 많은 타입이 몰려서 코드를 찾을 때 불편한 정도만 아니면 된다. 저자는 개인적으로 한 패키지에 가능하면 10~15개 미만으로 타입 개수를 유지하려고 노력한다고 한다.

느낀점

  • 지금까지 응용 서비스에서 도메인 로직을 직접 다뤘는데, 도메인 모델에서 도메인 로직을 구현하는 것이 보다 객체 지향적인거 같다.
  • 책에서도 언급되어 있듯이 인프라에 대한 의존성을 완전히 제거하는 것은 구현의 복잡성을 크게 증가시키기에 현실적이지 않은거 같다.
  • 개인적으로 다음과 같은 방식이 좋을거 같다.
    • 전체 영역에서 스프링의 DI, PSA, IoC를 적극 활용한다.
    • 도메인 모델에서 도메인 로직을 구현하고, 응용 서비스는 도메인 모델을 통해 기능을 구현한다.
    • 도메인 모델을 JPA 엔티티로 선언하여 스프링의 선언적 트랜잭션과 JPA의 변경 감지를 통해 트랜잭션을 관리한다.
    • 표현 계층은 응용 계층에 인터페이스를 통해 접근한다.
    • 인프라스트럭처 계층에서 도메인과 응용 계층에 의존하도록 DIP를 적용한다.