본문 바로가기

독서

DDD Start! 를 읽고 개념 정리

도메인 모델 시작

도메인

  • 도메인 : 소프트웨어로 해결하고자 하는 문제 영역
  • 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
    • 예를 들어, 온라인 서점 도메인은 다시 주문, 배송 등의 도메인으로 나눌 수 있다.

도메인 모델

  • 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.
  • 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다.
  • 모델의 각 구성요소는 특정 도메인을 한정할 때 비로소 의미가 완전해지기 때문에, 각 하위 도메인마다 별도로 모델을 만들어야 한다. 이는 주문 도메인 모델과 배송 도메인 모델은 따로 만들어야 한다.

도메인 모델 도출

  • 도메인 계층은 도메인읜 핵심 규칙을 구현한다.
    • 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바귀거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.
  • 도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.

엔티티와 밸류

  • 도출한 모델은 크게 엔티티와 밸류로 구분할 수 있다.

엔티티

  • 엔티티의 가장 큰 특징은 식별자를 갖는다는 것이다.
    • 엔티티를 생성하고 엔티티의 속성을 바꾸고 삭제할 때까지 식별자는 유지된다.

엔티티의 식별자 생성

  • 특정 규칙에 따라 생성
  • UUID 사용
  • 값을 직접 입력
  • 일련번호 사용 (시퀀스나 DB의 자동 증가 칼럼 사용)

밸류 타입

  • 밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다.
    • 장점으로 밸류 타입을 위한 기능을 추가할 수 있다.
  • 밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호한다.
    • 이유는 불변 타입을 사용하면 보다 안전한 코드를 작성할 수 있기 때문이다.

엔티티 식별자와 밸류 타입

  • 엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많다.
  • 식별자를 위한 밸류 타입을 만들면 타입 자체로 의미를 명확히 할 수 있다.

도메인 용어

  • 코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요하다. 도메인에서 사용하는 용어를 코드에 반영하지 않으면 그 코드는 개발자에게 코드의 의미를 해석해야 하는 부담을 준다.

 


아키텍처 개요

네 개의 영역

  • 아키텍처를 설계할 때 출현하는 전형적인 영역이 '표현', '응용', '도메인', '인프라스트럭처' 영역이다.
  • 표현 영역은 HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고, 응용 영역의 응답을 HTTP 응답으로 변환해서 전송한다.
  • 응용 영역은 시스템이 사용자에게 제공해야 할 기능을 구현한다.
    • 응용 서비스는 로젝을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다.
  • 도메인 영역은 핵심 로직이 담긴 도메인 모델을 구현한다.
  • 인프라스트럭처 영역은 구현 기술에 대한 것을 다룬다.
    • RDBMS 연동 처리, 메시징 큐에 메시지 전송 또는 수신, SMTP를 이용한 메일 발송 기능을 구현하거나 HTTP 클라이언트를 이용해서 REST API를 호출하는 것도 처리한다.

계층 구조 아키텍처

  • 전체적인 아키텍처는 다음 그림의 계층 구조를 따른다.
  • 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다.
  • 하지만, 구현의 편리함을 위해 계층 구조를 유연하게 적용하기도 한다.

  • 이렇게 인프라 스트럭처에 의존하면 '테스트 어려움' 과 '기능 확장의 어려움' 이라는 두 가지 문제가 발생한다.

DIP

  • DIP는 인터페이스를 이용해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다.
  • DIP를 적용하면 구현 기술을 변경하더라도 클라이언트 코드는 수정할 필요가 없다
    • 단지, 사용할 저수준 구현 객체만 교체해주면 된다.
  • 테스트를 할 때도 대용 객체를 사용해서 진행할 수 있다.
    • 실제 구현 대신, Mock과 같은 대용 객체를 사용한다.

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

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

엔티티와 밸류

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

애그리거트

  • 애그리거트는 관련 객체를 하나로 묶은 군집이다.
    • 예를 들어, 주문이라는 도메인 개념은 '주문', '배송지 정보', '주문자', '주문 목록', '결제 금액' 의 하위 모델로 구성되는데 이때 이 하위 모델들을 하나로 묶어서 '주문'이라는 상위 개념으로 표현할 수 있다.
  • 애그리거트는 군집에 속한 객체들을 관리하는 루트 엔티티를 갖는다.
    • 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다.

리포지터리

  • 도메인 객체를 지속적으로 사용하려면 RDBMS, NoSQL, 로컬 파일과 같은 물리적인 저장소에 도메인 객체를 보관해야 한다.
  • 리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.

 

요청 처리 흐름

  • 표현 영역은 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 데이터를 이용해서 응용 서비스에 기능 실행을 위임한다. 이때, 표현 영역은 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달한다.
  • 응용 서비스는 도메인 모델을 이용해서 기능을 구현한다. 기능 구현에 필요한 도메인 객체를 리포지터리에서 가져와 실행하거나 신규 도메인 객체를 생성해서 리포지터리에 저장한다.

인프라스트럭처 개요

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

 


애그리거트

애그리거트

  • 상위 수준 개념을 이용해서 전체 모델을 정리하면 전반적인 관계를 이해하는 데 도움이 된다.
  • 반대로, 개별 객체 수준에서 모델을 바라보면 상위 수준에서 관계를 파악하기 어렵다.
  • 상위 수준에서 모델이 어떻게 엮여 있는지 알아야 전체 모델을 망가뜨리지 않으면서 추가 요구사항을 모델에 반영할 수 있다.
  • 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들어 상위 수준에서 모델을 조망할 수 있는 방법이 애그리거트이다.
  • 애그리거트는 모델을 이해하는 데 도움을 줄 뿐만 아니라 일관성을 관리하는 기준이 된다.
  • 애그리거트는 경계를 갖는다. 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.

 

애그리거트 루트

  • 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야 한다.
  • 애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 경계를 관리할 주체가 필요한데 이 책임을 지는 것이 바로 애그리거트의 루트 엔티티이다.

 

도메인 규칙과 일관성

  • 애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다.
  • 애그리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안 된다.
    • 이는 애그리거트 루트가 강제하는 규칙을 적용할 수 없어 모델의 일관성이 깨지게 된다.
  • 불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 다음의 두 가지를 습관적으로 적용해야 한다.
    • set 메서드를 public 범위로 만들지 않는다.
    • 밸류 타입은 불변으로 구현한다.

애그리거트 루트의 기능 구현

  • 애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다.
  • 애그리거트 루트는 구성요소의 상태를 참조하거나 기능 실행을 위임한다.

트랜잭션 범위

  • 트랜잭션 범위는 작을수록 좋다.
  • 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다.
  • 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 응용 서비스에서 두 애그리거트를 수정하도록 구현해야 한다.

리포지터리와 애그리거트

  • 애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.

ID를 이용한 애그리거트 참조

  • 필드를 이용해서 다른 애그리거트를 직접 참조하는 것은 개발자에게 구현의 편리함을 제공한다.
  • 하지만, 필드를 이용한 참조는 다음의 문제를 야기할 수 있다.
    • 편한 탐색 오용
    • 성능에 대한 고민
    • 확장 어려움
  • 편한 탐색 오용 : 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 다른 애그리거트의 상태를 수정하고자 하는 유혹에 빠지기 쉽다.
  • 성능에 대한 고민 : JPA를 사용할 경우 참조한 객체를 지연 로딩과 즉시 로딩의 두 가지 방식으로 로딩할 수 있다. 이는 애그리거트가 어떤 기능을 사용하느냐에 따라 로딩 방식이 달라지고, 성능도 달라진다.
  • 확장 어려움 : 트래픽이 늘어서 하위 도메인별로 시스템을 분리할 경우, 더 이상 다른 애그리거트 루트를 참조하기 위해 JPA와 같은 단일 기술을 사용할 수 없음을 의미한다.
  • 이런 세 가지 문제를 완화할 때 사용할 수 있는 것이 ID를 이용한 참조이다.
  • ID 참조를 사용하면 한 애그리거트에 속한 객체끼리만 참조로 연결된다
  • ID를 이용한 참조 방식은 복잡도를 낮추는 것과 함께 애그리거트 별로 다른 구현 기술을 사용하는 것도 가능해진다.

 


응용 서비스와 표현 영역

표현 영역과 응용 영역

  • 표현 영역은 사용자의 요청을 해석한다.
    • URL, 요청 파라미터, 헤더 등을 이용해서 사용자가 어떤 기능을 실행하고 싶어하는지 판별하고 그 기능을 제공하는 응용 서비스를 실행한다.
  • 표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다.
  • 실제 사용자가 원하는 기능을 제공하는 것은 응용 영역에 위치한 서비스이다.

응용 서비스의 역할

  • 응용 서비스는 클라이언트가 요청한 기능을 실행한다.
  • 응용 서비스는 주로 도메인 객체 간의 흐름을 제어하기 때문에 다음과 같은 단순한 형태를 갖는다.
    1. 리포지터리에서 애그리거트를 구한다.
    2. 애그리거트의 도메인 기능을 실행한다.
    3. 결과를 리턴한다.
  • 응용 서비스가 이것보다 복잡하다면 응용 서비스에서 도메인 로직의 일부를 구현하고 있을 가능성이 높다.
  • 응용 서비스의 또다른 주된 역할은 트랜잭션 처리이다.

도메인 로직 넣지 않기

  • 도메인 로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드 품질에 문제가 발생한다.
    • 응집성이 떨어진다. 도메인 로직을 파악하기 어려워진다.
    • 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.

응용 서비스의 구현

응용 서비스의 크기

  • 응용 서비스는 보통 다음의 두 가지 방법 중 한가지 방식으로 구현한다.
    • 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
    • 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기

메서드 파라미터와 값 리턴

  • 응용 서비스에 데이터로 전달할 요청 파라미터가 두 개 이상 존재하면 데이터 전달을 위한 별도 클래스를 사용하는 것이 편리하다.

도메인 이벤트 처리

  • 응용 서비스의 역할 중 하나는 도메인 영역에서 발생시킨 이벤트를 처리하는 것이다.
    • 여기서 이벤트는 도메인에서 발생하는 상태 변경을 의미한다.
  • 도메인에서 이벤트를 발생시키면 응용 서비스는 이벤트를 받아서 알맞은 후처리를 할 수 있다.

표현 영역

  • 표현 영역의 책임은 크게 다음과 같다.
    • 사용자가 시스템을 사용할 수 있는 흐름을 제공하고 제어한다.
    • 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
    • 사용자의 세션을 관리한다.

값 검증

  • 값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다.
  • 표현 영역은 잘못된 값이 존재하면 이를 사용자에게 알려주고 값을 다시 입력받아야 한다.
  • 표현 영역에서 필수 값과 값의 형식을 검사하면 실질적으로 응용 서비스는 아이디 중복 여부와 같은 논리적 오류만 검사하면 된다.
    • 표현 영역 : 필수 값, 값의 형식, 범위 등을 검증한다.
    • 응용 서비스 : 데이터의 존재 유무와 같은 논리적 오류를 검증한다.

조회 전용 기능과 응용 서비스

  • 서비스에서 조회 외에 추가적인 로직이 없다면 굳이 서비스를 만들 필요 없이 표현 영역에서 바로 조회 전용 긴능을 사용해도 된다.

 


도메인 서비스

여러 애그리거트가 필요한 기능

  • 한 애그리거트에 넣기에 애매한 도메인 기능을 특정 애그리거트에서 억지로 구현하면 안 된다.

도메인 서비스

  • 한 애그리거트에 넣기 애매한 도메인 개념을 구현하려면 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내면 된다. 응용 영역의 서비스가 응용 로직을 다룬다면 도메인 서비스는 도메인 로직을 다룬다.
  • 도메인 서비스는 상태 없이 로직만 구현한다. 도메인 서비스를 구현하는 데 필요한 상태는 애그리거트나 다른 방법으로 전달받는다.

 


도메인 모델과 BOUNDED CONTEXT

도메인 모델과 경계

  • 재고 관리에서의 상품, 주문에서의 상품, 배송에서의 상품은 이름만 같지 실제로 의미하는 것은 다르다.
  • 올바른 도메인 모델을 개발하려면 하위 도메인마다 모델을 만들어야 한다.
    • 각 모델은 명시적으로 구분되는 경계를 가져서 섞이지 않도록 해야 한다.

BOUNDED CONTEXT

  • BOUNDED CONTEXT는 모델의 경계를 결정하며 한 개의 BOUNDED CONTEXT는 논리적으로 한 개의 모델을 갖는다.
  • 이상적으로는 하위 도메인과 BOUNDED CONTEXT가 일대일 관계를 가지면 좋겠지만 현실은 다르다.
  • 만약, 여러 하위 도메인을 하나의 BOUNDED CONTEXT에서 개발할 때 주의할 점은 하위 도메인의 모델이 뒤섞이지 않도록 하는 것이다.

BOUNDED CONTEXT 구현

  • BOUNDED CONTEXT는 도메인 모델뿐만 아니라 도메인 기능을 사용자에게 제공하는 데 필요한 표현 영역, 응용 서비스, 인프라 영역 등을 모두 포함한다.
  • 모든 BOUNDED CONTEXT를 반드시 DDD로 개발할 필요는 없다. 복잡한 도메인 로직이 없다면 CRUD방식으로 구현해도 된다.
    • 즉, DAO와 데이터 중심의 밸류 객체를 이용해서 구현해도 문제가 없다.

 


이벤트

시스템 간 강결합의 문제

  • 하나의 요청 로직에서 둘 이상의 BOUNDED CONTEXT가 뒤섞이면 문제가 발생한다.
    • 외부 서비스가 정상이 아닐 경우 트랜잭션 처리가 애매해진다.
    • 외부 시스템의 응답 시간이 길어지면 성능에 문제가 생긴다.
  • 이벤트를 사용하면 이런 강한 결합을 없앨 수 있다.

이벤트 개요

  • 이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다.
  • 이벤트가 발생하면 그 이벤트에 반응하여 원하는 동작을 수행하는 기능을 구현한다.

이벤트 관련 구성요소

  • 도메인 모델에서 이벤트 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체이다. 이들 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생한다.
  • 이벤트 핸들러는 이벤트 생성 주체가 발생한 이벤트에 반응한다.
    • 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다.
  • 이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패처이다.
    • 이벤트 생성주체는 이벤트를 생성해서 디스패처에 이벤트를 전달한다.
    • 이벤트를 전달받은 디스패처는 해당 이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다.

이벤트의 구성

  • 이벤트는 발생한 이벤트에 대한 정보를 담는다.
    • 이벤트 종류 : 클래스 이름으로 이벤트 종류를 표현
    • 이벤트 발생 시간
    • 추가 데이터 : 주문 번호, 신규 배송지 정보 등 이벤트와 관련된 정보
  • 이벤트는 현재 기준으로 과거에 벌어진 것을 표현하기 때문에 이벤트 이름에는 과거 시제를 사용한다.

이벤트 용도

  1. 첫 번째 용도는 트리거이다.
    • 도메인의 상태가 바뀔 때 다른 후처리를 해야 할 경우 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.
  2. 이벤트의 두 번째 용도는 서로 다른 시스템 간의 데이터 동기화이다.

이벤트 장점

  • 이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.
  • 이벤트 핸들러를 사용하면 기능 확장도 용이하다.

 


CQRS

단일 모델의 단점

  • 여러 애그리거트가 연관된 조회를 하면 즉시 로딩이나 지연 로딩의 고민이 생긴다.
  • 이런 고민이 발생하는 이유는 시스템의 상태를 변경할 때와 조회할 때 단일 도메인 모델을 사용하기 때문이다.
  • 복잡도를 낮추는 간단한 방법은 상태 변경을 위한 모델과 조회를 위한 모델을 분리하는 것이다.

 

CQRS

  • 시스템이 제공하는 기능은 크게 상태를 변경하는 기능과 상태 정보를 조회하는 기능이다.
  • 단일 모델로 두 종류의 기능을 구현하면 모델이 불필요하게 복잡해진다.

  • CQRS는 상태를 변경하는 명령을 위한 모델과 상태를 제공하는 조회를 위한 모델을 분리하는 패턴이다.
  • CQRS는 복잡한 도메인에 적합하다.
  • CQRS를 사용하면 각 모델에 맞는 구현 기술을 선택할 수 있다.

  • 단순히 데이터를 읽어와 조회하는 기능은 응용 로직이 복잡하지 않기 때문에 컨트롤러에서 바로 DAO를 실행해도 무방하다.

'독서' 카테고리의 다른 글

'함께 자라기' 를 읽고  (1) 2021.08.12