DDD 시작하기
본 글에서는 도메인 주도 설계(Domain Driven Design)
의 기본 개념에 대해서 알아본다.
도메인 주도 설계(이하 DDD)에 대해 알기 위해서는 우선 기본적인 용어에 대한 정리가 필요하다.
도메인과 도메인 모델
도메인(Domain) 이란 우리가 소프트웨어로 해결하고자 하는 대상이다. 예를 들어 화상 채팅 서비스를 구현하고자 하는 경우에는 화상 채팅이 도메인이 된다.
도메인은 하위 도메인으로 나뉠 수도 있다. 예를 들어, 화상 채팅 도메인은 화면 공유, 텍스트 채팅, 친구 목록 등의 하위 도메인으로 나뉜다.
도메인 모델(Domain Model) 은 여러 정의가 있지만 우선 기본적으로 도메인을 개념적으로 표현한 것을 도메인 모델이라고 한다.
도메인을 개념적으로 표현하는 방법은 다양하기 때문에, 예를 들어 클래스 다이어그램이나 상태 다이어그램, 또는 그래프나 수학 공식이 모두 도메인 모델이 될 수 있다.
또한, 도메인 모델은 도메인 모델 패턴(Domain Model Pattern) 을 의미하기도 하는데, 도메인 모델 패턴은 아래와 같은 아키텍처 구조에서 도메인 계층을 객체 지향 기법으로 구현한 것을 의미한다.
여기서 도메인 모델은 인프라스트럭쳐 계층(DB, OS 등)과 프레젠테이션 계층(HTTP, HTML 등)에 종속성 없이 순수하게 비즈니스 요구사항을 담는다.
본 글에서는 객체 지향 기법을 사용하여 도메인 모델링을 설명하기 때문에, 도메인 모델의 의미는 두번째 의미에 좀 더 가까우며, 도메인 모델은 클래스나 인터페이스 형태라고 가정한다.
하지만 두 가지 모두가 본질적으로 갖는 공통점은, 도메인 모델이 도메인 규칙(비즈니스 룰, 요구사항)을 담고 있다는 것이다. 예를 들어, 자리 비움 상태인 유저에게는 화상 채팅 요청을 할 수 없다는 조건을 표현할 수 있다.
도메인을 모델링하기 위해서는 우선 핵심 구성요소, 규칙, 기능을 정의해야 하며, 도메인 모델은 이를 잘 담고 있어야한다.
코드가 도메인을 잘 표현해야 가독성이 높아지고, 코드 자체로 문서의 역할을 할 수가 있다.
이를 방해하는 하나의 예시는, 도메인 모델에 습관적으로 public 인 set/get 메서드를 추가하는 것이다.
일부 도메인 규칙을 위해서는 public set/get 메서드가 있으면 안되는 경우가 있기 때문에 잘 생각하여 필요한 경우에만 추가해야한다.
물론 불변 타입을 사용하면 set 메서드는 자연스레 사라질 것이다.
엔티티와 밸류
엔티티(Entity) 와 밸류(Value) 는 대표적인 도메인 모델이다.
둘의 가장 큰 차이는 식별자의 유무이다. 각 엔티티는 고유한 식별자를 가져서 이를 통해 엔티티 간의 구분이 가능하다. 예를 들어, 화상 채팅 앱에서 id 라는 고유한 값을 두어서 각 User 를 구분할 수 있다.
반면, 밸류는 엔티티에 속하는 일종의 데이터의 묶음이며 식별자가 없다. 예를 들어, firstName, middleName, lastName 을 묶어서 Name 이라는 하나의 밸류 타입을 만들 수 있다.
동명 이인이 있을 수 있다는 사실만 보아도 Name 이라는 도메인 모델은 엔티티가 아닌 밸류라는 것을 알 수 있다.
주의할 점은, 밸류도 DB에 저장되면 테이블의 PK가 있을 텐데 식별자가 있는 것 아닌가 하는 생각이 들 수도 있다.
하지만 엔티티에서 얘기했던 식별자는 이것을 의미하는 것은 아니다. PK 는 단순히 관계형 데이터베이스에 저장하기 위해 필요한 개념이지, 도메인 모델간의 구분에 사용되는 것은 아니기 때문이다.
그런데 밸류 타입이 꼭 두 개 이상의 데이터를 가져야하는 것은 아니다. 예를 들어, 돈의 액수를 나타내기 위해 단순히 int 타입을 사용할 수도 있지만 의미를 확실히 하고 돈과 관련된 연산을 내재하기 위해
int 타입의 변수를 하나 가진, Money 라는 밸류 타입을 만들 수도 있다. 이럴 경우, 코드에서 도메인 규칙이 더 잘 드러나기 때문에 코드의 가독성이 높아진다.
물론, Money 라는 밸류 타입을 만들지 않고 변수명을 money 로 할 수도 있겠지만, 밸류 타입을 만들면 변수 네이밍을 더 자유롭게 할 수 있고, 도메인 로직을 메소드 형태로 넣을 수 있다는 것과 같은 추가적인 장점이 있다.
밸류 타입은 Immutable 로 구현하는 것을 선호한다. 즉, 내부 상태를 변경하는 것보단 변경된 상태를 갖는 새로운 객체를 생성하는 것이다.
그 이유는 참조 투명성과 Thread Safety 를 갖게 되어 안전한 코드를 작성할 수 있기 때문이다.
도메인 용어
도메인 용어는 말 그대로 도메인 내에서 사용하는 용어이다. 예를 들어, 도메인 규칙에 사용자가 '자리 비움' 이나 '바쁨' 상태일 때는 통화를 걸 수 없으며, '한가함' 항태일 때는 통화를 걸 수 있다는 내용이 있다고 가정하자.
그렇다면 '자리 비움', '바쁨', '한가함' 은 모두 도메인 용어이다. 이 코드에는 이러한 도메인 용어가 잘 드러나도록 작성되어야 한다.
즉, 'STATE1', 'STATE2', 'STATE3' 과 같은 네이밍이 아닌, 'ABSENT', 'BUSY', 'FREE' 와 같은 네이밍을 사용해야 도메인이 잘 표현되었다고 할 수 있으며, 유지보수성과 가독성이 높아진다.
애그리거트
애그리거트(Aggregate)는 서로 관련이 있는 도메인 모델들의 집합이다.
도메인이 커지면 엔티티와 밸류의 수도 점점 많아지게 된다. 이 때 애그리거트 단위로 캡슐화를 하면 내부 구현을 숨기고 도메인 모델을 더 상위 수준에서 바라볼 수 있게 되어 관리나 변경도 용이해지며, 도메인 모델간의 관계를 좀 더 쉽게 파악할 수 있게 된다.
예를 들어, 인스타그램에서 타임라인과 관련된 기능을 하나의 애그리거트로 묶고, 채팅과 관련된 기능을 하나의 애그리거트로 묶을 수가 있을 것이다.
같은 애그리거트에 속하는 모델들은 유사하거나 동일한 라이프사이클을 가진다. 함께 생성되는 구성 요소들은 같은 애그리거트에 속할 가능성이 높다.
예를 들어, 주문, 주문할 상품 정보(상품의 종류와 개수), 수령인 정보, 배송지 정보 등은 주문이 들어올 때 함께 생성되므로 같은 애그리거트에 속한다.
또한, 함께 변경되는 빈도가 높은 구성요소들도 같은 애그리거트에 속할 가능성이 높다.
예를 들어, 주문하는 상품의 개수가 변경되면 주문의 총 액수를 다시 계산해야하며, 배송지 정보와 주문할 상품의 개수, 수령인 등을 동시에 변경하기도 한다.
또한, 같은 애그리거트에 속하는 모델들은 변경의 주체가 같다.
예를 들어, 상품의 상세 정보와 상품의 리뷰를 생각해보자. 상품의 상세 정보 페이지가 리뷰를 포함하기 때문에 둘이 같은 애그리거트에 속한다고 착각하기 쉬운데,
상품의 상세정보는 판매자가 수정하지만, 리뷰는 고객이 수정하므로 변경의 주체가 다르며, 생성이나 변경이 함께 이루어지지도 않기 때문에 둘은 다른 애그리거트에 속한다.
참고로, 하나의 애그리거트는 하나의 엔티티 객체만 포함하는 경우가 많으며, 둘 이상의 엔티티로 이루어진 경우는 드물다. (밸류는 여러개가 있을 수 있다.)
애그리거트는 DDD 에서 굉장히 중요한 개념이기 때문에 자세한 설명은 별도의 포스트인 DDD 의 Aggregate에서 다루도록 하겠다.
리포지터리
리포지터리(Repository)는 도메인 모델의 영속성(Persistency)를 위해 필요한 도메인 모델이다.
엔티티와 밸류가 요구사항에서 도출되는 도메인 모델이라면, 리포지터리는 구현을 위한 도메인 모델이다.
리포지터리도 도메인 모델이기 때문에 엔티티와 밸류와 함꼐 도메인 계층에 포함된다.
덧붙이자면, 리포지터리는 특정 기술에 종속되지 않고 순수하게 도메인 규칙을 정의하기 때문에 보통 인터페이스 형태로 존재하는데
실제로 도메인 모델을 저장하기 위해서는 리포지터리를 구현하는 구체적인 구현체가 필요하며, 이는 인프라스트럭처 계층에 속한다.
리포지터리는 애그리거트 단위로 도메인 모들을 저장하거나, 애그리거트 루트의 식별자로 애그리거트를 조회하는 역할을 한다.
도메인 서비스
여러 애그리거트가 사용되어 하나의 애그리거트에 넣기는 애매한 로직을 가지는 개념이다.
여러 애그리거트가 관여하는 로직을 한 애그리거트에 넣게되면, 그 애그리거트는 역할이 늘어나고 응집도가 떨어지게 된다.
이 때 도메인 서비스라는 별도의 클래스를 만들어 이러한 로직을 가지는 메서드를 정의할 수 있다.
도메인 서비스는 응용 서비스에서 사용될 수도 있고, 애그리거트에서 사용될 수도 있는데,
애그리거트의 메소드 인자로 도메인 서비스와 다른 애그리거트를 전달하는 경우는 애그리거트가 도메인 서비스를 사용하는 주체가 된다.
이 때, 응용 서비스는 도메인 서비스를 애그리거트에게 전달해줄 책임이 있다.
도메인 서비스의 메서드 인자로 여러 애그리거트를 넣어 로직을 수행하는 경우엔 응용 서비스가 도메인 서비스의 사용 주체가 된다.
도메인 서비스를 응용 서비스와 헷갈릴 수 있는데, 둘은 엄연히 다르다.
도메인 서비스는 도메인 계층에 있으며 도메인 로직을 다루는 반면, 응용 서비스는 응용 계층에 있으며 응용 로직을 다룬다.
또한, 응용 서비스는 다른 도메인 모델들을 이용하여 애플리케이션의 기능을 구현한다.