[Domain Driven Design] 4. 계층 구조 아키텍처 (표현, 응용, 도메인, 인프라스트럭처)

반응형
728x90
반응형

네 개의 영역

'표현', '응용', '도메인', '인프라스트럭쳐' 는 아키텍처를 설계할 때 출현하는 전형적인 네가지 영역이다. 

표현 영역(또는 UI 영역)은 사용자의 요청을 받아 응용 영역에 전달하고 응용 영역의 처리 결과를 다시 보여주는 역할을 한다. 웹 애플리케이션을 개발할때 많이 사용하는 스프링 MVC 프레임워크가 표현 영역을 위한 기술에 해당한다. 웹 애플리케이션에서 표현 영역의 사용자는 웹 브라우저를 사용하는 사람일 수도 있고, REST API를 호출하는 외부 시스템일 수도 있다.

https://gnidoc.tistory.com/entry/%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%A3%BC%EB%8F%84-%EC%84%A4%EA%B3%84-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98

 

웹 애플리케이션의 표현 영역은 HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고 응용 영역의 응답을 HTTP 응답으로 변환하여 전송한다. 

 

표현 영역을 통해 사용자의 요청을 전달받는 응용 영역은 시스템이 사용자에게 제공해야 할 기능을 구현하는데 '주문 등록', '주문 취소', '상품 상세 조회'와 같은 기능 구현을 예로 들 수 있다. 응용 영역은 기능을 구현하기 위해 도메인 모델을 사용한다. 주문 취소 기능을 제공하는 응용 서비스를 예로 살펴보면 다음과 같이 주문 도메인 모델을 사용해서 기능을 구현한다.

 

CancelOrderService.java
package com.myshop.order.command.application;

import com.myshop.order.NoOrderException;
import com.myshop.order.command.domain.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CancelOrderService {
    private OrderRepository orderRepository;
    private CancelPolicy cancelPolicy;

    public CancelOrderService(OrderRepository orderRepository,
                              CancelPolicy cancelPolicy) {
        this.orderRepository = orderRepository;
        this.cancelPolicy = cancelPolicy;
    }

    @Transactional
    public void cancel(OrderNo orderNo, Canceller canceller) {
        Order order = orderRepository.findById(orderNo)
                .orElseThrow(() -> new NoOrderException());
        if (!cancelPolicy.hasCancellationPermission(order, canceller)) {
            throw new NoCancellablePermission();
        }
        order.cancel();
    }

}

1) 응용 서비스는 로직을 직접 수행하기 보다는 도메인 모델에 로직 수행을 위임한다.

order.cancel();

위 코드도 Order 객체에 취소 처리를 위임하고 있다.

 

Order.java
public void cancel() {
    verifyNotYetShipped();
    this.state = OrderState.CANCELED;
    Events.raise(new OrderCanceledEvent(number.getNumber()));
}

https://gnidoc.tistory.com/entry/%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%A3%BC%EB%8F%84-%EC%84%A4%EA%B3%84-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98

 

도메인 영역은 도메인 모델을 구현한다. Order, OrderLine, ShippingInfo와 같은 도메인 모델이 이 영역에 위치한다. 도메인 모델은 도메인의 핵심 로직을 구현한다. 예를 들어 주문 도메인은 '배송지 변경', '결제 완료', '주문 총액 계산'과 같은 핵심 로직을 도메인 모델에서 구현한다.

 

 

인프라스트럭처 영역

구현 기술에 대한 것을 다루는 영역이다. RDBMS 연동을 처리하고, 메시징 큐에 메시지를 전송하거나 수신하는 기능을 구현하고, 몽고 DB(MongoDB)나 레디스(Redis)와의 데이터 연동을 처리한다. 이 영역은 SMTP를 이용한 메일 발송 기능을 구현하거나 HTTP 클라이언트를 이용해서 REST API를 호출하는 것도 처리한다. 

 

논리적인 개념을 표현하기보다는 실제 구현을 다룬다.

https://gnidoc.tistory.com/entry/%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%A3%BC%EB%8F%84-%EC%84%A4%EA%B3%84-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98

 

도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않고, 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다. 예를들어 응용 영역에서 DB에 보관된 데이터가 필요하면 인프라스트럭처 영역의 DB 모듈을 사용하여 데이터를 읽어온다.

 

 

계층 구조 아키텍처

4개의 영역을 구성할때 많이 사용하는 아키텕처가 아래와 같은 계층 구조다.

https://incheol-jung.gitbook.io/docs/study/ddd-start/2

표현 영역와 응용 영역은 도메인 영역을 사용하고, 도메인 영역은 인프라스트럭처 영역을 사용하므로 계층 구조를 적용하기에 적당해보인다. 도메인의 복잡도에 따라 응용과 도메인을 분리하기도 하고 한 계층으로 합치기도 하지만 전체적인 아키텍처는 위 그림을 따른다.

 

계층 구조는 상위 계층 -> 하위 계층의 의존만 존재하고 하위 계층 -> 상위 계층에 의존하지 않는다. 예를 들어 인프라스트럭처 계층이 상위 계층인 도메인 계층에 의존하지 않는다.

 

편리함을 위해 계층 구조를 유연하게 적용하기도 한다. 예를 들어 응용 계층은 바로 아래 계층인 도메인 계층에 의존하지만, 외부 시스템과의 연동을 위해 더 아래 계층인 인프라스트럭처 계층에 의존하기도 한다. 

https://incheol-jung.gitbook.io/docs/study/ddd-start/2

 

 

인프라스트럭처 계층에 대한 의존성의 문제점

표현 계층, 응용 계층, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속된다는 점이다.

 

도메인의 가격 계산 규칙을 예로 들어보자. 

다음은 할인 금액을 계산하기 위해 Drools라는 룰 엔진을 사용해서 계산 로직을 수행하는 인프라스트럭처 영역의 코드를 만들어 본 것이다. Drools의 evalutate() 메서드에 값을 주면 별도 파일로 작성한 규칙을 이용해서 연산을 수행하는 코드 정도로만 생각하고 넘어가자.

 

DroolsRuleEngine.java
public class DroolsRuleEngine {
	private KieContainer kContainer;

	public void evalute(String sessionName, List<?> facts) {
		...
	}
}

 

응용 영역은 가격 계산을 위해 인프라스트럭처 영역의 DroolsRuleEngine을 사용한다.

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

	public Money calculateDiscount(OrderLine orderLines, String customerId) {
		Customer customer = findCusotmer(customerId);
        
		MutableMoney money = new MutableMoney(0);
		List<?> facts = Arrays.asList(customer, money);
		facts.addAll(orderLines);
		ruleEngine.evalute("discountCalculation", facts);
		return money.toImmutableMoney();
	}
    ...
}

CalculateDiscountService가 동작은 하겠지만 아래의 두가지 문제가 있다.

 

1) CalculateDiscountService만 테스트하기 어렵다. RuleEngine이 완벽하게 동작해야만 CalculateDiscountService를 테스트할 수 있다. 

2) 구현 방식을 변경하기가 어렵다.

  • Drools에 특화된 코드 : 연산 결과를 받기 위해 추가한 타입
MutableMoney money = new MutableMoney(0);
  • Drools에 특화된 코드 : 룰에 필요한 데이터 (자식)
List<?> facts = Arrays.asList(customer, money);
facts.addAll(orderLines);
  • Drools에 특화된 코드 : Drools의 세션 이름
ruleEngine.evalute("discountCalculation", facts);

 

코드만 보면 Drools가 제공하는 타입을 직접 사용하지 않으므로 CalculateDiscountService가 Drools 자체에 의존하지 않는다고 생각할 수 있지만, 'discountCalculation' 문자열은 Drools의 세션 이름을 의미한다. Drools의 세션 이름이 변경되면 CalculateDiscountService도 변경되어야한다. MutableMoney는 룰 적용 결괏값을 보관하기 위해 추가한 타입인데 다른 방식을 사용했다면 필요 없는 타입이다.

 

이처럼 CalculateDiscountService가 겉으로는 인프라스트럭처의 기술에 직접적으로 의존을 하지 않는 것처럼 보여도 실제로는 Drools라는 인프라스트럭처 영역의 기술에 완전하게 의존하고 있다. 이런 상황에서 Drools가 아닌 다른 구현 기술을 사용하려면 코드의 많은 부분을 고쳐야한다.

 

이를 해결하는 방법은 DIP에 있다.

 

DIP

포스팅 예정 

 

 

반응형

Designed by JB FACTORY