-
[방법론] 도메인 주도 개발 시작하기 DDD - 1방법론 2023. 12. 14. 20:49
책 제목 : 도메인 주도개발 시작하기 DDD
저자 : 한범균
출판사 : 한빛미디어
0. 도메인
일반적으로 들어본 도메인 정의
도메인 : 인터넷 주소(IP)를 인간 친화적으로 볼 수있게 문자 형식으로 표시 한 것.
프로그래밍상 정의
책에서 정의 : 소프트웨어적으로 해결하기 위한 대상을 의미
구글에서 찾은 정의 : 동일한 업무내에서 수행 하는 집합 혹은 그룹을 의미한다. ( 인터넷을 검색하면 미세하게 다르지만 다 비슷하게 언급된다.)하위 도메인
도메인은 여러 하위도메인(sub domain)으로 이루어지며 각 하위 도메인 간에 기능이 엮여서 하나의 기능을 수행한다
-> 카탈로그 도메인은 고객에게 상품 목록을 제공해주는 도메인을 제공하고 혜택 도메인은 고객에게 등급별 쿠폰 발급등의 도메인 기능을 수행한다.
예시
온라인 서점에서는 고객이 주문을 하면 결제와 배송이 이루어지고 결제 금액이 정산도 되는데 이런 각각의 역할을 수행 하는 집합들을 도메인이라 한다이해한 내용
비즈니스에서 하나의 연관된 기능들을 도메인이라고 칭하는 것 같다.
주문이면 주문관련된 기능을 주문 도메인, 회원이면 회원과 관련된 기능들을 도메인이라고 칭하는 것라고 생각한다.
도메인과 개발자
도메인은 각 주문, 결제, 배송 등의 전문가들의 요구사항을 제대로 이해해야 올바른 설계를 할 수 있다.
이해한 내용
개발자에게 기술도 중요하지만 더 중요한 부분은 비즈니스 도메인에 대한 이해도이다.
도메인에 대한 이해도가 있어야 그에 맞는 기술이 연결되기 때문이다.유비쿼터스 언어
에릭 에반스(DDD 저자)는 DDD에서 언어의 중요함을 강조하기 위해서 유비쿼터스 언어라는 용어를 사용했다.
개발자가 도메인 관련 공통언어를 만들어 대화, 문서, 도메인 모델, 코드 테스트에 적용하여 소통과정에서 발생하는 용어의 모호함을 줄일수있고 개발자는 도메인과 코드 사이에서 불필요한 해석 과정을 줄일 수 있다.이해한 내용
개발 시 변수명 지을때 타인이 봤을때 어떤 의미를 내포하고 있을지 유추 가능하게 지으라는 것 같다.
도메인 모델 개념적 의미
도메인을 개념적으로 표현한 것을 말하며 도메인을 이해하는데 도움이 된다면 표현 방식이 무엇인지 중요하지않다.
개념 모델을 이용해서 바로 코드 작성 할 수있는 것은 아니며 구현 기술에 맞는 구현 모델이 따로 필요하다.
자바 객체 형식 혹은 다이어그램 등으로 표현 한다. 관련 도메인 모델만 작성하며 관련없는 도메인을 넣어 이해에 방해가 되지않도록 한다.이해한 내용
도메인 모델의 개념적 의미는 누군가 한테 주문(도메인)에 대해 설명할때 쉽게 이해 할 수 있게 수단을 가리지 않고 표현 한다는 의미 인 것 같다.
모메인 모델 프로그래밍상 의미
도메인 계층을 구현할 때 사용하는 객체 모델 ( 엔티티와 도메인에 해당되는 규칙들을 포함한다. )
이해한 내용
회원(도메인)을 개발시 회원가입 관련 로직에서 이메일 체크( 규칙 ) 라던지 닉네임 변경 기능 이라던지를 이러한 기능을 갖는 실제 코드로 표현한 객체 모델DDD에서는 회원(도메인)과 관련된 기능은 응용 서비스 계층에서 구현하지않고 엔티티(도메인) 내부에서 구현하고 서비스에서는 기능의 순서만 정한다.
일반적이 어플리케이션 아키텍처
1번째 영역(프레젠테이션 영역) : 사용자 인터페이스
2번째 영역(응용 영역) : 사용자가 요청한 기능을 실행, 응용 계층에서 모든 로직을 구현하지않고 도메인 계층에서 구현한 기능을 순서만 조합하여 실행 (DDD핵심이라고 생각된다)
3번째 영역(도메인 영역) : 시스템이나 제공할 도메인 규칙 구현
4번째 영역 (인프라스트럭처 영역) : DB, 메시징 시스템과 같은 외부 시스템 연동 처리코드 예시
도메인 계층은 도메인의 핵심 규칙을 구현한다.
예) "주문시 재고를 확인한다." 라는 규칙은 상품(도메인) 계층에 위치하게 된다.(재고는 상품의 밸류타입 이기 때문에 상품 도메인에서 수정한다)
1) DDD와 맞지 않는 코드 (응용 계층에서 비즈니스 로직 구현)public class orderService { ... public Order orderProduct(User user, OrderInfo orderInfo){ Product product = ProductRepository.findById(orderInfo.getProductId()); // 재고 확인 if(product.stockInfo.getStock() < orderInfo.getStockInfo().getStock()){ throw new IllegalArgumentException("재고가 충분하지 않습니다."); } ..... }
2) 도메인 계층에서 비즈니스 로직 구현// 응용 계층 public class orderService { public Order orderProduct(User user, OrderInfo orderInfo){ Product product = ProductRepository.findById(orderInfo.getProductId()); // 재고 확인 product.checkStock(orderInfo); ..... } } // 상품 계층 public class Product { private Stock stockInfo; public void checkStock(OrderInfo orderStock){ if(this.stockInfo.getStock() < orderStock.getStockInfo().getStock()){ throw new IllegalArgumentException("재고가 충분하지 않습니다."); } .... } }
이해한 내용
도메인 모델 (도메인 계층) 패턴을 구현한경우 메서드 명만으로 확인이 가능하며 당장 확인이 필요한 로직이 아닌 경우 코드량이 줄어들어 가독성이 좋아질 수 있다.1. 엔티티와 밸류
엔티티
식별자를 갖고 있는 테이블을 의미한다.
밸류
개념적으로 하나를 표현 할 때 사용 ( 필드 수와 상관 없음)
코드 예시1) 주문 엔티티
@Entity public class Order { @Id private Long id; // 두 필드의 목적은 받는 사람을 위한 밸류이다. private String receiverName; private String receiverPhoneNumber; }
2) 밸류// 클래스로 정의 한다. public class Reciver { private String name; private String phoneNumber; ..기본 생성자 ( 접근 제어자를 protected 초기화시 올바르게 정보가 기입 될 수 있도록 막아 둔다.) ..생성자 ( 초기화 하고자 하는 생성자 ) ..get 메서드 } public class Order { @Id private Long id; // 하나의 밸류로 표기한다. private Reciver receiver; }
밸류타입은 꼭 두개 이상의 필드를 갖을 필요는 없다. 의미를 명확하게 전달하기위해서 사용하는 경우도 있다.
코드 예시1) 돈을 표현하는 price, amount를 필드로 적용한 경우
public class OrderLine { private int price; private int amount; }
2) Money 밸류로 변경한 경우public class OrderLine { // 명확하게 돈이라는 밸류 적용 private Money price; private Money amount; } public class Money { private int value; ..기본 생성자 ..생성자 }
이런방법으로 클래스를 한번 더 감싸지만 명확한 의미를 새겨서 밸류객체로 둔다면 가독성이 증가한다.set 사용 지양
또한 객체 불변성과 잘못된 접근으로 정보가 누락되는 것을 방지하기 위해서 set메서드 사용을 제한한다.
set 메서드를 생성하지않고 생성자를 통해서만 생성하여 정확한 정보를 전달하여 온전한 객체 상태를 갖게한다.이해한 내용
set을 외부에서 호출하는 순간 도메인 모델이 잘못 구현되었을 가능성이 있다고 생각된다.
매우 편한 객체 상태변화 메서드지만 DDD방법론에서는 금기시된다.
이미 도메인 모델을 통해서 값을 변경하기로 약속했는데 그 규칙을 어긴다면 스파게티 코드 시작을 알린다고 우려된다.
도메인 모델 내부의 명확한 상태 메서드를 통해서만 상태가 변경되어야 원치않는 상태 값 변경(레퍼런스)과 중복 로직을 막을 수 있다.
예시
도메인 계층에 회원 이메일 중복 체크 로직을 만들었는데 응용계층에서 누군가 이메일 중복체크 로직을 알지 못해서 새로 이메일 중복 체크 로직을 구현한다면 중복된 코드와 이러한 코드들의 혼재로 코드가 스파게티 처럼 된다. DDD적용시 기본 규칙을 어겨서는 안된다.
따라서 애초에 set메서드를 제한하여 도메인 모델 패턴의 초기 기획을 무시하지 않도록 해야 한다.2. 애그리거트
프로젝트 규모가 커지면 발생할 수 있는 문제
도메인이 커지면서 도메인 모델도 많아지고 밸류는 많아지고 모델은 복잡해진다.
도메인 모델이 복잡해지면 개발자가 전체 구조가 아닌 엔티티와 밸류에만 초점을 맞추는 상황이 발생한다.반대로 개별 객체 수준의 모델을 상위 수준에서만 본다면 관계를 파악하기 힘들다.
지도에서 대축적 지도만 보면 현재 위치가 전체에서 어디인지 정확히 알기 어려움과 비슷하다.
이런 상황을 타개하는 것에 도움이 되는 부분이 애그리거트(aggregate)다.애그리거트는 관련객체를 하나로 묶은 군집이다.
예시회원이 주문 -> 결제 -> 배송 의 관계를 주문으로 묶어서 주문 애그리거트라 한다.
주문 애그리거트 루트는 주문이며 주문을 기준으로 엔티티와 밸류가 속하게 되고 주문을 통해 일관된 정보를 관리하게 된다.
해결방안 애그리거트 루트
애그리거트 루트는 단순히 애그리거트에 속한 객체를 포함하는 것 뿐아니라 애그리거트의 일관성이 깨지지 않도록 하는 것이다.
이를 위해 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현한다.
예를 들어 주문 애그리거트는 배송지 변경, 상품 변경과 같은 기능을 제공하고 애그리 거트 루트인 Order가 이 기능을 구현한 메서드를 제공한다.애그리거트내 다른 애그리거트 참조
@OneToOne @ManyToOne @OneToMany 로 외부 테이블에 FK를 걸수 있는데 이런 편한 옵션들이 문제를 일으키는 경우가 많이 있다.
-> 편한탐색 오용, 성능 문제, 확장 어려움애그리거트내 참조시 문제 예시
1. 한 애그리거트에서 다른 애그리거트로 접근하는 문제 (편한 방법이기때문에 자주 사용해버린다) -> 다른 애그리거트 상태를 변경하는 유혹에 빠지기 쉽다.
2. N + 1 문제로 만일 게시글 하나에 댓글이 10개 달리면 11번의 쿼리가 날아간다. (게시글 조회 1번 해당되는 댓글 10번)
3. 서로 애그리거트간의 접근을 제거하면 주문은 RDB를 사용하고 상품은 몽고DB를 사용해서 도메인 서비스에서 하나의 기능으로 구현 할 수 있다.
-> 애그리거트마다 다른 저장소 사용시 (+캐시) 한번에 조회는 불가능하며 코드는 복잡해지지만 시스템 처리량을 늘릴 수 있다.2개 이상의 애그리거트 상태 변경
두개 이상의 애그리거트가 작용하는 기능에서는 특정 애그리거트 내에서 타 애그리거트 상태를 변경하면 안된다.
애그리거트간 결합도가 높아지면 수정하기 어려워진다.이 경우 도메인 서비스 계층에서 각 애그리거트 작업을 순서대로 작업 하도록 분리해서 진행한다.
코드 예시
1) 잘못된 상태 변경
public class Order { private Orderer orderer; public void shipTo(ShippingInfo newShippingInfo, boolean useNewShippingAddrAsMemberAddr){ verifyNotYetShipped(); setShippingInfo(newSHippingInfo); if(useNewShippingAddrAsMemberAddr) { // 주문 애그리거트 내에서 회원 애그리거트 변경 orderer.getMember().changeAddress(newShippingInfo.getAddress()); } } }
2) 올바른 상태 변경 방안
public class ChangeOrderService { // 두개 이상의 애그리거트 변경해야 하면, // 응용 서비스에서 각 애그리거트의 상태를 변경한다. @Transactional public void changeShippingInfo(OrderId id, ShippingInfo new ShippingInfo, boolean useNewShippingAddrAsMemberAddr) { Order order = orderRepository.findById(id); if(order == null) 예외 던지기 order.shipTo(newShippingInfo); if (userNewShippingAsMemberAddr) { Member member = findMember(order.getOrderer()); member.changeAddress(newShippingInfo.getAddress()); } } }
애그리거트와 레포지토리
Order와 Orderer가 다른 DB라 해서 레포를 개별로 만들지 않는다. Order에 속하는 구성 요소이므로 한개의 레포지토리만 만든다.
애그리거트 팩토리로 활용 예시
1. 중고 거래에서 상품을 생성시 이 상품이 생성이 가능한지 확인이 필요하다.
2. 상점 애그리거트에서 정지된 상점이 아닌경우 상품을 생성 할 수 있도록 검증과 함께 생성을 진행하도록 한다. or 검증만하고 상품 생성 팩토리에 생성을 위임할 수 도있다.3. 바운디드 컨텍스트
한개의 모델로 다 맞출 수 없기때문에 용어를 다 같이 쓰지 않고 구분해야한다.
DDD에 맞는 도메인을 개발하기 위해서는 하위 도메인마다 모델을 만들고 명시적으로 구분 되는 경계를 가져서 섞이지 않도록 해야한다.한개의 모델로 여러 하위 모델을 표현할 수 없다.
예시
같은 회원 도메인이지만 보는 입장에서 다를 수 있다.
회원 도메인 -> 회원
주문 도메인 -> 주문자
배송 도메인 -> 수취인이해한 내용
같은 도메인 모델을 사용하더라도 컬럼 이름 다르게 하여 명시적으로 경계를 구분 한다.
4. DIP
DIP (Dependency Inversion Principle)
저수준 모듈에서 고수준 모듈을 의존하여 사용
고수준 모듈이란 의미있는 단일 기능을 제공하는 모듈을 의미한다.
예시
고수준 모듈 ( 활용하는 로직 )
0. 가격 할인 계산
저수준 모듈 ( 실제 구현 로직 )
1. 고객정보 구한다.(RDBMS에서 고객 정보를 JPA로 구한다.)
2. 룰을 이용해서 할인 금액을 구한다. (Drools로 룰을 적용한다.)토비의 스프링 1장에 잘 나와있는 부분이다.
외부로 부터의 주입을 통해서 코드를 실행하도록 하는 것이다.
사용할 상위 계층에서 직접적인 로직을 구현하지 않고 실행할 추상메서드 즉 인터페이스를 생성하여 구현 코드를 작성하여 상위 계층에 주입 시킨다.코드를 수정 시 하위 계층 모듈 수정시 상위 계층은 영향이 없다.
인프라스트럭처 코드에 직접 의존하지 않도록 주의한다. 단 @Entity @Table @Transaction 등 편하게 사용이 가능 한 부분은 예외다.5. 도메인 영역 주요 구성 요소
엔티티
고유 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 주문, 회원, 상품과 같이 도메인의 고유한 개념을 표현한다.
도메인 모델의 데이터를 포함하여 해당 데이터와 관련된 기능을 함께 제공한다.밸류
고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현할 때 사용된다.
예) 금액, 주소 등이 밸류 타입이다.애그리거트
연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다. 예를 들어 주문과 관련된 Order 엔티티, OrderLine 밸류(주문 항목) , Orderer 밸류(주문자) 객체를 주문 애그리거트로 묶을 수 있다.
레포지토리
도메인 모델 영속성 처리 DBMS에 객체 로딩 혹은 저장하는 기능
도메인 서비스
특정 엔티티에 속하지 않은 도메인 로직 제공, 할인 금액 계산은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건이 이용되는데 도메인 로직을 어느 한 모델에서 구현 할 수 없을때 도메인 서비스에서 구현
6. 정리
1. 주문(도메인)은 하나의 큰 기능을 두고 수행하는 단위이다.
2. 주문(도메인)은 하위 도메인을 갖는다. 배송, 결제, 정산, 혜택 등
3. 도메인 모델(+패턴) 객체는 도메인에 해당되는 규칙, 기능들을 포함한다.
4. DDD는 응용 서비스 계층에서 도메인 모델 객체들의 규칙과 기능의 순서를 정한다.
5. (주문)애그리거트는 관련된 도메인을 주문 애그리거트 루트로 묶어서 관리해 여러 도메인의 상태를 변경하여 일관성을 유지한다.
6. 바운디드 컨텍스트는 하나의 도메인이 여러곳에서 사용되면 사용이 하는 도메인에 모델에 맞게 변경하여 적용
7. set 메서드를 사용하지 않고 상태변경 메서드를 유비쿼터스언어로 작성
8. 객체의 불변성과 참조 투명성 위해 도메인의 하위 밸류 값들은 기본 생성자를 protected로 걸고 도메인을 통해서 객체를 생성하도록 설정
7. 소스 출처
https://github.com/madvirus/ddd-start2/tree/main
'방법론' 카테고리의 다른 글
[방법론] 도메인 주도 개발 시작하기 DDD - 2 (0) 2023.12.15