객체지향

[DDD, 도메인 주도 개발] 2.아키텍처 개요

마디니 2023. 3. 12. 19:18
반응형

아키텍처

표현 영역

  • 사용자의 요청을 받아 응용 영역에 전달하고 응용영역의 결과를 다시 사용자에게 전달한다.
  • HTTP요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고 응용영역의 응답을 HTTP응답으로 변환하여 전송한다.
  • Controller

응용 영역

  • 시스템이 사용자에게 제공해야할 기능을 구현
  • 기능 구현을 위해 도메인 모델을 사용하고 로직을 직접 수행하기 보다는 도메인 모델에 로직 수행을 위임한다.
  • Service

도메인 영역

  • 도메인의 핵심로직을 구현
  • 기능 구현을 위해 도메인 모델을 사용하고 고직을 직접 수행하기 보다는 도메인 모델에 로직 수행을 위임한다.
  • @Entity, @Repository

 

인프라스트럭처 영역

  • 논리적인 영역보다는 실제 구현을 다루는 영역
  • RDBMS, MONGO DB, SMTP, HTTP 클라이언트를 이용한 외부  API 호출
  • 표현/응용/도메인 영역은 구현기술을 사용한 코드를 직접 만들지 않고 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.

계층 구조 아키텍처

 

  • 상위 계층에서 하위 계층으로의 의존만 존재한다.
  • 응용 계층은 바로 아래 도메인 계층에만 의존해야하지만 구현의 편리함을 위해 더 아래 계층인 인프라스트럭처 계층에 의존하기도 한다.

 

 

 

 

 

 

 

 

[전형적인 계층 구조상의 의존 관계]

계층 구조에 따르면 도메인과 응용 계층은 DB나 외부시스템 연동을 위해 인프라스트럭처에 의존하게 된다. 

// 인프라스트럭처 영역
public class DroolsRuleEngine {
  private KContainer kcontainer;
  ...
  
  //룰 엔진 구현
  public void evaluate(String sessionName, List<?> facts) {
  	KSession kSession = kContainer.newKSession(sessionName);
    ... (구현 생략) ...
  }
}

// 응용 영역이 인프라스트럭처 영역인 DroolsRuleEngine에 의존하고 있다.
public class CalculateDiscountService {
	private DroolsRuleEngine ruleEngine;
    
    public CalculateDiscountService() {
    	ruleEngine = new DroolsRuleEngine();
    }
    
    public Money calculatedDiscount(List<OrderLine> orderLines, String customerId) {
    	Customer customer = findCustomer(customerId);
        
        MutableMoney money = new MutableMoney(0);
        List<?> facts = Arrays.asList(customer, money);
        facts.addAll(orderLines);
        
        ruleEngine.evalueate("discountCalculation", facts);  //discountCalculation 는 구현체의 세션이름으로 사용된다.
        return money;
    }
	
}

 

 

위 코드에는 아래와 같은 두가지 문제점이 존재한다.

1. 테스트가 어렵다.

CalculateDiscountService 가 올바르게 동작하는지 확인 하기 위해서는 Drools 코드가 잘 동작됨이 우선  보장되어야 한다.

2. 기능 확장이 어렵다.

내부에 Drools 코드에 특화된 코드가 많기 때문에 구현 방식을 변경하기 어렵다. 

예를들어 Drools의 세션 이름을 변경하면 CalculateDiscountService 의 코드도 함께 변경해야 한다.

 

위 문제는 DIP를 통해 고수준 모듈이 더이상 저수준 모듈에 의존하지 않고 구현을 추상화한 인터페이스에 의존하게 하여 해결할 수 있다.


DIP (Dependency Inversion Principle)

고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데,

반대로 저수준 모듈이 고수준 모듈에 의존할 때 DIP(의존역전원칙) 라고 부른다

 

고수준 모듈 : 의미 있는 기능을 제공하는 모듈

저수준 모듈 : 고수준 모듈을 구현하기 위해 필요한 하위 기능들을 실제로 구현한 것

 

고수준과 저수준의 이해

 

[DIP 적용]

저수준 모듈이 고수준 모듈에 의존하도록 DIP를 적용한 예

 

DIP 적용 시 장점

1. 변경이 유연해짐 - 구현 기술 교체가 용이

  • CalculateDiscountService는 생성자 주입방식을 통해 RuleDiscounter 을 받는다.
  • 구현 기술이 바뀌면 Service로직의 생성자 파라미터만 바뀐 구현체로 변경해주면 된다.
// 사용할 저수준 객체 생성
//RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();
// 구현체가 바뀌면 바뀐 구현체 객체 생성
RuleDiscounter ruleDiscounter = new NewRuleDiscounter();

// 서비스 생성자 주입
CalculateDiscountService service = new CalculateDiscountService(ruleDiscounter);

2. 테스트가 쉬워진다.

  • CalculateDiscountService 가 의존하는 객체의 동작여부에 상관 없이 테스트 코드 작성 가능
  • 테스트 코드에서의 객체의 의존 주입은 mock객체 사용

[DIP 적용시 주의사항]

  • DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다.
  • DIP 목적은 고수준 모듈이 저수준 모듈에 의존하지 않게 하기 위해서임
  • 저수준에서 인터페이스를 추출하는 것을 조심

[DIP와 아키텍처]

  • DIP를 적용하면 아키텍처 수준에는 인프라스트럭처 영역이 응용영역과 도메인 영역에 의존하는 구조가 된다.
  • 응용영역과 도메인 영역에 영향을 최소화 하면서 구현체를 변경하거나 추가 할 수 있다.
💡DIP를 항상 적용할 필요는 없다. 사용하는 구현 기술에 따라 완벽한 DIP를 적용하기보다는 구현 기술에 의존적인 코드를 도메인에 일부 포함하는게 효과적일 때도 있다. 
💡예를 들어 프레임워크의 기술(@Transaction)이나 영속성 처리(@Entity)와 같은 경우에는 DIP가 주는 이점보다 구현의 편리함이 주는 이점이 더 크다
💡 추상화 할 부분이 잘 떠오르지 않으면 DIP 이점을 얻는 수준에서 적용 범위 고민해보자

 

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

엔티티 (ENTITY) 

  • 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 도메인의 고유한 개념을 표현하며 도메인 모델의 데이터와 관련된 기능을 제공한다. 예) Order, Member
  • 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화 해서 데이터가 임의로 변경되는 것을 막는다.
  • 도메인 모델의 엔티티와 DB관계형 모델의 엔티티는 다르다.
    • 엔티티는 데이터 + 기능 포함
    • 두개 이상의 (DB) 데이터가 밸류(VALUE)타입으로 엔티티에 포함 될 수 있다. 예) 주문자 이름 +  주소 = Orderer

밸류(VALUE)

  • 개념적으로 표현할 때 사용되며 고유의 식별자가 없다. 엔티티의 속성으로도 사용되고 다른 밸류의 속성으로 사용될 수도 있다. 예) Address, Money(금액)
  • 밸류 타입의 데이터를 변경시 새로운 객체로 교체한다. (불변)

애그리거트(AGGREGATE)

  • 연관된 엔티티와 밸류 객체를 개념적으로 묶어놓은 군집.
    • 예) '주문'애그리거트에는 Order엔티티와 Orderer라는 밸류가 포함되어 있다.
  • 루트 엔티티를 갖는다.

💡루트엔티티는 애그리거트의 상태를 관리한다. 루트엔티티는 애그리거트에 속해있는 엔티티와 밸류 객체를 이용해 기능을 제공한다.

    애그리거트를 사용하는 코드는 루트엔티티를 통해서 다른 엔티티나 객체에 접근 할 수 있다. 

  • 서비스가 크고 복잡해질수록 많은 엔티티와 밸류가 출현 하는데, 이 때 애그리거트는 도메인 모델을 상위 수준에서 바라볼 수 있게 해준다.

https://viblo.asia/p/domain-driven-design-phan-2-MgNeWoZAeYx

 

리포지터리(REPOSITORY)

  • 구현을 위한 도메인 모델로 고수준 모듈에 속한다. (도메인 모델의 영속성을 처리하는데 필요한 기능을 추상화 했다.)
  • 엔티티 객체를 조회하거나 저장하는 기능을 제공한다.

도메인서비스(DOMAIN SERVICE)

  • 특정 엔티티에 속하지 않는,  여러 엔티티와 밸류를 필요로 하는 도메인 로직을 제공한다. 
  • 예) 할인금액 계산은 상품, 쿠폰, 회원, 주문금액 등의 다양한 조건이 필요한데 이때 도메인 서비스에서 로직을 구현한다.

 

요청처리 흐름

  • Controller
    • 사용자가 전송한 데이터 형식 validation 검사, 서비스가 요구하는 형식으로 변환
    • 문제가 없다면 Service에 위 데이터와 함께 기능 실행 위임
  • Service
    • 도메인 모델을 이용해서 기능 구현
    • 기능구현에 필요한 도메인 객체를 리포지토리에서 가져오거나, 신규 도메인 객체를 저장
    • 도메인 상태를 변경하게 되는 영역이므로 트랜잭션 관리 필요
  • Domain Object
    • 서비스에서 요청한 도메인 로직 실행
  • Repository
    • DB로부터 도메인 객체 조회하거나 저장

모듈 구성

아키텍처의 각 영역은 별도 패키지에 위치하고, 패키지 구성에 정답은 없다.

  • 도메인이 크다면 하위 도메인별로 모듈을 나눈다
  • 도메인 모듈은 도메인에 속한 애그리커트를 기준으로 다시 패키지를 구성한다.
  • 애그리거트, 모델, 리포지토리는 같은 패키지에 위치시킨다.
  • 도메인이 복잡하면 도메인 모델과 도메인 서비스를  별도 패키지에 위치 시킬 수도 있다.

아키텍처별, 도메인별로 패키지 구분은 필요하다. 나만의 방식을 찾아 놓으면 유용할 듯

 

 

Reference

도메인 주도개발 시작하기, 최범균 저

반응형