(ing...)DDD start! 최범균

2019-01-30·programming

Table of Contents

도메인 모델 시작

  • '온라인 서점'은 소프트웨어로 해결하고자 하는 문제 영역, 즉 도메인에 해당된다.

  • 한 도메인은 다시 하위 도메인으로 나눌 수 있다. 예를들어 온라인 서점 도메인은 주문, 결제, 혜택, 회원 등 하위 도메인으로 나눌 수 있다.

  • 도메인 모델에는 다양한 정의가 존재하지만, 기본적으로 개념적인 표현한 것을 칭한다.

  • 개념 모델을 이용하여 바로 코드를 작성할 수 있는 것은 아니기에 구현 기술에 맞는 구현 모델이 따로 필요하다.

  • 일반적으로 아키텍쳐는 표현 - 응용 - 도메인 - 인프라 4개의 계층을 띈다. 응용 계층은 도메인 계층을 조합하여 기능을 실행하고, 도메인은 규칙을 구현한다. 인프라스트럭쳐는 데이터베이스나 메시징 시스템과 같은 연동을 담당한다.

  • 도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않은 버릇이다. 특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.

  • 도메인에서 사용하는 용어는 매웆 중요하다. 적당한 단어를 찾는 노력을 하지 않고 도메인에 어울리지 않는 단어를 사용하면 코드는 도메인과 점점 멀어지게 된다. 도메인에 알맞는 단어를 찾는 시간을 아까워하지 말자.

아키텍처 개요

  • 일반적으로 아키텍쳐는 표현 - 응용 - 도메인 - 인프라 4개의 계층을 띈다.

  • 도메인은 엔티티와 값(가격, 배송지 등)을 포함하는 개념이며, 응용 서비스는 로직을 집적 수행하기 보다는 도메인에 수행을 위임한다.

  • 인프라스트럭쳐 영역은 구현 기술에 대한 것을 다룬다. 데이터베이스, 메시징 큐 등 연동을 담당한다. 논리적 개념보다는 실제 구현을 다룬다.

  • 표현 -> 응용 -> 도메인 -> 인프라스트럭쳐 계층을 엄격하게 가져가면 상위 계층은 바로 아래의 계층만을 의존해야 하지만, 구현의 편리함을 위해 구조를 유연하게 적용한다. 가령 도메인 가격 계산 로직이 복잡해지면 도메인의 객체 지향으로 로직을 구현하는 것 보다 응용 계층에서 바로 인프라 계층을 의존할 수 있다.

  • 인프라스트럭처에 의존하면 '테스트 어려움'과 '기능 확장의 어려움'이라는 두가제 문제가 발생한다. 이를 해결하기 위해 DIP(Dependency Inversion Principle)를 적용하는 방법이 있다.

    • 가령 금액을 계산하는 고수준 서비스가 있고, 이것이 인프라 스트럭쳐에 의존하지 않게 한다면 저수준 구현을 인터페이스로 고수준으로 끌어 올리고, 서비스는 인터페이스를 의존하게 하면 된다. 따라서 저수준의 구현체가 고수준의 인터페이스를 의존하게 된다. 의존이 역전된 것이다.

    • 이렇게 적용하면 구현 교체가 어렵다는 문제와 테스트가 어려운 문제를 해소할 수 있다. 서비스 테스트 코드에 인터페이스 대용 객체(Mock)을 사용해서 테스트를 진행할 수 있다.

    • DIP 를 잘못 생각하면 단순히 인터페이스와 구현 클레스를 분리하는 정도로 받아드릴 수 있다. DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않기 위함인데 DIP를 적용한 결과 구조만 보고, 인프라 레이어 안에서 저수준으로 인터페이스를 추출하는 경우가 있다. 이런 경우 서비스레이어에서 저수준의 인터페이스를 의존하게 되며 DIP 를 만족하지 못하다. DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 관점에서 도출한다.

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

    • 엔티티: 고유 식별자를 갖는 객체. 도메인 모델의 데이터와 관련된 기능을 함께 제공한다.
    • 벨류: 고유 식별자를 갖지 않는 객체. 가령 배송지 주소를 표현하는 객체가 있을 수 있다.
    • 애그리거트: 관련된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다. 예를들어 주문과 관련된 Order. OrderLine(상품옵션),Orderer 등을 주문 애그리거트로 묶을 수 있다.
    • 도메인 서비스: 특정 엔티티에 속하지 않는 도메인 로직을 제공한다. '할인 금액 계산'은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하게 되는데, 이렇게 도메인 로직이 여러 엔티티와 벨류를 필요할 경우 도메인 서비스에서 로직을 구현한다.
  • 엔티티 & 벨류

    • 디비 테이블의 엔티티와 도메인 모델의 엔티티는 동일하지 않다. 가령 주문을 표현하는 엔티티는 주문과 관련된 데이터뿐만 아니라 배송지 주소 변경을 위한 기능을 함께 제공한다.

    • 도메인 모델의 엔티티는 벨류타입도 속한다. 가령 주문자를 표현한다면 사실 디비에는 name 과 email 컬럼이겠지만 이것을 Orderer 라는 값타입 객체로 표현할 수 있다.

  • 에그리거트

    • 도메인이 커질수록 개발할 도메인 모델도 커디면서 많은 엔티티와 밸류가 출현하고 연관되면서 모델은 점점 더 복잡해진다.
    • 에그리거트는 관련 객체를 하나로 묶은 군집이다.
    • 에그리거트는 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다. 루트 엔티티는 에거리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다.
  • 리포지토리

    • 도메인 객체를 지속적으로 사용하려면 물리적인 저장소에 도메인 객체를 보관해야 한다.
    • 엔티티가 벨류가 요구사항에서 도출되는 도메인 모델 이라면 레포지터리는 구현을 위한 도메인 모델이다.
    • 도메인 모델 관점에서 repository는 객체를 영속화하는 데 필요한 기능을 추상화 한 것으로 고수준 모듈에 속한다.
  • 인프라스트럭쳐

    • 구현의 편리함은 DIP가 주는 장점(변경 유연함 + 테스트하기 쉬움)만큼 중요하기 때문에 DIP의 장점을 해치지 않는 범위에서 응용 영역과 도메인 영역에서 구현 기술에 대한 의존을 가져가는 것이 현명하다.
  • 모듈 구성

    • 도메인이 크다면 하위 도메인으로 패키지를 구성한다. (order, catalog, member 등)
    • 패키지에 가능하면 10개 미만으로 타입 개수를 유지하려고 노력한다. 개수가 넘어가면 모듈을 분리하는 시도를 해본다.

애그리거트

  • 애그리거트는 관련 모델을 개념적으로 묶은 것이다. 애그러그트에 속한 객체는 유사하거나 동일한 라이프사이클을 갖는다.

  • 'A가 B를 갖는다' 관계일지여도 이것이 반드시 A 와 B가 하나의 애그리거트에 속한다는 것을 의미하는 것은 아니다. 좋은 예가 상품과 리뷰다.

  • 도메인 규칙을 제대로 이해할 수록 실제 애그리거트의 크기는 줄어들게 된다. 경험을 비추어 보면 다수의 애그리거트가 한 개의 엔티티 객체만 갖는 경우가 많으며 두 개 이상의 엔티티로 구성되는 애그리거트는 드물게 존재한다.

  • 공개 set 메서드는 중요 도메인이나 의미나 의도를 표현하지 못하고 도메인 로직이 도메인 객체가 아닌 응용 영역이나 표현 형역으로 반산되게 만드는 원인이 된다. 가령 setter 를 상용하지 않으면 자연스레 changePassword, cancel 등 의미가 더 잘 드러나는 이름을 사용하는 빈도가 높아진다.

  • 트랜잭션 범위는 작을수록 좋다. 충돌을 막기 위해 잠그는 대상이 여러개일 수록 데이터베이스 동시성이 줄어든다. 한 트랜잭션에서 한 애그리거트만 수정한다는 것은 애그리거트에서 다른 애그리거트를 수정하지 않는 것을 뜻한다.

    public Class Order {
        // BAD!
        /**
        한 애그리거트 내부에서 다른 애그리거트를 변경하는 기능을 실행하면 안된다.
        자신의 책임 범위를 넘는다!
        이런 경우 응용 서비스에서 두 애그리거트를 수정하도록 구현해야 한다.
        **/
        order.getCustumer().changeAddress(shipppingInfo.getAddress());
    }
    
  • Order 와 OrderLine 을 물리적으로 각각 별도의 DB 테이블에 저장한다고 해서 각각을 위한 repository 를 만들지 않는다. 한번에 애그리거트 전체를 영속화해야한다.

  • ORM덕분에 애그리거트 루트에 대한 참조를 쉽게 구현할 수 있다. 이 때 다음과 같은 문제를 고려해야 한다

    • 편한 탐색 오용 : 책임을 넘어 다른 애그리거트를 수정하고자 하는 유혹에 빠지기 쉽다. (order.getMember().setXXXX())
    • 성능에 대한 고민 : lazy/eager fetch 전략을 고민해야한다.
    • 확장 어려움 : 단일 DBMS 로 제공하다 도메인별로 시스템을 분리하기 시작할 때, 애그리거트 루트간 참조가 많으면 분리에 비용이 발생한다.
  • 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면, 애그리거트에 팩토리 메서드를 구현하는 것을 고려해보자.