도메인 주도 설계 (2) - 아키텍처

gentledot·2021년 8월 15일
0

아키텍처의 전형적인 영역

  • 표현
  • 응용
  • 도메인
  • 인프라

그림 2.1 아키텍처 네 개의 영역 (p37)

  • 표현 영역은 사용자의 요청을 해석해서 응용 서비스에 전달하고 응용 서비스의 실행 결과 를 사용자가 이해할 수 있는 형식으로 변환해서 응답한다.

    • Spring MVC는 표현 영역을 위한 기술에 해당한다.
    • Web Application에서 표현 영역의 사용자는 web browser를 사용하는 사람일 수 있고, REST(style) API를 호출하는 외부 시스템일 수 있다.
  • 웹 애플리케이션에서 표현 영역은 HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고, 응용 영역의 응답을 HTTP 응답으로 변환해서 전송한다.

    • 표현 영역은 웹 브라우저가 HTTP 요청 파라미터로 전송한 데이터를 응용 서비스가 요구하는 형식의 객체 타입으로 변환해서 전달하고, 응용 서비스가 리턴한 결과를 JSON형식으로 변환해서 HTTP 응답으로 웹 브라우저에 전송한다.
  • 표현 영역을 통해 사용자의 요청을 전달받는 응용 영역은 시스템이 사용자에게 제공해야할 기능을 구현한다.

  • 응용 영역은 기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다.

  • 응용 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다.
    그림 2.2 응용영역의 도메인 로직 구현은 도메인 모델에 위임한다.

  • 인프라스트럭처 영역은 구현 기술에 대한 것을 다룬다.

    • 이 영역은 RDBMS 연동을 처리하고, 메시징 큐에 메시지를 전송하거나 수신하는 기능을 구현하고, Mongo DB 나 HBase를 사용해서 데이터베이스 인동을 처리한다.
    • 이 영역은 SMTP를 이용한 메일 발송 기능을 구현하거나 HTTP 클라이언트를 이용해서 REST(Style) API를 호줄하는 것도 처리한다.
  • 인프라스트럭처 영역은 논리적인 개념을 표현하기보다는 실제 구현을 다룬다.

  • 도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 구현하진 않는다.

  • 인프라스트럭처 영역에서 제공하는 기능을 사용, 필요한 기능을 개발한다. (DB 모듈 사용한 데이터 조회, SMTP 연동 모듈을 이용한 메일 전송 등)

계층 구조 아키텍처

  • 계층 구조의 아키텍처 구성
    그림 2.4 계층 구조 아키텍처

    • 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다.
      • 표현 계층은 응용 계층에 의존 (O)
      • 인프라스트럭처 계층이 도메인에 의존(X)
  • 구현의 편리함을 위해 계층 구조를 유연하게 적용할 수 있다.
    인프라 계층에 종속되는 구조

    • 표현, 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속되는 구조
    • 인프라스트럭처에 의존되는 구조는 테스트가 어렵고, 기능 확장이 어려운 문제가 발생한다.
      • 인프라 영역에 연결하는 기술에 연결되므로 로직과 연결 기능이 함께해야 하는 문제로 인해 발생되는 문제로 정리될 수 있다.

DIP (Dependency Inversion Priciple)

  • 고수준 모듈과 저수준 모듈

    그림 2.7 고수준, 저수준 모듈

    • 고수준 모듈 : 의미 있는 단일 기능을 제공하는 모듈
    • 저수준 모듈 : 하위 기능을 실제로 구현한 것
    • 고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야 한다.
      • 다만, 고수준 모듈이 저수준 모듈을 시용하면 '구현 변경'과 '테스트가 어려운' 문제가 발생한다.
  • 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); 
        }
    
    }
    
    • 추상화한 인터페이스를 활용.

    • 다른 버전의 룰 적용 객체 생성 시 인터페이스를 구현(implements)하여 생성

      그림 2.8 DIP를 적용한 구조

  • DIP를 적용하면 저수준 모듈이 고수준 모듈에 의존하게 됨.

    • 고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 DIP(Oependency Invereion Principle, 의존 역전 원칙)라고 부른다.
    • 구현 기술 교체 ⇒ 생성자 주입으로 구현된 객체를 전달받아 구현
    • 테스트 ⇒ 인터페이스의 mock 객체를 설정하여 로직 구현의 테스트를 작성할 수 있음.

DIP의 주의사항

  • DIP를 잘못 생각하면 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아들일 수 있다.

    • DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함

    • DIP 적용한 결과 구조만 보고 저수준 모듈에서 인터페이스를 추출하는 경우가 있음.

      그림 2.10 잘못된 DIP - 고수준 모듈이 저수준 모듈에 의존하는 형태

    • 도메인 영역이 구현 기술을 다루는 인프라스트럭처 영역에 의존하는 형태

      • 즉, 고수준 모듈이 저수준 모듈에 의존하는 상태
      • RuleEngine 인터페이스는 고수준 모듈인 도메인 관점이 아니라 룰 엔진이라는 저수준 모듈 관점에서 도출한 것.
  • 즉, '할인 금액 계산' 을 추상화한 인터페이스는 저수준 모듈이 아닌 고수준 모듈에 위치해야 한다.
    그림 2.11 정상적인 모듈 구조

DIP와 아키텍처

  • 아키텍처 수준에서 DIP를 적용하면 인프라 영역이 응용 영역과 도메인 영역에 의존하는 구조가 된다.

    그림 2.13 DIP를 적용한 구조

    • 도메인과 응용 영역에 대한 영향을 주지 않거나 최소화하면서 구현 기술을 변경하는 것이 가능.
      • Notifier 변경 시 OrderService를 변경할 필요가 없음.
      • MyBatisOrderRepository → JpaOrderRepository 교체 가능

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

  • 주요 구성요소 (에릭 에반스 [도메인 주도 설계] 기반)
    • entity
      • 고유의 식별자를 갖는 객체로 자신의 라이프사이클을 갖는다.
      • 주문(Order), 회원(Member), 상품(Product)과 같은 도메인의 고유한 개념을 표현한다.
      • 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.
    • value (type)
      • 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 도메인 객체의 속성을 표현할 때 사용된다.
      • 배송지 주소를 표현하기 위한 주소(Address)나 구매 금액을 위한 금액(Money)과 갈은 타입
      • 엔티티의 속성으로 사용될 뿐만 아니라 다른 밸류 타입의 속성으로도 사용될 수 있다.
    • aggregate
      • 애그리거트는 관련된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것.
      • 예를 들어, 주문과 관련된 Order 엔티티, OrderLine 밸류, Orderer 밸류 객체를 '주문' 애그리거트로 묶을 수 있음.
    • repository
      • 도메인 모델의 영속성을 처리한다.
      • 예를 들어, DBMS 테이블에서 Entity 객체를 로딩하거나 저장하는 기능을 제공
    • domain service
      • 특정 엔티티에 속하지 않은 도메인 로직을 제공한다.
      • '할인 금액 계산'은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하게 되는데, 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 할 경우 도메인 서비스에서 로직을 구현한다.

Entity와 Value

  • Entity는
    • DB Table의 Entity, 그리고 domain model의 Entity로 구분될 수 있다.
      • 두 모델의 가장 큰 차이점은 domain model의 Entity에서는 데이터와 함께 도메인 기능을 함께 제공한다는 점.
        • ex) 주문 entity = 주문 정보 + 주문 관련 기능(배송지 변경 등) 제공
        • 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화해서 데이터가 임의로 변경되는 것을 막는다.
      • domain model의 entity는 두 개 이상의 데이터가 개념적으로 하나인 경우 value type을 이용해서 표현할 수 있다는 것.
        • 주문자의 필드로 주문자명, 주문자 이메일 데이터를 포함할 수 있음.
        • RDBMS의 entity에서는 value type을 제대로 표현하기 힘들다. (정규화하여 주문정보와 주문자 정보로 분리된 table을 구성하거나 반정규화하여 주문정보에 주문자정보 컬럼을 함께 구성)
  • Value는
    • 불변으로 구현하는 것을 권장!
      • entity의 value type 데이터를 변경할 때 객체 자체를 완전히 교체한다는 것! (변경된 값을 가진 새로운 객체 복사(생성)

Aggregate

  • entity와 value 개수가 많아지만 모델은 점점 복잡해진다.

    • 개발 시 전체 구조가 아닌 한 개 entity와 value에만 집중하게 되는 경우가 발생
    • 상위 수준에서 모델을 관리하기보다 개별 요소에만 초점을 맞추다보면 큰 수준에서 모델을 관리할 수 없는 상황에 빠질수도
  • 도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 바로 Aggregate (에그리거트)

    • 관련 객체를 하나로 묶은 군집
    • 예를 들어, 주문 = 주문, 배송지 정보, 주문자, 주문목록, 총 결제금액 등으로 구성
  • Aggregate는 군집에 속한 객체들을 관리하는 루트 엔티티를 갖는다.
    그림 2.18 Aggregate의 Root인 Order 객체가 하위에 속한 객체를 관리하고 관련 기능을 제공한다.

  • 에그리거트에 속해 있는 엔티티와 벨류 객체를 이용, 구현해야 할 기능을 제공한다.

    • Aggregate를 사용하는 코드는 root가 제공하는 기능을 실행하고 root를 통해 간접적으로 다른 엔티티, 벨류 객체에 접근하게 된다. ⇒ aggregate의 내부 구현을 숨겨 구현을 캡슐화 할 수 있도록 도움
  • 에그리거트 구현 시에는 고려할 부분이 많음.

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

리포지터리

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

    • 이를 위한 도메인 모델이 repository (리포지터리). 즉, 구현을 위한 도메인 모델.
  • 리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.

    • 예를 들어, 주문 애그리거트를 위한 리포지터 리는 다음과 같이 정의할 수 있다.

      public interface OrderRepository {
      		public Order findByNumber(OrderNumber number); 
      		public void save(Order order);
      		public void delete(Order order);
      }
      • 대상을 찾고 저장하는 단위가 애그리거트 루트인 Order 객체
        - Order는 애그리거트에 속한 모든 객체를 포함하고 있으므로 결과적으로 애그리거트 단위로 저장하고 조회
      • 도메인 모델을 사용해야 하는 코드는 리포지터리를 통해서 도메인 객체를 구한 뒤에 도메인 객체의 기능을 실행 (service에서 repository 사용)
      • 도메인 모델 관점에서 OrderRepository는 도메인 객체를 영속화하는데 필요한 기능을 추상화한 것으로 고수준 모듈에 속한다.
      • 기반 기술을 이용해서 OrderRepository를 구현한 클래스는 저수준 모듈로 인프라스트럭처 영역에 속한다.

      그림 2.19 리포지터리 인터페이스와 실제 구현 클래스

  • 응용 서비스와 리포지터리는 밀접한 연관이 있다.

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

    • 가장 기본이 되는 메서드는
      • 애그리거트를 저장하는 메서드
      • 애그리거트 루트 식별자(id)로 애그리거트를 조회하는 메서드
    • 필요에 따라 counts(), delete(id) 등의 메서드를 제공하기도 한다.

요청 처리 흐름

  • 사용자가 애플리케이션에 기능 실행을 요청하면 그 요청을 처음 받는 영역은 표현 영역
    • 스프링 MVC를 사용해서 웹 애플리케이션을 구현했다면 컨트롤러가 사용자의 요청을 받아 처리하게 된다.
  • 표현 영역은 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 데이터를 이용해서 응용 서비스에 기능 실행을 위임한다.
  • 이때 표현 영역은 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달한다.
    그림 2.20 요청 처리 흐름
    • 웹 브라우저를 이용해서 기능 실행을 요청할 경우, HTTP Request parameter를 응용 서비스가 필요로 하는 데이터 로 변환해서 응용 서비스를 실행할 때 파라미터로 전달한다.
    • 응용 서비스는 도메인 모델을 이용해서 기능을 구현한다. 기능 구현에 필요한 도메인 객체를 리포지터 리에서 가져와 실행하거나 신규 도메인 객체를 생성해서 리포지터리에 저장한다. (두 개 이상의 도메인 객체를 사용해서 구현하기도 한다.)
  • 예매 하기, 예매 취소와 같은 기능을 제공하는 응용 서비스는 도메인의 상대를 변경하므로 변경 상태가 물리 저장소에 올바르게 반영되도록 트랜잭션을 관리해야 한다.
    • 스프링 프레임워크를 사용하면 스프링이 제공하는 트랜잭선 관리 기능을 이용해서 트랜잭션을 처리할 수 있다.
      • 선언적 트랜잭션 : @Transactional 또는 <tx:advice>

인프라스트럭처 개요

  • infrastructure는 표현 영역, 응용 영역, 도메인 영역을 지원한다.

    • 도메인 객체의 영속성 처리, 트랜잭션, SMTP 클라이언트, REST 클라이언트 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원
  • DIP를 통해 infrastructure 기능을 직접 사용하는 것 보다 추상화된 인터페이스의 구현 객체를 활용하는 것이 시스템을 더 유연하고 테스트하기 쉽게 만들어준다.

    • 다만, 무조건 infrastructure에 대한 의존을 없애는 것은 좋은 것이 아니다.

    • 예를 들어, 스프링을 사용할 경우 응용 서비스는 트랜잭선 처리를 위해 스프링이 제공하는 @Transactional을 사용하는 것이 편리하다.

    • 영속성 처리를 위해 JPA를 사용할 경우 @Entity 나 @Table 과 같은 JPA 전용 애노테이션(annotation)을 도메인 모델 클래스 에 사용하는 것이 XML 매핑 설정을 이용하는 것보다 편리하다.

      // 구현의 편리함을 위해 인프라스트릭처에 대한 의존을 일부 도메인에 넣은 코드. 
      // JPA의 @Table annotation을 이용해서 엔티티를 저장할 테이블 이름을 지정.
      // XML 설정을 사용하는 것보다 펀리하게 테이블 이름을 지정할 수 있다. 
      
      @Entity
      @Table(name = "TBL_ORDER"public class Order {
      
      }
  • 구현의 편리함은 DIP가 주는 다른 장점(변경의 유연함, 테스트가 쉬움)만큼 중요하기 때문에 DIP의 장점을 해치지 않는 범위에서 응용 영역과 도메인 영역에서 구현 기술에 대한 의존을 가져가는 것이 현명하다.

  • 응용 영역과 도메인 영역이 인프라스트럭처 에 대한 의존을 완전히 갖지 않도록 시도하는 것은 자칫 구현을 더 복잡하고 어렵게 만들 수 있다.

    • 예를 들어, @Transactional을 사용하면 한 줄로 트랜잭션을 처리할 수 있는데 코드에서 스프링에 대한 의존을 없애려면 복잡한 스프링 설정을 시용해야 한다. 의존은 없앴지만 특별히 테스트를 더 쉽게 할 수 있다거나 유연함을 증가시켜주지 못한다. 단지 설정만 복잡해지고 개발 시간만 늘어날 뿐이다.
  • 표현 영역 - 인프라스트럭처 영역

    • Spring MVC - MVC framework에 맞게 표현 영역을 구현
    • Vert.x를 사용한 REST API 서버 구축 - Vert.x에 맞게 웹 요청 처리 부분을 구현

모듈 구성

  • 아키텍처의 각 영역은 별도 패키지에 위치한다.
  • 패키지 구성 규칙에 한 개의 정답만 존재하는 것은 아니다.
    • 하위 도메인별로 모듈 구성
      그림 2.22 도메인이 크면 하위 도메인별로 모듈을 나눠 구성
    • 하위 도메인을 하위 패키지로 구성
      그림 2.23 하위 도메인을 하위 패키지로 구성한 모듈구조
    • 각 애그리거트와 모델과 리포지터리는 같은 패키지에 위치시킨다.
      • com.myshop.order.domain에 위치할 기능(객체)
        • Order, OrderLine, Orderer, OrderRepository 등
    • 도메인이 복잡하면 도메인 모델과 도메인 서비스를 다음과 같이 별도 패키지에 위치시킬 수도 있다.
      • com.rnyshop.orcler.domain.order: 애그리거트 위치
      • com.myshop.order.domain.service: 도메인 서비스 위치
    • 응용 서비스도 다음과 같이 도메인 별로 패키지를 구분할 수 있다.
      • com.myshop.catalog.application.product
      • com.myshop.catalog.application.category
  • 모듈 구조를 얼마나 세분화해야 하는지에 대해 정해진 규칙은 없다. 단지, 한 패키지에 너무 많은 타입이 몰려서 코드를 찾을 때 불편한 정도만 아니면 된다.
    • 책의 저자 개인적으로는 한 패키지에 가능하면 10개 미만으로 타입 개수를 유지하려고 노력한다. 이 개수가 넘어가면 모듈을 분리하는 시도를 해본다고 한다.
profile
그동안 마신 커피와 개발 지식, 경험을 기록하는 공간

0개의 댓글