[DDD] 2장. 아키텍처 개요

매빈·2023년 3월 11일
0

1. 아키텍처

2.1 네 개의 영역: 표현, 응용, 도메인, 인프라스트럭처

2.1.1 표현

  • 사용자의 요청을 해석해서 응용 서비스에 전달하고, 응용 서비스의 실행 결과를 사용자가 이해할 수 있는 형식으로 변환하여 응답.
  • 스프링 MVC 프레임워크가 표현 영역을 위한 기술에 해당
  • HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환하여 응용 영역에 전달 ➡️ 응용 영역의 응답을 HTTP 응답으로 변환하여 전송

2.1.2 응용

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

2.1.3 도메인

  • 도메인의 핵심 로직을 구현

2.1.4 인프라스트럭처

  • 구현 기술에 대한 것을 다룸

  • 구현 기술

    • RDBMS 연동을 처리
    • 메시징 큐에 메시지를 전송하거나 수신하는 기능 구현
    • mongoDB나 Redis의 데이터 연동 처리
      SMTP를 이용한 메일 발송 기능을 구현
    • HTTP 클라이언트를 이용하여 REST API를 호출
  • 도메인, 응용, 표현 영역은 구현 기술을 사용한 코드를 만들지 않고, 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발함

2.2 계층 구조 아키텍쳐

  • 계층 구조의 아키텍처 구성
  • 상위 계층 ➡️ 하위 계층으로의 의존만 존재
    ex. 표현 ➡️ 응용 의존 (O) / 인프라 ➡️ 도메인 의존 (X)
  • 엄격하게는 바로 아래의 계층에만 의존을 가져야 함. 하지만 구현의 편리함을 위해 계층 구조를 유연하게 적용하기도 함.
  • 표현, 응용, 도메인 계층은 인프라스트럭처 계층에 종속됨
  • 인프라스트럭처에 의존하면 '테스트 어려움'과 '기능 확장의 어려움'이 발생하는데, 이것을 해결하기 위해 DIP를 알아야 함

2. DIP

2.3 DIP

  • 고수준 모델과 저수준 모델

  • 고수준 모듈이 제대로 동작하기 위해 저수준 모듈을 사용해야 하는데, 이때 구변 변경과 테스트가 어렵다는 문제가 발생함.
    ➡️ DIP는 이 문제를 해결하기 위해 추상화한 인터페이스를 이용하여 저수준 모듈이 고수준 모듈에 의존하도록 바꿈.

    // 소스코드에도 어디에 있는지 모르겠어서... 일단 여기에 작성
    public class DroolsRuleDiscounter implements RuleDiscounter {
    private KieContainer kContainer;
    
    public DroolsRuleEngine() {
    KieServices ks = KieServices.Factory.get();
    kContainer = ks.getKieClasspathContainer();
    
    @Override
    public Money applyRules(Customer customer, List<OrderLine> orderLines) {
    	KieSession kSession = kContainer.newKieSession("discountSession");
    
        try {
          ...
          kSession.fireAllRules();
        } finally {
          kSession.dispose();
        }
        return money.toImmutableMoney();
      }
    }
    
  • 즉, 저수준 모듈이 고수준 모듈에 의존하는 것을 DIP, Dependency Inversion Principle이라고 함.

    		// 사용할 저수준 객체 생성
      RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();
      // 생성자 방식으로 주입
      CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);
      
      // 구현 기술 변경
      // 사용할 저수준 구현 객체 변경
      RuleDiscounter ruleDiscounter = new SimpleRuleDiscounter();
      // 사용할 저수준 모듈을 변경해도 고수준 모듈을 수정할 필요가 없음
      CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);

2.3.1 DIP 주의사항

  • DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함.
  • 저수준모듈에서 인터페이스를 추출하면 안 됨, 여전히 고수준 모듈이 저수준 모듈에 의존하고 있는 것이기 때문.

2.3.2 DIP와 아키텍쳐

  • 저수준 모듈: 인프라스트럭처 영역
  • 고수준 영역: 응용 영역, 도메인 영역
    ➡️ DIP 적용시 인프라스트럭처 영역이 응용, 도메인 영역에 의존(상속)하는 구조가 됨. 따라서 도메인과 응용 영역에 대한 영향을 주지 않거나 최소화하며 구현 기술을 변경하는 것이 가능.
  • DIP 적용 구조
    • EmailNotifier(인프라)는 Notifier(응용) 인터페이스를 상속 ➡️ 요구사항을 수정해야 할 때 OrderService를 변경할 필요 없이 구현 클래스를 인프라 영역에 추가하면 됨.

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

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

  • 엔티티: 고유 식별자를 갖는 객체로 자신의 라이프사이클을 가지며, 도메인의 고유한 개념을 표현함.
  • 밸류: 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현할 때 사용함.
  • 애그리거트: 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것.
  • 리포지터리: 도메인 모델의 영속성을 처리
  • 도메인 서비스: 특정 엔티티에 속하지 않은 도메인 로직을 제공

2.4.1 엔티티와 밸류

  • 도메인 모델의 엔티티 vs. DB 모델의 엔티티
    • 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공.
    • 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용하여 표현할 수 있음.

2.4.2 애그리거트

  • 규모가 커질 수록 도메인 모델의 구성요소는 복잡해짐 ➡️ 개발자가 전체 구조가 아닌 한 개 엔티티와 밸류에만 집중하는 상황 발생 ➡️ 큰 틀에서 모델을 관리할 수 없는 상황에 빠짐
  • 애그리거트: 관련 객체를 하나로 묶은 군집으로, 상위 수준에서 모델을 볼 수 있게 함.
  • 루트 엔티티: 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용하여 애그리거트가 구현해야 할 기능을 제공.
  • 애그리거트를 사용하는 코드는 애그리거트 루트가 제공하는 기능을 실행, 또한 이를 통해 애그리거트 내의 다른 엔티티나 밸류 객체에 접근 ➡️ 애그리거트의 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화할 수 있도록 도움.

2.4.3 리포지터리

  • 리포지터리: RDBMS, NoSQL, 로컬 파일과 같이 도메인 객체를 지속적으로 보관, 사용하기 위해 이용하는 도메인 모델.

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

    public interface OrderRepository {
    	// 대상을 찾고 저장하는 단위가 애그리거트 루트인 Order임.
      // 애그리거트에 속한 모든 객체를 포함하고 있기 때문.
    	Order findByNumber(OrderNumber number);
      void save(Order order);
      void delete(Order order);

    ➡️ 도메인 객체를 영속화하는데 필요한 기능을 추상화한 것으로 고수준 모듈에 속하며, 실제 구현 클래스틑 인프라스트럭처 영역에 속함

  • 응용 서비스와 리포지터리의 연관성

    • 응용 서비스는 필요한 도메인 객체를 구하거나 저장할 때 리포지터리를 사용
    • 응용 서비스는 트랜잭션을 관리하는데, 트랜잭션 처리는 리포지터리 구현 기술의 영향을 받음
  • 리포지토리가 제공하는 메서드

    • 애그리거트를 저장하는 매서드
    • 애그리거트 루트 식별자로 애그리거트를 조회하는 메서드
      ex.
    public interface SomeRepository {
    	void save(Some some);
      Some findById(SomeId id);
  • 리포지터리를 구현하는 방법은 선택한 구현 기술에 따라 달라짐.

2.5 요청 처리 흐름

  1. 표현 영역
  • 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 데이터를 이용해서 응용 서비스에 기능 실행을 위임.
  • 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달.
  1. 응용 서비스
  • 도메인 모델을 이용하여 기능을 구현.

  • 기능 구현에 필요한 도메인 객체를 리포지터리에서 가져와 실행하거나 신규 도메인 객체를 생성해서 리포지토리에 저장. 혹은 두 개 이상의 도메인 객체를 사용하여 구현하기도 함.
    ex.

    public class CancelOrderService {
    	private OrderRepository orderRepository;
      
      @Transactional // 응용 서비스는 트랜잭션을 관리
      public void cancel(OrderNumber number) {
      	Order order = orderRepository.findByNumber(number);
          if (order == null) throw new NoOrderException(number);
          order.cancel();
      }
    }

4. 인프라스트럭처

2.6 인프라스트럭처 개요

  • 표현 영역, 응용 영역, 도메인 영역을 지원
    = 도메인 객체의 영속성 처리, 트랜잭션, SMTP 클라이언트, REST 클라이언트 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원

  • 도메인 영역과 응용 영역에서 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는 것(=인프라에 대한 의존성을 없애는 것, DIP)이 시스템을 더 유연하고 테스트하기 쉽게 만들어줌.

  • 하지만 무조건 인프라스트럭처에 대한 의존을 없앨 필요는 없음. 구현의 편리함을 위해 DIP의 장점을 해치지 않는 범위에서 의존을 가져가는 것도 괜찮고, 완전히 의존을 없애고자 시도하는 것은 자칫 구현을 더 복잡하고 어렵게 만들 수 있기 때문.
    ex. @Transactional 애너테이션 ➡️ 이 애너테이션을 사용하지 않으려면 스프링에 대한 의존을 없애기 위해 복잡한 스프링 설정을 사용해야 함.

5. 모듈

2.7 모듈 구성

  1. 아키텍처 모듈 구조
  • 아키텍처의 각 영역은 별도 패키지에 위치함.
  • 패키지 구성 규칙에 정답이 존재하는 것이 아님.
  • 도메인이 크면 하위 도메인 별로 모듈을 나눌 수 있음.
  1. 도메인 모듈 구조
  • 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성

  • 모듈 구조를 얼마나 세분화해야 하는지 정해진 규칙은 없지만, 한 패키지에 너무 많은 타입이 몰려 코드를 찾을 때 불편하지 않도록 가능한 10~15개 미만으로 타입 개수를 유지하기


실습 링크

0개의 댓글