코딩못하는사람

2.아키텍처 개요 본문

DDD/도메인 주도 개발 시작하기

2.아키텍처 개요

공부절대안함 2022. 9. 3. 18:04

 

2장 주요내용

  • 아키텍처
  • DIP
  • 도메인 영역의 주요 구성 요소
  • 인프라스트럭처
  • 모듈

 

 

2.1 네 개의 영역

 

도메인,응용,표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다 - ? 64p 

대신 인프라 스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.

https://azderica.github.io/til/docs/dev/ddd-start/ch2/

ex) DB에 보관된 데이터가 필요하다면 인프라 영역의 DB모듈을 사용하여 데이터를 읽음.

 

 

2.2 계층 구조 아키텍처

계층 구조 아키텍처

계층 구조는 특성상 상위 계층에서 하위 계층으로의 의존 만 존재하고 하위 계층은 상위 계층을 의존하지 않는다.

하지만 구현의 편리함을 위해 계층 구조를 유연하게 적용하기도 한다.

예를 들어 응용 계층은 바로 아래 도메인 계층에 의존하지만 외부시스템과의 연동을 위해 인프라 계층에 의존하기도 한다.

 

하지만 이러한 구조를 가지게 된다면 응용,도메인 계층이 자세한 기술을 다루는 인프라 계층에 종속된다는 점을 인지해야 한다.

이렇게 되면 테스트가 어려워지고 기능 확장이 어려워 진다.

 

테스트가 어려움- CaclutaeDiscountService는 무조건 ruleEngine이 완전하게 동작해야 함.

기능 확장이 어려움- 종속적인 코드가 있으므로 다른 룰엔진을 사용하게 될때 코드 변경이 많아짐.

 

'예전에는 테스트하기가 어렵다' 라는 뜻의 의미를 이해하기 힘들었음.

DIP를 통해 해결


2.3 DIP

고수준과 저수준 모듈

고수준 모듈은 의미 있는 단일 기능을 제공하는 모듈로 위 사진에서 CalculateDiscountService는 "고객 할인 가격 계산" 기능을 구현하게 된다. 이러한 고수준 모듈 기능을 구현하려면 여러 하위 기능(저수준 모듈)이 필요하다.

예를 들어 jpa로 고객 정보를 구하고 고객에 따라 할인률을 적용하는 룰에 적용시켜야 한다.

하지만 2.2에서 얘기한 것 처럼 저수준 모듈을 사용하다보면 구현 변경과 테스트에 취약해지는데 DIP로 극복할 수 있다.

 

DIP는 고수준 모듈이 저수준 모듈에 의존하는 것이 아니라, 기능을 추상화한 인터페이스를 통해 저수준 모듈이 고수준 모듈에 의존하도록 해준다.

예를 들어 보자

public interface RuleDiscounter {
  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 customerId) {
    Customer customer = findCustomer(customerId);
    return ruleDiscounter.applyRules(customer, orderLines);
  }
}
public class DroolsRuleDiscounter implements RuleDiscounter {
  private KieContainer kContainer;

  public DroolsRuleDiscounter) {
    KieService ks = KieServices.Factory.get();
    kContainer = ks.getKieClasspathContainer();
  }

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

CalculateDiscountService는 할인룰적용을 Drools를 통해 구현했는지, 다른 모듈로 구현했는지 관심이 없고 그저 applyRules 메서드만 수행할 수 있으면 된다. 따라서 RuleDiscounter 인터페이스를 만들어 룰 적용을 구현할 클래스들이 상속받게 만들면 CalculateDiscountService는 더 이상 룰 관련 코드를 이해할 필요가 없다.

 

CalculateDiscountService RuleDiscounter는 "룰을 이용한 할인 금액 계산"을 추상화한 고수준 모듈 RuleDiscounter에 의존하게 되고, DroolsRuleDiscounter는 RuleDiscounter를 구현한 것으로 저수준 모듈이 고수준 모듈에 의존한 것이 된다.

(CustomerRepository 같은 인터페이스 레포지토리도 고수준 모듈)

 

이렇게 DIP 는 고수준 모듈이 저수준 모듈에 의존하는 것이 아니라, 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 DIP(Depedency Inversion Principle) 의존 역전 원칙 이라고 부른다

 

저수준 모듈은 의존 주입을 통해 생성자 주입등으로 전달 받게 되고, 스프링같은 의존관계 주입을 지원하는 프레임워크를 사용하면 설정코드 등으로 쉽게 구현체를 변경 가능하다.

 

이렇게 저수준 모듈에 의존했다면 실제 RuleDiscounter와 레포지토리를 구현해서 작동해야했기에 테스트가 힘들었겠지만, 인터페이스는 Mock과 같은 대역 객체를 사용해서 테스트를 쉽게 진행할 수 있다.

 

DIP 주의사항

그림 a
그림 b

DIP는 단순히 인터페이스와 구현 클래스를 분리하는 것이 아니라, 고수준 모듈이 저수준 모듈에 의존하지 않도록 하는것.

그림 a는 저수준 모듈에서 인터페이스를 추출한 경우이다. 이 구조는 도메인 영역이 구현 기술을 다루는 인프라 영역에 의존하고 있다. 왜? CaculateDiscountService 입장에서는 할인 금액을 구하는 모듈이 룰엔진을 사용하는지 어떤 방식을 사용하는지는 중요하지 않고 계산하는 것이 중요하기 때문이다. 

"DIP를 적용할때는 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해야한다"

DIP와 아키텍처

 

DIP를 통해 계층 구조에서 벗어나 인프라가 응용, 도메인 영역에 의존하는 구조가 되므로써 도메인과 응용영역에 영향을 주지 않거나 최소화하면서 구현 기술(구현체)을 변경 및 추가 할 수 있게 된다 ( 변경이 쉬워진다).

 

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

요소설명

요소 설명
엔티티(ENTITY) 고유의 식별자를 갖는 객체, 자신의 라이프 사이클
밸류(VALUE) 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 도메인 객체의 속성을 표현할 때 사용. 엔티티의 속성과 밸류 타입의 속성으로도 사용
애그리거트(AGGREGATE) 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것
리포지터리(REPOSITORY) 도메인 모델의 영속성을 처리
도메인서비스(DOMAIN SERVICE) 특정 엔티티에 속하지 않은 도메인 로직을 제공, 여러 엔티티와 벨류가 필요한 서비스일 때

-앤티티와 밸류

도메인 모델의 엔티티와 RDBMS형 모델의 엔티티는 다르다.

도메인 모델의 엔티티는 데이터와 도메인 기능을 함께 제공한다는 점 (ex 주문 엔티티는 배송지 주소 변경을 위한 기능도).

단순히 데이터를 담고 있는 구조가 아니라 데이터와 기능을 제공하고 도메인 관점에서 기능 구현을 캡슐화해서 데이터 임의 변경을 막기도 한다.

또 RDBMS 같은 관계형 DB는 밸류 타입을 제대로 표현하기 힘들다(ex- Orderer(name, email) 밸류 타입 속성으로 빼내기) 밸류 타입을 통해 도메인 엔티티는 모델의 이해를 돕기 쉽게 한다.

 

밸류는 불변으로 구성하기를 권장하고 그것은 밸류타입을 변경할때 객체 자체를 완전히 교체한다는 의미이다.

 

-애그리거트

도메인이 커질수록 개발할 도메인 모델도 커지면서 많은 엔티티와 벨류가 생기게 된다(복잡해진다)

전체적인 틀에 초점을 맞추지 못하고 개별 요소에 빠질 수 있기 때문에, 전체적인 틀을 나눠서 볼 필요가 있다.

애그리거트는 관련 객체를 하나로 묶은 군집이다.

애그리거트를 사용하면 개별 객체가 아닌 객체 군집 단위로 모델을 바라볼 수 있게 되고, 객체 간의 관계가 아니라 애그리거트 간의 관계로 도메인 모델의 큰 틀을 관리할 수 있다.

 

애그리거트는 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다.

루트 엔티티는 애그리거트에 속해있는 엔티티와 밸류 객체를 이용해 애그리거트가 구현해야 할 기능을 제공.

애그리거트 루트를 통해 간접적으로 애그리거트 내의 객체에 접근하므로써 애그리거트 내부구현을 숨겨 애그리거트 단위로 캡슐화할 수 있도록 한다.

ex) 주문 애그리거트는 Order를 통하지 않고 ShippingInfo를 변경할 수 없게 설계한다.

 

애그리거트를 어떻게 구성했느냐에 따라 구현이 복잡해지기도 하고 트랜잭션 범위도 달라진다. 또한 구현 기술에 따라 제약이 생기기도 한다.

 

-리포지터리

도메인 객체를 지속적으로 사용하려면 RDMBS, NoSQL, 로컬 파일과 같은 물리적인 저장소에 보관해야 한다.

이를 위한 도메인 모델을 Repository라고 부른다. 엔티티나 벨류가 요구사항에서 도출되는 도메인 모델이라면 리포지터리는 구현을 위한 도메인 모델이다(?).

애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.

OrderRepository는 Order 루트 엔티티를 저장할 것이고, Order안에 애그리거트에 속한 모든 객체들이 포함되므로 결과적으로 애그리거트 단위로 저장하고 조회한다.

 

도메인 모델 관점에서 Repository는 도메인 객체를 영속화하는데 필요한 기능을 추상화 한것으로 고수준 모듈에 속한다.(구현체들은 저수준 모듈로 인프라)

애그리거트를 저장하고 루트 식별자로 조회하는 두가지 메서드가 기본이 된다.

 

2.6 인프라스트럭처 개요

무조건 인프라 단에 의존을 없앨 필요는 없다. 스프링을 사용할 경우 트랜잭션 처리를 위해 @Transactional을 사용하는 것이 편리하고, JPA를 사용할 경우 @Entity나 @Table 같은 전용 어노테이션을 사용하는것이 편리하다.

 

구현의 편리함은 DIP가 주는 장점만큼 중요하기 때문에 DIP를 해치지 않는 범위에서 의존을 가져가는 것이 좋다. 의존을 완전히 없앤다면 구현이 복잡할 수 있다.

ex) @Transactional 한줄로 처리가능한 부분을 복잡하게 구현해야함.

 

2.7모듈 구성

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

  • 아키텍처의 각 영역은 별도 패키지에 위치합니다.
  • 도메인이 크면 하위 도메인으로 나누고 각 하위 도메인마다 별도 패키지를 구성합니다.
  • 도메인이 복잡하면 도메인 모델과 도메인 서비스를 다음과 같이 별도 패키지에 위치할 수도 있습니다.
    • com.myshop.order.domain.order : 애그리거트 위치
    • com.myshop.order.domain.service : 도메인 서비스 위치
  • 응용 서비스도 다음과 같이 도메인 별로 패키지를 구분할 수 있습니다.
    • com.myshop.catalog.application.product
    • com.myshop.catalog.application.category
  • 모듈 구조를 얼마나 세분화해야 하는지에 대해 정해진 규칙은 없습니다.
    • 가능하면 한 패키지에 10개 미만으로 유지하는 것이 좋습니다

 

 

 

그림참조 https://azderica.github.io/til/docs/dev/ddd-start/ch2/

Comments