본문 바로가기
Book/도메인 주도 개발 시작하기

[도메인 주도 개발 시작하기] Chapter 1. 도메인 모델 시작하기

by 프로그래 밍구 2023. 1. 12.

1.1 도메인이란?

 도메인은 소프트웨어로 해결하고자 하는 문제 영역을 말한다. 한 도메인은 다시 하위 도메인으로 나눌 수 있다. 예를 들어 온라인 서점이라는 도메인은  주문, 배송, 혜택 등 하위 도메인으로 나눌 수 있다. 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 직접 구현하는 것은 아니다. 예를 들어 온라인 쇼핑몰이 외부 배송 업체의 시스템을 사용하고 배송 추적 정보를 제공하는 데 필요한 기능만 일부 연동하는 것처럼 말이다. 결제 시스템도 직접 구현하기보다는 결제 대행업체를 이용해서 처리할 경우가 많다.

 

1.2 도메인 전문가와 개발자 간 지식 공유

 온라인 홍보, 정산, 배송 등 각 영역에는 전문가가 있다. 전문가들은 해당 도메인에 대한 지식과 경험을 바탕으로 본인들이 원하는 기능 개발을 요구한다. 개발자는 이런 요구사항을 분석하고 설계하여 코드를 작성하며 테스트하고 배포한다. 이 과정에서 요구사항은 첫 단추와 같다. 그래서 코딩에 앞서 요구사항을 올바르게 이해하는 것이 중요하다. 올바르게 요구사항을 이해하려면 개발자와 전문가가 직접 대화하는 방법이 비교적 간단하다.  그리고 도메인 전문가 만큼은 아니겠지만 개발자와 이해관계자도 도메인 지식을 갖춰야 한다. 도메인 전문가, 관계자, 개발자가 같은 지식을 공유하고 직접 소통할 수록 도메인 전문가가 원하는 제품을 만들 가능성이 높아진다.

 

1.3 도메인 모델

  도메인 모델에는 다양한 정의가 존재한다. 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다. 도메인을 이해하려면 도메인이 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야 하는데 이런 면에서 기능과 데이터를 함께 보여주는 객체 모델은 도메인을 모델링하기에 적합하다. 객체로만 도메인 모델을 모델링할 수 있는 것은 하니다. 상태 다이어그램을 이용해서 주문의 상태 전이를 모델링할 수 있다. 관계가 중요한 도메인이라면 그래프를 이용해서, 계산 규칙이 중요하다면 수학 공식을 활용해서 도메인 모델을 만들 수도 있다. 도메인의 개념 모델과 구현 모델은 서로 다른 것이지만 구현 모델이 개념 모델을 최대한 따르도록 할 수는 있다. 예를 들어 객체 기반 모델을 기반으로 도메인을 표현했다면 객체 지향 언어를 이용해 개념 모델에 가깝게 구현할 수 있다.

 

1.4 도메인 모델 패턴

 일반적인 애플리케이션의 아키텍처는 다음과 같이 네 개의 영역으로 구성된다.

 

 '도메인 모델'이란 용어는 도메인 자체를 표현하는 개념적인 모델을 의미하지만, 도메인 계층을 구현할 때 사용하는 객체 모델을 언급할 때에도 '도메인 모델'이란 용어를 사용한다. 이 절에서의 도메인 모델은 마틴 파울러가 쓴 '엔터프라이즈 애플리케이션 아키텍처 패턴' 책의 도메인 모델 패턴을 의미한다. 도메인 모델은 아키텍처 상의 도메인 계층을 객체 지향 기법으로 구현하는 패턴을 말한다. 도메인 계층은 도메인의 핵심 규칙을 구현한다. 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.

 

1.5 도메인 모델 도출

 도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다. 이 과정은 요구사항에서 출발한다. 요구사항에서 도메인 모델을 점진적으로 만들어 나갔다면, 이 모델은 요구사항 정련을 위해 도메인 전문가나 다른 개발자와 논의하는 과정에서 공유하기도 한다. 모델을 공유할 때는 화이트보드나 위키와 같은 도구를 사용해서 누구나 쉽게 접근할 있도록 하면 좋다.

 문서화를 하는 주된 이유는 지식을 공유하기 위함이다. 전반적인 기능 목록이나 모듈 구조, 빌드 과정은 코드를 보고 직접 이해하는 것보다 상위 수준에서 정리한 문서를 참조하는 것이 소프트웨어 전반을 빠르게 이해하는 데 도움이 된다. 전체 구조를 이해하고 더 깊게 이해할 필요가 있는 부분을 코드로 분석해 나가면 된다. 코드를 보면서 도메인을 깊게 이해하게 되므로 코드 자체도 문서화의 대상이 된다. 단순히 코드를 보기 좋게 작성하는 것뿐만 아니라 도메인 관점에서 코드가 도메인을 잘 표현해야 비로소 코드의 가독성이 높아지고 문서로서 코드가 의미를 갖기 때문에 도메인 지식이 잘 묻어나도록 코드를 작성해야한다.

 

1.6 엔티티와 밸류

 도출한 모델은 크게 엔티티(Entity)와 밸류(Value)로 구분할 수 있다. 엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있기 때문에 이 둘의 차이를 명확하게 이해하는 것은 도메인을 구현하는 데 있어 중요하다.

 

1.6.1 엔티티

 엔티티의 가장 큰 특징은 식별자를 가진다는 것이다. 식별자는 엔티티 객체마다 고유해서 각 엔티티는 서로 다른 식별자를 갖는다. 예를 들어 주문 도메인에서 각 주문은 주문번호를 가지고 있는데 이 주문번호는 각 주문마다 서로 다르다. 따라서 주문번호가 주문의 식별자가 된다.  주문 도메인 모델에서 주문에 해당하는 클래스가 Order라면 Order가 엔티티가 되며 주문번호를 속성으로 갖는다. 주문에서 배송지 주소가 바뀌거나 상태가 바뀌더라도 주문번호가 바뀌지 않는 것처럼 엔티티의 식별자는 바뀌지 않는다. 마찬가지로 엔티티의 식별자는 바뀌지 않고 고유하기 때문에 두 엔티티 객체의 식별자가 같으면 두 엔티티는 같다고 판단할 수 있다.

 

1.6.2  엔티티의 식별자 생성

엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다. 주로 식별자는 다음 중 한 가지 방식으로 생성한다.

  • 특정 규칙에 따라 생성
  • UUID나 Nano ID와 같은 고유 식별자 생성기 사용
  • 값을 직접 입력
  • 일련번호 사용(시퀀스나 DB의 자동 증가 칼럼 사용)

 주문번호, 운송장번호, 카드번호와 같은 식별자는 특정 규칙에 따라 생성한다. 이 규칙은 도메인에 따라 다르고, 같은 주문번호라도 회사마다 다르다. 흔히 사용하는 규칙은 현재 시간과 다른 값을 함께 조합하는 것이다. 주의할 점은 같은 시간에 동시에 식별자를 생성해도 같은 식별자가 만들어지면 안 된다는 것이다.

UUID(universally unique identifier)를 사용해서 식별자를 생성할 수 있다. 다수의 개발 언어가 UUID 생성기를 제공하고 있다. 자바는 java.util.UUID 클래스를 사용해서 UUID를 생성할 수 있다.

UUID uuid = UUID.randomUUID();

// 615f2ab9-c374-4b50-9420-2154594af151과 같은 형식 문자열
String strUuid = uuid.toString();

 

회원의 아이디나 이메일과 같은 식별자는 값을 직접 입력한다. 사용자가 직접 입력하는 값이기 때문에 식별자를 중복해서 입력하지 않도록 사전에 방지하는 것이 중요하다. 

 일련번호 방식은 주로 데이터베이스가 제공하는 자동 증가 기능을 사용한다. 예를 들어 오라클을 사용한다면 시퀀스를 이용해서 자동 증가 식별자를 구하고 MySQL을 사용한다면 자동 증가 칼럼을 이용해서 일련번호 식별자를 생성한다. 자동 증가 칼럼을 제외한 다른 방식은 식별자를 먼저 만들고 엔티티 객체를 생성할 때 식별자를 전달한다. 자동 증가 칼럼은 DB 테이블에 데이터를 삽입해야 비로소 값을 알 수 있기 때문에 테이블에 데이터를 추가하기 전에는 식별자를 알 수 없다. 이는 엔티티 객체를 생성할 때 식별자를 전달할 수 없음을 의미한다.

 

1.6.3 밸류 타입

 밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다. 예를 들어 받는 사람을 위한 밸류 타입인 Receiver를 다음과 같이 작성할 수 있다.

public class Receiver{
    private String name;
    private String phoneNumber;
    
    public Receiver(String name, String phoneNumber){
    	this.name = name;
        this.phoneNumber = phoneNumber;
    }
    
    public String getName(){
    	return name;
    }
    
    public String getPhoneNumber(){
    	return phoneNumber;
    }
}

 

 Receiver는 '받는 사람'이라는 도메인 개념을 표현한다. 밸류 타입을 사용함으로써 개념적으로 완전한 하나를 잘 표현할 수 있는 것이다. 밸류 타입의 또 다른 장점은 밸류 타입을 위한 기능을 추가할 수 있다는 것이다. 예를 들어 Money 타입은 다음과 같이 돈 계산을 위한 기능을 추가할 수 있다.

public class Money{
    private int value;
    
    public Money add(Money money){
    	return new Money(this.value + money.value);
    }
    
    public Money multiply(int multiplier){
    	return new Money(value * multiplier);
    }
}

 

 밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호한다. 예를 들어 앞서 Money 클래스의 add( ) 메서드를 보면 Money를 새로 생성하고 있다.

 Money처럼 데이터 변경 기능을 제공하지 않는 타입을 불변(immutable)이라고 표현한다. 밸류 타입을 불변으로 구현하는 여러 이유가 있는데 가장 중요한 이유는 안전한 코드를 작성할 수 있다는 데 있다.

 

1.6.4 엔티티 식별자와 밸류 타입

 엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많다. Money가 단순 숫자가 아닌 도메인의 '돈'을 의미하는 것처럼 이런 식별자는 단순한 문자열이 아니라 도메인에서 특별한 의미를 지니는 경우가 많기 때문에 식별자를 위한 밸류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다. 예를 들어 OrderNo 대신에 String 타입을 사용한다면 'id'라는 이름만으로는 해당 필드가 주문번호인지를 알 수 없다. 필드의 의미가 드러나도록 하려면 'id'라는 필드 이름 대신 'orderNo'라는 필드 이름을 사용해야 한다. 반면에 식별자를 위해 OrderNo 타입을 만들면 타입 자체로 주문번호라는 것을 알 수 있으므로 필드 이름이 'id'여도 실제 의미를 찾는 것은 어렵지 않다.

 

1.6.5 도메인 모델에 set 메서드 넣지 않기

 도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않다. 특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다. 또 다른 문제는 도메인 객체를 생성할 때 온전하지 않은 상태가 될 수 있다. 도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다. 즉 생성자를 통해 필요한 데이터를 모두 받아야 한다. set 메서드를 구현해야 할 특별한 이유가 없다면 불변 타입의 장점을 살릴 수 있도록 밸류 타입은 불변으로 구현한다.

 

1.7 도메인 용어와 유비쿼터스 언어

 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만들고 이를 대화, 문서, 도메인 모델, 코드, 테스트 등 모든 곳에서 사용하는 언어를 유비쿼터스 언어라고 한다. 예를 들어 STEP1, STEP2, STEP3이 아닌 PAYMENT_WAITING, PREPARING, SHIPPED와 같이 도메인에서 사용하는 용어를 최대한 코드에 반영하면 코드를 분석하고 이해하는 시간을 줄여준다. 개발자는 도메인 용어에 알맞은 단어를 찾는 시간을 가져야한다.

댓글