도메인 주도 설계 책 리뷰
Book Review

도메인 주도 설계 책 리뷰

반응형

 

다층구조 (Layered Architecture)

기본 개념

  • 매우 복잡한 작업을 처리하는 소프트웨어를 만들경우 관심사의 분리가 필요하며, 이로써 격리된 상태에 있는 각 설계 요소에 집중할 수 있다.
  • 대표적인 모델인 Layered Architecture를 통해서 같은 계층의 모든 요소는 오직 같은 계층에 존재하는 다른 요소나 계층상 아래에 위치한 요소에만 의존한다는 매커니즘을 가져야한다. (상위 계층과는 결합도를 느슨하게 유지해야한다.)
  • 다층 구조를 통해서 응집력이 있는 설계가 가능해지며 설계의 이해가 쉬우지고 이는 어너정도 관습화 되어 널리 사용되고 있다. 계층별 설명은 아래와 같다.
    • Presentation Layer : 사용자에게 정보를 보여주고 해석하는 책임을 가진 Layer
    • Application Layer :  실제로 작업하는 코드가 존재하며 도메인 객체가 문제를 해결하게 한다. 이곳은 최대한 얇게 유지되어야 하며 도메인 관련 내용 (업무 규칙, 지식)등에 대한 로직을 가지면 안되고 오직 작업을 조정하고 도메인 Layer에 작업을 위임해야 한다.
    • DomainLayer :  업무내용의 핵심 내용을 담고 있으며 업무 상황에 관한 정보, 규칙등을 표현하는 책임을 진다.
    • Infrastructure Layer : 상위 계층을 지원하는 기술적 기능을 제공
      • 바람직한 아키텍처 프레임워크 (J2EE, Spring..)는 도메인 개발자가 모델을 표현하는데 집중하게 해서 복잡한 기술적 난제를 해결하게 한다.
      • 도메인 계층은 모델의 개념을 반영하는 곳으로 다른 관심사와 섞여있지 않게 하는 것이 중요하다.

 

 

연관관계

  • 연관관계를 조금 쉽게 다룰수 있는 방법은 3가지가 존재한다.
    • 탐색방향을 부여한다.
      • 양방향 연관관계의 경우 두 객체가 모두 있어야만 이해가 가능하기 때문에 두 방향을 모두 탐색해야하지 않는다면 한쪽 방향으로 설계를 하여 상호 의존성을 줄이고 단순화 시킨다.
      • 물론 두 객체 사이에 연관성이 설계의 목적과 부합하지 않는다고 한다면 아예 없애는 것이 더 좋다.
    • 한정자 (qulifier)를 추가하여 사실상 다중성을 줄인다.
    • 중요하지 않는 연관관계를 제거한다.
  • 연관관계 관련해서는 우아한 형제들 세미나가 제일 좋았다. 

 

 

Entity

  • entity는 자신의 생명주기동안 형태와 내용이 급격하게 바뀔수도 있지만 연속성을 유지해야한다. 또한 entity를 추적하기 위해서 entity만에 고유 식별성이 정의되어 있어야 한다.
    • 계좌에 두 번의 은행업무를 했을 경우 발생한 두 개의 은행 업무 기록은 각각 개별의 entity이지만 두 개는 같은 속성을 가진 인스턴스 객체이다.
  • 식별성은 객체의 속성으로 일치 여부를 판단하는 것이 아닌 객체를 구별할 수 있는 수단이다.
  • entity는 개념에 필수적인 행위만 하고 그 행위에 필요한 속성만 추가한다. 그 밖의 개체는 행위와 속성을 검토해서 가장 중심이 되는 entity와 연관관계에 있는 다를 객체로 옮긴다.
  • 모델에 특정 요소에 관심이 있다면 value object로 분류하고 이는 immutable하게 다루며 value object에는 아무런 식별성을 부여하지 말고 entity를 유지하는데 필요한 설계상에 복잡도를 피하라.
  • value object는 연관관계를 가질 수 없다.
  • 정상적인 service의 경우 entity와 value object의 일부를 구성하는 것이 아니라 도메인 개념과 관련되어 있으며 도메인 모델의 외적 요소의 측면에서 정의되며 도메인의 상태값을 가지지 않는다.
  • 도메인 계층에서의 service와 다른 계층에서의 service를 구분해야한다.

 

 

Module

  • 모듈은 패키지라고 하며 각 모듈간의 결합도가 낮아야하고 모듈 내부는 응집도가 높아야 한다. 또한 모듈을 나누는 조건은 코드가 아닌 개념으로 나눈다.

 

 

Aggregate (집합체)

  • Aggregate는 우리가 데이터 변경의 단위로 다루는 연관 객체의 묶음을 말한다.
  • Aggregate에는 root와 boundary가 있는데 root는 단 하나만 존재하며 Aggregate에 포함된 특정 Entity를 가리킨다. 그리고 boundary는 aggregate에 포함되고 포함되지 않는 정의를 의미한다.
  • boundary안에서 서로 참조할 수 있지만 경계 바깥의 객체는 해당 Aggregate의 구성 요소 가운데 루트만 참조 할 수 있다. root이외에 모두 지역 식별성을 가지며 Aggregate내부에서만 사용된다. 즉 외부에서는 Aggregate의 root를 제외하고는 볼 수 없다.
  • Aggregate를 구현하려면 모든 트랜잭션에 적용되는 규칙
    • root entity는 전역 식별성을 지니며 궁극적으로 불변식을 체크할 책임을 가진다.
    • 각 root entity는 전역 식별성과 지역 식별성 두 가지 속성을 가진다.
    • Aggregate내에 루트 엔티티를 제외하고는 다른 객체 정보를 접근할 수 없으며 필요할 때는 root Aggregate를 통해서 value object를 통해 값을 복사해서 사용할 수 있다.
    • Aggregate 경계안에 모든 요소를 한번에 제거해야한다.

 

 

Factory

  • 복잡한 객체와 Aggregate의 인스턴스를 생성하는 책임을 별도의 객체로 옮겨서 실제 생성해야하는 인스턴스의 복잡한 부분을 외부에 공개하지 말고 인터페이스만 제공하여 Aggregate를 하나의 단위로 생성해서 그것의 불변식을 이행되게 하자.
  • 빌더와 추상 팩터리 패턴등을 통해서 생성할 수 있으며 각 생성방식은 원자적이어야 한다.

 

 

Repository

  • 데이터베이스 질의를 통해 필요한 데이터를 별도로 지정해놓은 Aggregate 내부에 루트 엔티티를 거치지 않고 추출해서 사용한다면 지금까지 정의했던 Aggregate가 모두 모호해지고 실제로 도메인 객체와 Aggregate의 캡슐화를 어길수도 있다.
  • 전역 인터페이스 (ex. JpaRepository)를 토대로 접근방법을 마련하여 객체를 추가하고, 제거하는 메소드등을 제공하지만 실제로 연산과정은 캡슐화 해야한다. 도메인에 대하여 데이터 접근이 필요한 경우에는 지정된 Aggregate내의 root entity만을 가져갈수 있도록 repository에서 제공하고 모든 객체 저장과 접근은 repository에서 제공하도록 위임하여 실제 클라이언트는 로직에 집중하도록 한다.
  • repository가 있으면 데이터베이스에 대한 전략과 여러 datasource로 부터 애플리케이션과 도메인을 분리할 수 있으며 repository 영역을 쉽게 mocking하여 테스트시 대체하기 쉬워진다.
  • repository의 타입은 인터페이스마다 지정해주거나 특정 타입의 상위 클래스를 넣을 수 있다. 또한 클라이언트와 분리를 활용하여 repository를 유연하게 사용하여 캐싱 기능도 추가할 수 있으며 트랜잭션 제어를 repository가 아닌 클라이언트 단에서 접근하여 사용할 수 있다.
  • respository에서 데이터를 추출하고 실제 사용될 데이터를 factory에서 재구성하여서 데이터베이스 내부에 실제 데이터를 캡슐화 할 수 있다.

 

 

유연한 설계

  • 의도를 드러내는 인터페이스 (Intention revealing interface)
    • 컴포넌트 사용 시 컴포넌트의 구현 세부사항을 고려해야한다면 캡슐화로써 가치가 사라진다.
    • 인터페이스로 제공한 정보를 토대로 추측한 내용이 원래의 취지에 어긋난다면 이는 서로 다른 개발자들에게 문제를 야기한다.
  • 부수효과가 없는 함수 (side effect free fuction)
    • 특정 function을 사용할 때 해당 기능에 구현과 관련된 세부사항에 대해 함꼐 이해해야 한다면 인터페이스 추상화로 얻을 수 있는 유용성이 부족해지며 잘못된 예측으로 인해 side effect가 발생할 수 있다.
    • 이를 해결하기 위해서는 여러 연산을 한번에 묶지 않고 최대한 쪼개서 기능을 단순하게 사용한다.
    • 연산에 들어간 input 객체를 변경하지 않고 연산의 결과로써 새로운 value object를 반화하게 하여 부수효과(side effect)에서 기존 데이터를 격리 시킨다.
    • 실제로 잘못만들어지고 오해를 줄 수 있는 기능은 프로그램에서 많은 문제를 야기하는 부분이다.
  • Assertion
    • 연산의 결과에 대해 Assertion을 테스트 코드에서 사용하여 사용되는 연산에 대해 추측 가능하게 명시하라.
    • 개발자들이 의도된 Assertion을 추측할 수 있게 인도하고 쉽게 배울 수 있고 모순된 코드를 작성하는 위험을 줄이는 응집도 높은 개념이 포함된 모듈을 만들도록 노력하라
  • 개념적 윤곽 (Conceptual contour)
    • 서로 다른 개념이 섞여 있을 경우 파악하기 어렵다.
    • 도메인을 중요 영역을 나누는 것과 관련한 직관을 감안해서 설계요소를 응집력 있는 요소로 나누고 계속적인 리팰토링을 통해 변경되는 부분과 변경되지 않는 부분에 대해 나누고 변경을 분리하기 위한 정확한 개념적 윤곽을 찾아라
    • 목표는 혼란과 유지모수 부담이 없는 단순한 인터페이스 집합을 얻는 것이다.
  • 독립형 클래스 (Standalone class)
    • 모듈 (패키지)내에 의존성이 증가할수록 설계를 파악하는데 따르는 어려움이 가파르게 높아진다. 그렇기에 낮은 결합도는 객체 설계의 기본원리이다. 가능한 늘 결합도를 낮추도록 노력하고 현재 객체와 무관한 모든 개념을 제거하라. 그럼 완전한 독립된 클래스가 나올 것이고 이는 module을 이해하는데 부담을 줄여준다.
  • 연산의 닫힘 (Closure of operation)
    • 수학에서 항상 참인 부분을 가리키면서 닫혀 있다는 표현을 사용한다.
    • 적절한 위치에 반환타입과 인자 타입이 동일한 연산을 정의하라. 이런 방식으로 정의된 연산은 해당 타입의 인스턴스 집합에 닫혀있어 부차적인 개념을 사용하지 않고도 고수준의 인터페이스를 제공할 수 있다.
  • 선언적 설계
    • 실행 가능한 명세로서 프로그램 전체 혹은 프로그램의 일부를 작성하는 방식을 의미
    • 특성을 매우 정확하게 기술함으로써 소프트웨어를 제어하는 것 
    • 예를 들어 논리 연산에 대한 아래 예를 살펴보면 필요한 기술에 대해서 미리 선언을 해서 사용하는 방식인데 이는 보는 관점에 따라 비효율적일 수 있지만 상황에 따라서는 이런 방식이 복잡도가 낮고 더 빠르게 개발을 할 수 있게 도움을 준다.
public abstract class AbstractSpecification implements Specification {

    @Override
    public Specification and(Specification other) {
        return null;
    }

    @Override
    public Specification or(Specification other) {
        return null;
    }

    @Override
    public Specification not() {
        return null;
    }
}


public class AndSpecification extends AbstractSpecification {

    Specification one;
    Specification other;

    public AndSpecification(Specification one, Specification other) {
        this.one = one;
        this.other = other;
    }

    @Override
    public boolean isSatisfiedBy(Object candidate) {
        return one.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
    }
}

public class NotSpecification extends AbstractSpecification {

    Specification wrapped;

    public NotSpecification(Specification wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public boolean isSatisfiedBy(Object candidate) {
        return !wrapped.isSatisfiedBy(candidate);
    }
}

public class OrSpecification extends AbstractSpecification {

    Specification one;;
    Specification other;

    public OrSpecification(Specification one, Specification other) {
        this.one = one;
        this.other = other;
    }

    @Override
    public boolean isSatisfiedBy(Object candidate) {
        return one.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate);
    }
}

 

 

 

 

출처 : 도메인 주도 설계 소프트웨어의 복잡성을 다루는 지혜

 

반응형