Home

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

도메인 주도 개발 시작하기

도메인 주도 개발 시작하기

최범균 지음

한빛미디어

2. 아키텍처 개요

2-1. 네 개의 영역

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

    • 표현 영역은 사용자의 요청을 받아 응용 영역에 전달하고, 처리 결과를 받아 보여주는 역할을 한다.
    • 응용 영역은 시스템이 사용자에게 제공해야 할 기능을 구현한다. 기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다.
    • 도메인 영역은 도메인 모델을 구현하며, 도메인의 핵심 로직을 구현한다.
    • 인프라스트럭처 영역은 구현 기술에 대한 것을 다루는데, 논리적인 개념을 표현하기 보다는 실제 구현을 다룬다.

아키텍쳐

2-2. 계층 구조 아키텍처

  • 일반적인 아키텍쳐는 다음과 같다.

아키텍쳐

  • 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고, 하위 계층은 상위 계층에 의존하지 않도록 해야 한다.
  • 계층 구조를 엄격하게 적용한다면 바로 아래 계층으로만 의존해야하지만, 때때로 개발의 편의성을 위해서 다음처럼 유연하게 가져가기도 한다. 하지만 한가지 유의 해야할 점은 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속된다는 것이다.

유연한 아키텍처

  • 위와 같이 유연한 구조를 가지든, 계층형 구조를 가지든 상위 계층은 하위 계층에 의존적이게 된다. 이렇게 하위 계층에 의존적인 코드가 되는 경우 2가지 문제가 발생할 수 있는데, 테스트가 어려워지고 기능 확장이 어려워진다는 것이다. ( 이쪽 부분 설명이 조금 불친절한 느낌인데.. 코드를 같이 보자.)
// 인프라 스트럭쳐단이라 생각한다.
public class DroolsRuleEngines {
 private kieContainer kContainer;
 
 public DrrolsRuleEngine() {
 // 생성자
 }

 public void evalute(String sessionName, List<?> facts) {
 // 평가 함수
 }
}

// 응용 계층이다.
public class CalculateDiscountService {
 private DroolsRuleEngines ruleEngine;

 public CalculateDiscountService() {
  ruleEngine = new DroolsRuleEngines();
 }

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

  MutableMoney money = new MutableMoney(0);
  List<?> facts = Arrays.asList(customer, money);
  facts.addAll(orderLine);
  ruleEngine.evalute("discountCalculation", facts);
  return money.toImmutableMoney();
 }
}
  • CalculateDiscountService를 테스트 하기 위해서는 DroolsRuleEngines 인스턴스가 있어야 한다. DroolsRuleEngines가 동작하지 않는 환경(staging 환경이 없는 등)이라면 테스트 하는게 어렵다.
  • calculateDiscount 함수에서 evalute 함수를 호출할때 사용하는 "discountCalculation" 파라미터가 다른 이름으로 호출되야 하도록 기능이 변경되었다고 생각해보자. 파라미터가 변경되었으니 calculateDiscount() 메소드 안에서 호출할때 넘긴 파라미터도 수정해야 한다. 즉, 현재 CalculateDiscountService 클래스는 DroolsRuleEngines 클래스에 의존하고 있다는 것이다. 그렇다면 어떻게 해야 이 문제를 해결 할 수 있을까.?

2-3. DIP

  • calculateDiscount() 함수를 잘 보면 우리는 다음처럼 생각 할 수 있다.
 ------ CalculateDiscountService -----
 |         고객 정보를 구한다         |  --> RDBNS에서 JPA로 구한다.
 |  룰을 이용해서 할인 금액을 구한다   |  --> Drools롤 룰을 정한다.
 -------------------------------------
             고수준                             저수준
  • 고수준 모듈은 의미 있는 단일 기능을 제공하는 모듈을 말하는데, 고수준 모듈을 구현하기 위해서는 여러 하위기능이 필요하다.
  • 저수준 모듈은 하위 기능을 실제로 구현한것을 의미한다.
  • 고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야 한다. 그런데 고수준 모듈이 저수준 모듈을 사용하면서 계층 구조 아키텍쳐에서 언급했던 두가지 문제, 구현 변경의 어려움테스트의 어려움이라는 두가지 문제에 부딪치게 된다.
  • DIP는 이 문제를 해결하기 위해서 저수준 모듈이 고수준 모듈에 의존하도록 바꿔준다. 이를 바꿔줄때 사용하는 것이 추상화한 인터페이스이다.
public class DroolsRuleEngines {
 private kieContainer kContainer;
 
 public DrrolsRuleEngine() {
 // 생성자
 }

 public void evalute(String sessionName, List<?> facts) {
 // 평가 함수
 }
}

// 인터페이스가 추가 되었다.
public interface RuleDiscounter {
 Money applyRules(Customer customer, List<OrderLine> orderLines);
}

// 인터페이스 구현 클래스
public class DroolsRuleDiscounter implements RuleDiscounter {
 private kieContainer kContainer;

 public DroolsRuleEngine() {
 //.. DroolsRuleEngine을 가져온다.
 }

 @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(List <OrderLine> orderLines, String coustomerId) {
  Customer customer = findCustomer(customerId);
  return ruleDiscounter.applyRules(customer, orderLines);
 }
}
  • CalculateDiscountService는 더이상 Drools에 의존하는 코드가 존재하지 않는다. 룰을 이용한 할인 금액 계산을 추상화한 RuleDiscounter 인터페이스에 의존한다.
  • 또한 RuleDiscounter가 룰을 적용한다는 사실(applyRules())만 알고 있으며 RuleDiscounter는 생성자를 통해서 외부에서 주입받는다.
  • 위 구조를 그려보면 다음과 같아 진다.

DIP

  • DIP을 적용하면 저수준 모듈이 고수준 모듈에 의존하게 된다. 고수준 모듈이 저수준 모듈을 이용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 DIP(Dependency Inversion Principle), 의존성 역전 법칙이라 한다.
  • DIP를 적용하면 구현 변경의 어려움테스트의 어려움 이 2가지 문제를 해결 할 수 있다.

    • 구현변경의 어려움
    • 구현 기술을 변경하더라도 생성자에 넘기는 객체만 변경해서 넘겨주면 된다.
    • 단 넘기는 객체는 RuleDiscounter Class를 구현한 클래스여야 한다.
    • 테스트의 어려움
    • 테스트 프레임워크를 이용한다면 저수준 모듈의 Mock이나 Stub은 손쉽게 만들 수 있다.
  • 하지만 DIP을 사용할때 주의해야하는 점이 있는데, 하위 기능을 추상화한 인터페이스는 고수준 모듈에서 도출해야 한다는 것이다. CalculateDiscountService 입장에서 봤을 때 할인 금액을 구하기 위해 룰 엔진을 사용하는지, 직접 연산하는지는 중요하지 않다. 단지 규칙에 따라서 할인금액이 계산된다는 점만이 중요한 것이다.

고수준 모듈에 위치한 인터페이스

  • DIP을 항상 적용할 필요는 없다. 사용하는 구현 기술에 따라 완벽한 DIP을 적용하기 보다는 구현 기술에 의존적인 코드를 도메인에 일부 포함하는게 효과적인 경우도 있고 또는 추상화 대상이 잘 떠오르지 않는 경우가 있다. 이때는 DIP를 무조건적으로 대입하기 보다는 대입했을 때 얻는 이점을 잘 생각해보고 대입하는게 좋다.

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

도메인 영역의 모델은 도메인의 주요 개념을 표현하며 핵심 로직을 구현한다. 그렇다면 도메인을 구성하는 요소는 무엇이 있을까.?

  • 엔티티(ENTITY)

    • 고유한 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다.
    • 도메인 모델의 데이터를 포함하여 해당 데이터와 관련된 기능을 제공한다.
  • 밸류(VALUE)

    • 고유의 식별자를 갖지 않는 객체로 개념적으로 하나인 값을 표현할 때 사용한다.
    • 엔티티의 속성으로 사용할 뿐만 아니라 다른 밸류 타입의 속성으로도 사용할 수 있다.
  • 애그리거트(AGGREGATE)

    • 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것을 말한다.
  • 리포지토리(REPOSITORY)

    • 도메인 모델의 영속성을 처리한다.
  • 도메인 서비스(DOMAIN SERVICE)

    • 특정 엔티티에 속하지 않은 도메인 로직을 처리한다.

엔티티와 밸류

착각할 수 있는 것이 있는데, 실제 도메인 모델의 엔티티와 DB 관계형 모델의 엔티티는 같은 것이 아니다. 이 2개를 착각하면 안되는데, 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 제공한다. 즉, 도메인 모델의 엔티티는 단순히 데이터를 담고 있는 데이터 구조라기 보다는 데이터와 함께 기능을 제공하는 객체를 의미한다. 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화 해야 하는 것이다.

도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류를 통해서 표현할 수 있으며 불변값 형태로 이를 표현해야 한다.

애그리거트

도메인이 커질수록 개발할 도메인 모델도 커지게 되는데, 이때 도메인 모델이 복잡해지면서 개발자가 전체 구조가 아닌 한 개 엔티티와 밸류에만 집중하는 상황이 발생할 수 있다. 개별요소에만 초점을 맞추다 보면 큰 수준에서 모델을 이해하지 못해 관리 할수 없게 되는 경우가 발생한다. 애그리거트는 도메인 모델에서 전체 구조를 이해하는 데 도움이 되게 한다.

애그리거트는 관련 객체를 하나로 묶은 군집이다. 애그리거트를 사용하면 개별 객체가 아닌 관련 객체를 묶어서 객체 군집 단위로 모델을 바라볼 수 있게 된다. 개별 객체 간의 관계가 아닌 애그리거트 간의 관계로 도메인 모델을 이해하고 구현하게 되며, 이를 관리 할 수 있게 해준다.

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

리포지토리

엔티티나 밸류가 요구사항에서 도출되는 도메인 모델이라면 리포지토리는 구현을 위한 도메인 모델이다. 리포지토리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.

리포지토리는 다음 두가지 메서드가 기본이 된다.

  • 애그리거트를 저장하는 메서드
  • 애그리거트 루트 식별자로 애그리거트를 조회하는 메서드

2-5 요청 처리 흐름

요청 처리 흐름

Loading script...