학습일지/DDD
[DDD] 도메인, 엔티티와 밸류, DIP 주의사항
Merge Log
2025. 9. 19. 15:45
도메인
- 소프트웨어로 해결하고자 하는 문제 영역
- 하나의 도메인은 다시 하위 도메인으로 나눌 수 있다
- 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다
- 제공하는 서비스뿐 아니라 타켓 사용자가 누구인지에 따라서도 달라질 수 있다
- ex. 기업 고객을 대상으로 대형 장비를 판매하는 곳은 온라인으로 카탈로그를 제공하고 주문서를 받는 정도만 필요하다
- 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다
요구사항을 올바르게 이해하려면?
- 소비자는 자신이 소프트웨어를 통해 원하는 목적을 해결한다
- 이 요구사항을 잘 파악해야한다
- 요구사항을 올바르게 이해하려면 어떻게 해야하는가? → 해당 도메인의 전문가와 직접적으로 대화해보는 것 이다
- 전문가와 개발자 사이에 전달하려는 사람이 많으면 중간중간 정보가 왜곡되고 손실된다
- 전문가나 관련자가 요구한 내용이 항상 올바른 것은 아니며 때론 본인들이 실제로 원하는 것을 정확하게 표현하지 못할 때도 있다
- 그래서 개발자는 요구사항을 이해할 때 왜 이런 기능을 요구하는지 또는 실제로 원하는 게 무엇인지 생각하고 전문가와 대화를 통해 진짜로 원하는 것을 찾아야 한다
도메인 모델
- 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것 이다
- 도메인 모델을 통해 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다
- 도메인을 이해하는 데 도움이 된다면 객체 모델이든 UML 이든 그래프, 수학적 공식 등 표현 방식이 무엇인지는 중요하지 않다
- 기본적으로 도메인 자체를 이해하기 위한 개념 모델이다
하위 도메인과 모델
- 도메인은 다수 하위 도메인으로 구성된다
- 각 하위 도메인이 다루는 영역은 서로 다르기 때문에 같은 용어라도 하위 도메인마다 의미가 달라질 수 있다
- 예를 들어 카탈로그 도메인의 상품이 상품 가격, 상세 내용을 담고 있는 정보라면 배송 도메인의 상품은 고객에게 실제 배송되는 물리적인 상품을 의미한다
- 도메인에 따라 용어 의미가 결정되므로 여러 하위 도메인을 하나의 다이어그램에 모델링하면 안된다
- 다른 도메인의 같은 용어를 이해하는데 방해가 되기 때문이다
- 모델의 각 구성요소는 특정 도메인으로 한정할 때 비로소 의미가 완전해지기 때문에 각 하위 도메인마다 별도로 모델을 만들어야 한다
도메인 계층
- 도메인의 핵심 규칙을 구현한다 / 시스템이 제공할 도메인 규칙을 구현한다
- 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다
응용 계층
- 응용 영역은 기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다
- 응용 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다
public class CancelOrderService {
@Transactional
public void cancelOrder(String orderId) {
Order order = findOrderById(orderId);
if (order == null) throw new OrderNotFoundException(orderId);
order.cancel();
}
}
완벽한 도메인 모델
- 개념 모델은 순수하게 문제를 분석한 결과물이다
- 개념 모델을 만들 때 처음부터 완벽하게 도메인을 표현하는 모델을 만드는 시도를 할 수 있지만 실제로 이것은 불가능하다
- 소프트웨어를 개발하는 동안 개발자와 관계자들은 해당 도메인을 더 잘 이해하게 된다.
- 프로젝트 초기에 이해한 도메인 지식이 시간이 지나 새로운 통찰을 얻으면서 완전히 다른 의미로 해석되는 경우도 있다
- 따라서 처음부터 완벽한 개념 모델을 만들기보다는 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다.
- 프로젝트 초기에는 개요 수준의 개념 모델로 도메인에 대한 전체 윤곽을 이해하는 데 집중하고 구현하는 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야 한다
모델은 크게 엔티티(Entity) 와 밸류(Value / 값 객체) 로 구분할 수 있다
엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있기 때문에 둘의 차이를 이해하는 것은 중요하다
엔티티
- 식별자를 가진다
- 예: 각 주문은 주문번호를 가지고 주문번호는 주문마다 다르기 때문에 주문을 식별하는데 사용된다 즉 주문번호는 주문의 식별자이다 → 고유하다
- 식별자가 같으면 같은 객체라고 판단할 수 있다
- 도메인 모델의 엔티티는 DB 모델의 엔티티와 같은 것이 아니다
- 가장 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인의 핵심 기능을 제공한다는 점이다
밸류
- 하나의 개념을 표현할 때 사용한다
- 예: 받는 사람을 위한 밸류 타입인 Receiver / 주소 밸류 타입인 Address / 돈을 의미하는 Money
- 밸류 타입을 사용함으로써 개념적으로 완전한 하나를 잘 표현할 수 있는 것 이다
- 밸류 타입의 또 다른 장점은 밸류 타입을 위한 기능을 추가할 수 있다는 것 이다
- 예: Money 타입은 돈 계산을 위한 기능을 추가할 수 있다
- 밸류는 기존 데이터를 변경하기 보다는 새로운 밸류를 만드는 것을 선호한다 → 불변
- 엔티티의 식별자를 단순한 문자열로 표현하는 것이 아닌 특정 도메인 개념이 담긴 식별자라는 것을 표현하기 위해 밸류로 구성하는 것이 좋다
- 의미가 잘 드러나도록 / 주문번호 → OrderNumber
애그리거트 (Aggrigate)
- 도메인 모델이 복잡해지면 개발자가 전체 구조가 아닌 한 개 엔티티와 밸류에만 집중하는 상황이 발생한다
- 이때 상위 수준에서 모델을 관리하지 않고 개별 요소에만 초점을 맞추다 보면 큰 수준에서 모델을 이해하지 못해 큰 틀에서 모델을 관리할 수 없는 상황에 빠질 수 있다
- 도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 바로 애그리거트이다
- 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것 → 관련 객체를 하나로 묶은 군집
- 예를 들어 주문과 관련된 Order 엔티티, OrderLine 밸류, Orderer 밸류 객체를 "주문" 애그리거트로 묶을 수 있다
- 개별객체가 아닌 관련 객체를 묶어서 객체 군집 단위로 모델을 바라볼 수 있고 개별 객체 간의 관계가 아닌 애그리커트 간의 관계로 도메인 모델을 이해하고 구현하게 된다 → 이를 통해 큰 틀에서 도메인 모델을 관리할 수 있다
루트 엔티티
- 애그리거트는 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다
- 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다
- 애그리거트를 사용하는 코드는 애그리거트 루트가 제공하는 기능을 실행하고 애그리거트 루트를 통해서 간접적으로 애그리거트 내의 다른 엔티티나 밸류 객체에 접근한다
- 이것은 애그리거트의 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화할 수 있도록 돕는다
- 예: 배송지 정보를 변경할 때 주문(Root)을 통해 배송지를 변경할 수 있는지 확인한 뒤에 배송지 정보를 변경한다 / 주문 애그리거트는 주문(Root) 를 통하지 않고 정보를 변경할 수 있는 방법을 제공하지 않는다
- 애그리거트를 구현할 때는 고려할 것이 많다.
- 애그리거트를 어떻게 구성했느냐에 따라 구현이 복잡해지기도 하고, 트랜잭션 범위가 달라지기도 한다
Repository는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다
public interface OrderRepository {
Order findByOrderNumber(OrderNumber orderNumber);
void save(Order order);
void delete(Order order);
}
Order는 애그리거트 Root 이며 애그리거트에 속한 모든 객체를 포함하고 있으므로 결과적으로 애그리거트 단위로 저장하고 조회한다
get/set
- 도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않은 버릇이다
- 특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다
- completePayment() 를 setOrderState() 로 변경한다면 "결제 완료" 라는 의미가 사라진다
- 습관적으로 작성한 set 메서드는 필드값만 변경하고 끝나기 때문에 상태 변경과 관련된 도메인 지식이 코드에서 사라지게 된다
- Client 에서 무분별한 set 메서드 사용으로 인해 불완전한 도메인이 생성될 수 있다
- 도메인 객체가 불완전한 상태로 사용되는 것을 막기으려면 생성 시점에 필요한 것을 전달해 주어야 한다
유비쿼터스 언어 (ubiqquitous language)
- 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만들고 이를 대화, 문서, 도메인 모델, 코드, 테스트 등 모든 곳에서 같은 용어를 사용한다
- 이렇게 하면 소통 과정에서 발생하는 용어의 모호함을 줄일 수 있고 개발자는 도메인과 코드 사이에서 불필요한 해석 과정을 줄일 수 있다
- 시간이 지날수록 도메인에 대한 이해가 높아지는데 새롭게 이해한 내용을 잘 표현할 수 있는 용어를 찾아내고 이를 다시 공통의 언어로 만들어 다 같이 사용한다
DIP 주의사항
- DIP
- DIP 의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함이다!
- 그러나 인터페이스를 추출할 때 저수준 모듈을 바라보며 인터페이스를 추출하는 경우가 있다
- 이는 잘못된 구조이다. 여전히 고수준 모듈이 저수준 모듈에 의존하고 있는 것이다
- 예를 들어
KakaoEnginAPI이라는 저수준 모듈이 있다면 이를 저수준 모듈 관점에서KakaoAPI혹은KEngineAPI등의 저수준 관점에서 인터페이스 추출을 하는 것
- 예를 들어
- DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해야한다
- 고수준 모듈에서 필요한 것이 무엇인지를 생각하고 그에 맞는 이름과 인터페이스를 추출해야한다!