도메인 주도 설계 (1) - 도메인 모델

gentledot·2021년 8월 1일
3

도메인 주도 설계를 정리하는 주된 이유는 구현해야 할 문제, 즉 도메인(domain)을 어떻게 구현해야 지속가능한 application으로 개발할 수 있는지에 대한 답을 찾고자 함입니다.

이번 내용을 정리하면서 아래의 질문에 대한 답을 찾을 수 있으면 좋겠습니다.

  • 도메인(domain)에 대한 이해와 도메인을 객체로 구현하려면?
  • 엔티티(entity)는 어떻게 구현해야 하는지?
  • entity와 RDB와의 Mapping에 대한 내용, 그리고 JPA에 대해
  • domain 영역의 구성요소는 어떤 것들이 있는지?
  • 객체 지향 기법과 도메인 설계 간 상관관계는? (캡슐화, 추상화, 다형성 등)

매 주 일요일에 포스트를 작성, 내용을 추가할 생각입니다.

도메인 모델

도메인

  • 온라인 서점을 예로 들면
    1. 개발자 입장에서 온라인 서점은 구현해야 할 소프트웨어의 대상이 된다.
      • '온라인 서점' = 소프트웨어로 해결하고자 하는 문제의 영역 = 도메인 (domain)
    2. 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
      • 도메인 구성
        • 주문 (핵심 기능)
        • 회원
        • 혜택
        • 결제
        • 배송
        • 정산
        • 카탈로그
        • 리뷰
      • 한 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
        • ex) 고객 → 물건 주문 → 금액 결제 → 물건 배송 → 혜택 제공
    3. 특정 도메인을 위한 소프트웨어라 해서 도메인이 제공해야 할 모든 기능을 구현하는 것은 아니다.
      • 결제는 외부 PG를 가져다 연동한다거나
      • 배송은 외부 물류 업체와 연계하여 배송 체계를 연동한다거나
    4. 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다.
      • B2B, B2C

도메인 모델

  • 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것
    • 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는데 도움이 된다.
  • 도메인을 이해하려면 다음을 파악해야하는데
    • 도메인이 제공하는 기능
    • 도메인의 주요 데이터 구성
  • 기능과 데이터를 함께 보여주는 객체 모델은 도메인을 모델링하기 적합.
  • 도메인 모델을 객체로만 모델링할 수 있는 것이 아니다.
    • 도메인을 이해하는 데 도움이 된다면 표현 방식이 무엇인지는 중요하지 않다.
  • 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이다.
    • 개념 모델로 바로 코드를 작성할 수 있는 것은 아니기에 구현 기술에 맞는 구현 모델이 따로 필요하다.
    • 객체 기반 모델을 이용해 도메인을 표현했다면 객체 지향 언어를 이용해 개념 모델에 가깝게 구현 모델을 구현할 수 있다.
  • 각 하위 도메인마다 별도로 모델을 만들어야 한다.

도메인 모델 패턴

  • 일반적인 application architecture는 다음과 같이 4개의 계층으로 구성된다.
    • DB →
      • Infrastructure
      • Domain (도메인)
      • Application (응용)
      • Presentation / UI (표현 또는 사용자 인터페이스)
    • 사용자 ←
  • 도메인 모델 패턴 (엔터프라이즈 애플리케이션 아키텍처 패턴 (PEAA), 마틴 파울러)
    • architecture 상의 domain 계층을 객체 지향 기법으로 구현하는 패턴
    • 도메인 계층의 객체 모델 : 도메인 모델
  • 도메인과 관련된 중요 업무 규칙은 도메인 모델에서 구현한다.
    • 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.

개념 모델 / 구현모델

  • 개념 모델은 순수하게 문제를 분석한 결과물
    • 처음부터 완벽하게 도메인을 표현하는 것은 불가능에 가깝다.
    • 개발하면서 도메인에 대한 새로운 지식이 쌓이게 되고, 모델을 보완하거나 수정하는 일이 발생할 수 밖에 없다.
  • 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다.
    • 초반은 도메인에 대한 전체 윤곽을 이해하는 데 집중
    • 구현 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야 한다.

도메인 모델 도출

  • 도메인에 대한 이해 없이 코딩을 시작할 순 없다.
  • 도메인을 모델링 할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.
  • 도메인 구현을 위해 요구사항을 정렬이 필요한데, 도메인 전문가나 다른 개발자와의 논의 과정으로 공유되어야 하기 때문에 문서화를 통해 쉽게 접근할 수 있도록 하면 좋다.

도메인 모델링의 예 - 주문 도메인과 관련된 요구사항

  • 주문 요구사항

    • 최소 한 종류 이상 상품을 주문해야
    • 한 상품을 한 개 이상 주문할 수 있어야
    • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액
    • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값
    • 주문할 때 배송지 정보를 반드시 지정해야 한다.
    • 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성
    • 출고를 하면 배송지 정보를 변경할 수 없다.
    • 출고 전에 주문을 취소할 수 있다.
    • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
  • 요구사항에서 알 수 있는 것은 다음의 기능을 제공한다는 것.

    • 출고 상태로 변경하기
    • 배송지 정보 변경하기
    • 주문 취소하기
    • 결제 완료로 변경하기
  • 주문 항목이 어떤 데이터로 구성되어야 하는지는 아래 요구사항에서 확인된다.

    - 한 상품을 한 개 이상 주문할 수 있어야
    - 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값
    • 주문 항목을 표현하는 도메인 객체는 적어도 주문할 상품, 상품의 가격, 구매 개수, 각 구매 항목의 구매 가격 등을 포함하고 있어야 한다.
  • 주문 - 주문항목 간의 관계는 다음의 요구사항에서 확인된다.

    - 최소 한 종류 이상 상품을 주문해야
    - 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액
    • 주문 도메인 객체는 최소 한 개 이상의 주문항목 객체를 포함해야 한다.
    • 그리고 주문항목 객체로부터 총 주문 금액을 구할 수 있다.
  • 배송지 정보는 받는 사람 이름, 전화번호, 주소 데이터를 가짐.

    - 주문할 때 배송지 정보를 반드시 지정해야 한다.
    • 주문 객체 생성 시 주문항목 객체의 목록 뿐 아니라 배송지 정보 객체도 함께 전달해야 한다는 것이 확인된다.
    • 그리고 주문 시 배송지 정보는 필수로 입력되어야 한다는 '배송지 정보 필수' 도메인 규칙을 구현해야 한다.

문서화

  • 문서화의 주된 이유는 지식을 공유하기 위함이다.
  • 코드를 이용해 전체 소프트웨어를 분석하려면 많은 시간을 투자해야 한다.
    • 전반적인 기능 목록, 모듈 구조, 빌드 과정 등은 코드를 보고 직접 이해하는 것보다 상위 수준에서 정리한 문서를 참조하는 것이 이해에 용이하다.
  • 코드를 작성할 때도 도메인 지식이 잘 묻어나도록 코드를 작성해야 한다.
    • 그렇지 않은 코드는 동작 과정을 해석할 순 있어도, 도메인 관점에서 코드를 그렇게 작성했는지 이해하기 난해해진다.
    • 보기 좋게 작성 + 코드가 도메인을 잘 표현하도록 작성 = 코드의 가독성 향상, 문서로서 코드가 의미를 가짐

Entity와 Value

  • 도출한 도메인 모델은 크게 entity와 value로 구분할 수 있다.
  • entity와 value를 제대로 구분해야 domain을 올바르게 설계하고 구현할 수 있음.

Entity

  • 가장 큰 특징은 식별자를 갖는다는 것.

    • 각 entity는 서로 다른 식별자를 갖는다.
  • entity 객체의 식별자는 생성, 속성 변경, 삭제의 과정까지 유지된다.

  • 비교하는 두 entity 객체의 식별자가 같으면 두 객체는 같다고 판단할 수 있다.

  • 식별자를 이용해서 equals()hashCode() 를 구현할 수 있다.

    public class OrderNo  {
        private String number;
    
        private OrderNo() {
        }
    
        public OrderNo(String number) {
            this.number = number;
        }
    
        public String getNumber() {
            return number;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            OrderNo orderNo = (OrderNo) o;
            return Objects.equals(number, orderNo.number);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(number);
        }
    }

entity의 식별자 생성

  • 식별자를 생성하는 시점은 domain의 특징과 사용하는 기술에 따라 달라진다.
  • 식별자는 다음 중 하나의 방식으로 생성한다.
    • 특정 규칙에 따라 생성
      • ex) 주문번호, 운송장번호, 카드번호
      • 흔히 사용되는 규칙은 현재 시간과 다른 값을 함께 조합하는 것 (같은 식별자를 만들지 않도록)
    • UUID (Universally Unique Identifier) 사용
    • 값을 직접 입력
      • 사용자가 직접 입력하는 ID, Email 주소 등
      • 직접 입력 값이므로 중복 입력이 되지 않도록 사전에 방지하는 것이 중요.
    • 일련번호 사용 (sequence 또는 DB의 Auto increment column 사용)
      • 주로 DB가 제공하는 자동 증가 기능을 사용한다.
        • oracle은 sequence 사용
        • mySQL은 auto_increment 컬럼 사용
        • entity 객체를 생성할 때 식별자를 전달할 수는 없음.

value type

  • value type은 개념적으로 완전한 하나를 표현할 때 사용한다.

    • 받는 사람(이름, 전화번호)는 receiver로 구현 가능하고, 수령지(주소1, 주소2, 우편번호)는 address로 구현 가능하다.

      public ShippingInfo(Address address, String message, Receiver receiver) {
            this.address = address;
            this.message = message;
            this.receiver = receiver;
        }
      
      ...
      
      public Address(String address1, String address2, String zipcode) {
        ...
      }
    • 필드의 명칭을 통해 객체를 유추하지 않고 valueType을 사용함으로써 객체를 완전한 개념으로 표현할 수 있음.

  • 개념적으로 완전한 하나의 표현이 둘 이상의 필드(데이터)를 가져야 한다는 의미는 아니다.

    • int type인 amount, price 등은 money 객체로 설정할 수 있음.
      public class Money {
      	private int value;
          public Money(int value) {
          	this.money = money;
          }
          
          public int getValue() {
          	return this.value;
          }
      }
      • Money price, Money amounts로 명확한 개념으로 표현이 가능.
  • value type을 사용하면 개념과 연관된 기능을 객체에 구현할 수 있음.

    • Money.add(), Money.multiply()
    • 이러한 기능을 통해 int(정수) 연산이 아닌 돈 계산 이라는 의미로 명확히 작성할 수 있음. (가독성 향상!)
  • value type의 데이터 변경 (setter) 은 field 값을 변경하는 것이 아니라 새로운 value 객체를 생성하는 방식으로 구현하는 것이 좋다. (불변(immutable)로 구현)

    • 불변으로 구현하는 가장 중요한 이유 는 불변 타입을 사용하면 보다 안전한 코드를 작성할 수 있다는 것!
    public class Money { 
    		private int value;
    		public Money add(Money money) { 
    			return new Money(this.value + money.value); 
    		}
    		// value를 변경할 수 있는 메시드 없음
    }
  • entity type의 두 객체가 같은지 비교할 때 ⇒ 식별자를 사용

  • 두 value 객체가 같은지 비교할 때는 모든 속성이 같은지 비교해야 한다.

    • equals(), hashCode() 가 구성되어야 한다.

          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null || getClass() != o.getClass()) return false;
              Receiver receiver = (Receiver) o;
              return Objects.equals(name, receiver.name) && Objects.equals(phone, receiver.phone);
          }
      
          @Override
          public int hashCode() {
              return Objects.hash(name, phone);
          }

Entity 식별자와 value type

  • entity 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많다.
    • 신용카드 16개 숫자
    • 이메일 주소를 ID로 사용하는 경우 등
  • 해당 식별자를 사용할 때 Data type보다는 value type을 사용한다면 field의 명칭 뿐 아니라 type으로도 실제 의미를 찾기 유용해진다.

domain model 내 setter (set method)를 넣지 않기

  • getter, setter method를 무조건 추가하는 것은 좋지 않은 버릇이다!

    • 특히 setter는 domain의 핵심 개념이나 의도를 코드에서 사라지게 한다.
    • ex)
      • changeShippingInfo() : 배송지 정보를 새로 변경하는 의미로 이해할 수 있다
      • setShippingInfo() : 배송지 값을 설정한다는 의미로 받아들여진다.
  • 도메인 지식을 코드로 구현하는 것이 자연스럽다.

    • setter로의 구현이 단순히 상대 값만 변경할지 아니면 상태 값에 따라 다른 처리를 위한 코드를 함께 구현할지 애매해진다.
    • setter로 구현하면 상태 변경과 관련된 도메인 지식이 코드에서 사라지게 된다.
  • domain 객체를 생성할 때 완전한 상태가 아닐 수 있는 상태에서 사용될 수 있는 문제

    // set 메시드로 데이터를 전달하도록 구현하면 
    // 처음 Order를 생성하는 시점에 order는 완전하지 않다. 
    Order order = new Order();
    
    // set 메서드로 필요한 모든 값을 전달해야 함 
    order.setOrderLine(lines); 
    order.setShippingInfo(shippingInfo);
    
    // 주문자(Orderer)를 실징하지 않은 상태에서 주문 완료 처리
    order.setState(OrderState.PREPARING);
    
    // 도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다.
    // 생성자를 통해 필요한 데이터를 모두 받도록 한다.
    Order order = new Order(orderer, lines, shippingInfo, OrderStarte.PREPARING);
  • setter를 private 지시자로 설정하여 constructor 에서 활용할 수 있음.

    • private이기 때문에 외부에서 데이터를 변경할 목적으로 setter를 사용할 수 없음.
  • setter method를 구현해야 할 특별한 이유가 없다면 불변 타입의 장점을 살릴 수 있도록 value type은 불변으로 구현한다.

DTO의 getter와 setter

  • DTO(Data Transfer Object) : 프레젠테이션 계층과 도메인 계층이 데이터를 서로 주고받을 때 사용하는 일종의 구조체이다.
  • 오래 전에 사용했던 프레임워크는 요청 파라미터나 DB 칼럼의 값을 설정할 때 set 메서드를 필요로 했기 때문에 구현 기술을 적용하려면 어쩔 수 없이 DTO에 getter, setter 메서드를 구현해야 했다고 한다.
    • iBatis의 sql mapping
  • DTO가 도메인 로직을 담고 있지는 않기에 getter, setter 메서드를 제공해도 도메인 객체의 데이터 일관성에 영향을 줄 가능성이 높지 않다.
  • 최근의 개발 프레임워크나 개발 도구는 set 메서드가 아닌 private 필드에 직접 값을 할당할 수 있는 기능을 제공하고 있다.
    • set 메서드를 제공하지 않아도 프레임워크를 이용해서 데이터를 전달받을 수 있다.
  • 프레임워크가 필드에 직접 값을 할당하는 기능을 제공하고 있다면 set 메서드를 만드는 대신 해당 기능을 최대한 활용하는 것을 권장.
    • DTO도 불변 객체가 되어 불변의 장점을 DTO까지 확장 할 수 있게 된다.

도메인 용어

  • 코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요.

    • 도메인 용어를 코드에 반영하지 않으면 그 코드를 해석할 때 의미를 확인해야하는 부담이 생긴다.

      // 상품 준비중, 배송중, 배송완료됨
      public OrderState {
      	STEP1, STEP2, STEP3
      }
      
      // 도메인 사용 용어를 최대한 반영하면 이해하기 용이하다.
      public enum OrderState { 
      	PREPARING, SHIPPED, DELIVERY_COMPLETED
      }
    • step1, step2 를 비교한다거나 값을 확인한다거나 할 때 해당 변수명이 정확히 어떤 의미인지 확인하는 것 없이 코드를 구현할 가능성도 있고

    • 현업이나 다른 개발자와의 협업 시에 '상품 준비중', '배송중' 으로 논의 중일 때 STEP1, STEP2로 해석해서 로직을 이해해야 한다.

  • 도메인 용어를 코드에 사용하면 가독성이 높아지면서 코드를 분석하고 이해하는 시간을 절약한다.

    • + 도메인 규칙의 변경 시 의미를 잘못 변경하여 버그가 줄어들 수 있음.
  • 도메인 용어를 영어로 해석하는 노력이 필요하다. (시간을 아까워하지 말자.)

    • 한글 변수명을 지정할 수는 있지만..... 보통은 알파벳과 숫자를 사용해 class, field, method 등의 이름을 작성한다.
    • 적절한 영어 단어 사용, 도메인에 어울리는 단어를 사용해야 코드가 도메인과 멀어지지 않게된다.
profile
그동안 마신 커피와 개발 지식, 경험을 기록하는 공간

0개의 댓글