본문 바로가기

프로그래밍_도서/도메인_주도_설계_시작하기

chapter2. 아키텍처 개요

2.0. 개요

아키텍처의 전형적인 4가지 영역(표현, 응용, 도메인, 인프라스트럭처) 각각의 역할을 설명하고, 계층 기반 아키텍처에서 응용영역과 도메인 영역에서 상세 구현 기술을 다루는 인프라스트럭처 계층을 직접 의존할 경우 발생할 수 있는 문제에 대해서 설명한다. 고수준 모듈과 저수준 모듈을 정의하고, 저수준 모듈이 고수준 모듈을 의존하게 하는 DIP 를 적용함으로써 위의 문제점을 해결할 수 있는 방법을 제시한다.

2.1. 네 개의 영역

아키텍처 설계의 전형적인 4가지 영역으로는 '표현', '응용', '도메인', '인프라스트럭처'가 있다.

  • 표현 영역
    • 사용자의 요청을 응용 영역에 잔달하고 결과를 사용자에게 전달
  • 응용 영역
    • 시스템이 사용자게에 제공하는 기능 구현
      ex) '주문 등록', '주문 취소', '상품 상세 조회'
    • 도메인 모델이 비즈니스 로직을 수행하도록 기능을 위임한다.
public void cancelOrder(String id){
    Order order = findById(id);
    if(order == null){
        throw new OrdernotFoundException(id);
    }

    // 도메인 모델에 로직 수행을 위임
    order.cancel();
}

 

응용 영역은 사용자에게 제공할 기능을 도메인 모델을 이용해서 구현한다.

 

  • 도메인 영역
    • 도메인 핵심 로직을 구현하는 도메인 모델을 구현한다.
      ex) '배송지 변경', '결제 완료', '주문 총액 계산' 등
  • 인프라스트럭처 영역
    • RDBMS 연동, 메시징 큐 기능 구현, 몽고DB나 레디스와의 데이터 연동과 같이 실제 구현 기술에 대한 것

도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않고, 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.

 

2.2. 계층 구조 아키텍처

 

도메인의 복잡도에 따라 응용과 도메인을 분리하기도 하고 한 계층으로 합치기도 하지만, 계층형 구조는 아래와 같는 구조를 따른다. 

 

 

 

계층 구조는 상위 계층에서 하위 계층으로 의존하고 하위 계층은 상위 계층을 의존하지 않는다. 구현의 편의를 위해 계층 구조를 유연하게 설계한다면 상위 계층이 바로 아래 계층만 의존하지 않도록 설계할 수도 있다.  

 

 

이와 같은 계층형 구조를 가진다면 응용 영역이나 도메인 영역에서는 DB나 외부 시스템 연동 등 상세 구현 기술을 다루는 인프라스트럭처 계층에 종속된다. 이와 같은 구조에서는 두가지 문제점이 발생한다. 

 

  1. 테스트가 어려운 구조
    • 응용 역역이나 도메인 영역 테스트를 위해선 인프라스트럭처의 기능이 완벽하게 동작해야 한다.
  2. 구현 방식 변경의 어려움
    • 구현 기술의 변경이 생기면 이를 의존하고 있는 응용 영역이나 도메인 영역의 코드에도 영향을 미친다.

 

인프라스트럭처에 직접 의존하게 된다면, '테스트 어려움'과 '기능 확장의 어려움'의 문제가 발생하는데, 이는 DIP를 통해서 문제를 해결할 수 있다. 

 

2.3. DIP

고수준 모듈은 애플리케이션의 핵심 정책이나 비즈니스 로직을 담당하는 모듈이다. 이 모듈이 동작하기 위해서는 여러 하위 기능이 필요하며, 이를 실제로 구현한 모듈이 저수준 모듈이다.

예를 들어, 고객 등급에 따라 상품 가격에 할인 정책을 적용하는 기능을 생각해보자. 이 기능을 구현하려면,

  1. 고객 정보를 조회한다.
  2. 할인 정책에 따라 할인 금액을 계산한다.

와 같은 기능이 필요하다. 고객 정보를 조회하는 기능은 "JPA를 사용해 RDBMS에서 고객 정보를 조회하는 모듈"이 될 수 있고, 할인 정책은 "등급별 할인 정책 구현 클래스" 등이 될 수 있다. 이 두 모듈은 각각 저수준 모듈이며, 고수준 모듈은 이들을 조합해 할인 기능이라는 상위 정책을 구현한다.

 

고수준 모듈이 동작하려면 저수준 모듈을 사용해야 하지만, 계층형 아키텍처에서 언급한 문제가 발생한다. DIP는 추상화한 인터페이스를 사용하여 저수준 모듈이 고수준 모듈을 의존하도록 바꿀 수 있다. 

 

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

public class CalculateDiscountService {
	private RuleDiscounter ruleDiscount;
    
    public CalculateDiscountService(RuleDiscounter ruleDiscounter){
    	this.ruleDiscounter = ruleDiscounter;
    }
    
    public Money calculateDiscount(List<OrderLine> orderLines, String customerId){
    	Customer customer = findCustomer(customerId);
    	return ruleDiscouter.applyRules(customer, orderLines);
    }
    ...
}

 

 

이와 같은 구조를 가지게 된다면 CalculateDiscountService는 구체적인 구현 기술에 의존하지 않는다. "할인 정책에 따른 금액 계산"은 고수준 모듈의 개념이기 때문에 RuleDiscounter는 고수준에 속하고, CalculateDiscountService는 고수준 모듈인 RuleDiscounter를 의존하게 된다. 그리고 OOORuleDiscounter는 RuleDiscounter를 구현하게 된다. 즉, 저수준의 모듈이 고수준의 모듈을 의존하게 된다. 

 

고수준의 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 의존성 역전 원칙(Dependency Inversion Principle)이라고 부른다. 

 

DIP를 적용하면 인프라스트럭처를 의존했을 때 발생하는 문제를 해결할 수 있다. 

 

DIP를 이용하면 구현 방식 변경에 따른 어려움을 해결할 수 있다. 

// 저수준 객체 생성
RuleDiscounter aDiscounter = new ADiscounter();

// 생성자 주입
CalculateDiscountService discountService = new CalculateDiscountService(aDiscounter);

 

구현 방식이 변경됐을 경우, 변경된 저수준 객체만 변경해주면 CalculateDiscountService의 코드를 변경하지 않아도 된다. 

// 변경된 저수준 객체 생성
RuleDiscounter bDiscounter = new BDiscounter();

// 변경된 저수준 객체를 생성자 주입
CalculateDiscountService discountService = new CalculateDiscountService(bDiscounter);

 

 

DIP를 이용한다면 테스트를 용이하게 할 수 있다. DIP를 이용하면 의존하는 객체가 고수준 모듈의 인터페이스이므로 대역객체를 사용해서 테스트가 가능해진다. 

 

@AllArgsConstrunctor
public class CalculateDiscountService {
	private CustomerRepository customerRepository;
    private RuleDiscounter ruleDiscounter;
    
    public Money calculateDiscount(List<OrderLine> orderLines, String customerId){
    	Customer customer = findCustomer(customerId);
    	return ruleDiscounter.applyRules(customer, orderLines);
    }
    
    private Customer findCustomer(String customerId){
    	Customer customer = customerRepository.findById(customerId);
        if(customer == null) throw new NoCustomerException();
        return customer;
    }
}

public class CalculateDiscountServiceTest{
	
    @Test
    public void customer가_없는_경우_예외_발생() {
    	// 대역 객체
        CustomerRepository stubRepo = mock(CustomerRepository.class);
        when(stubRepo.findById("id")).thenReturn(null);
        
        RuleDiscounter stubRule = (cust, lines) -> null;
        
        // 대역 객체로 테스트
        CalculateDiscountService calDisSvc = 
        	new CalculateDiscountService(stubRepo, stubRule);
            
        assertThrows(NoCustomerException.class, 
        	() -> calDisSvc.calculateDiscount(someLines, "id"));
    }
}

 

 

2.3.1. DIP 주의사항

DIP는 저수준 모듈이 고수준 모듈을 의존하게 하는 것이기 때문에 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출하고 고수준 무듈에 위치해야 한다. 

 

 

 

2.3.2. DIP와 아키텍처

 

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

 

 

 

 

 

 

참고도서 

최범균 - 도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지 (https://product.kyobobook.co.kr/detail/S000001810495)