FeGwan-Training / FeGwan

0 stars 0 forks source link

1. 도메인 기본개념 #43

Open HoFe-U opened 1 year ago

HoFe-U commented 1 year ago

Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/42

Originally posted by **HoFe-U** August 23, 2023 # 도메인 주도 개발(Domain-Driven-Design) > 이 책을 읽게된 경위는 먼저 기본적인 아키텍처 구조를 layered 밖에 모르고 JPA 를 공부하면서 Entity 나 Domain 쪽에대한 내용이 나왔기때문에 해당 부분에 대해 학습을 하고자 이책을 읽습니다. > ## 1. 도메인 모델 시작하기 **도메인(Domain) 이란?** - 소프트웨어로 해결하고자 하는 문제 영역, 즉 도메인(Domain) 에 해당한다. - 하나의 도메인은 여러개의 하위 도메인으로 나뉘어 질 수 있다. ## 2. 네 개의 영역(아키텍처) > 일단 아키텍처란 무엇일까?? **아키텍처** → 시스템 혹은 소프트웨어 구조의 상호작용 방식을 결정하는 체계적인 설계 > > > 어떤걸 생각하며 작성해야 할까? > > - 보안적 측면 > - 유지보수성 > - 패턴과 프레임워크 > > 등을 고려하면서 작성해야 할것같다. > **그럼 아키텍처를 설계하기위한 필수요소들은 무엇이 있을까?** **Application** **Presentation** **Domain** **Infrastructure** ![structure](https://user-images.githubusercontent.com/88470887/262562418-132bfe83-0554-4853-91d3-a22cd5c1e513.png ) 위의 4가지 영역이 필요하다. 상단의 그림처럼 구성되어 있는데 이를 하나씩 알아보고자 한다. ### Presentation 영역 > 상단의 그림을 보면 Presentation 영역(UI 영역 이라고도 불림)은 사용자의 요청을 받아 응용 영역에 전달하고 영역의 처리결과를 다시 사용자에게 보여주는 역할을 한다. > 기본적인 UI 부분이나 API end-point 부분이 해당영역을 대신한다고 볼 수 있다. 그러니까 Presentation 영역이 우리가 흔히 mvc 모델에서 말하는 controller 의 영역이라고 할 수 있다. ### Application 영역 > 상단의 그림을 보면 Application 영역은 사용자의 요청을 Presentation 영역에서 전달받아 처리하는 역할을 한다. > 기본적으로 주문 생성, 주문 취소 등 비즈니스 로직을 처리하는 역할을 한다. ### Domain 영역 > 상단의 그림에서 Application 영역이 Domain 으로 넘어가는 것을 볼 수 있다. Domain 영역이 `주문취소` 를 실행한다면 이를 직접적으로 로직을 수행하는것이 Domain 영역이다. > ### InfraStructure 영역 > 상단의 그림에는 나와 있지않지만 InfraStructure 영역은 구현 기술에 대한 것을 다룬다. 이 영역은 RDBMS 연동을 처리하거나 Message 처리를 한다. > 즉 데이터베이스의 Repository 영역, 외부 API 영역은 여기에 해당한다고 볼 수 있다. (그 외 Redis, Kafka 도 마찬가지다) ## 3. 계층 구조 아키텍처 (Layered Architecture) > 위의 4개의 영역을 구성할때 많이 쓰는 방법이 Layered Architecture 인데 이 구조는 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는 특징있다. > 자 이때 구현의 편리함을 위해 아래와 같이 구성을 하면 ![presentation](https://user-images.githubusercontent.com/88470887/262567212-d6c8b9c6-aa5a-4846-bde8-82e93e1d9f79.png ) 위의 그림을 보게 되면 편의를 위해서 infraStructure 에 의존적인걸 볼 수있다. 이렇게 되면 2가지 문제가 발생하는데 특정 Service 가 DB 모듈에만 의존하고 있으면 **DB모듈을 변경시에** 문제가 발생한다. 1. 테스트의 어려움 2. 기능 확장의 어려움 그럼 위와같은 2가지의 어려움이 발생할때 어떻게 해결할 수 있을까?? ### DIP(Dependency Injection Principal) > 일단 DIP 을 이해하기 전에 모듈의 종류 부터 이해하고자 한다. > - 고수준 모듈 - 일반적으로 ‘주문 취소’, ‘가격 할인 계산’ 등 비즈니스로직의 하나의 로직을 처리할 수 있으면 고수준의 모듈로 볼 수 있다. - 저수준 모듈 - 상위의 비즈니스로직을 처리하는거 즉 `RDBMS 는 JPA` 로 `Drools 로 룰을 적용` 등이 저수준의 모듈이라고 할 수 있다. 자 이제 2가지 모듈에 대한 정의를 알았으니 앞서 있던 문제를 해결해 보고자 한다. 1. 테스트의 어려움 2. 기능 확장의 어려움 위의 2가지문제를 해결하기 위해서는 DIP 를 통해 ```css 저수준 모듈이 고수준의 모듈을 의존하도록 변경하면된다. ``` 우리가 잘아는 추상화를 통해 injection 을 받으면된다. ```css public class CalculateDiscountService { private DroolsRuleEngine ruleEngine; public Money calculateDiscount(OrderLine orderLines, String customerId) { Customer customer = findCusotmer(customerId); MutableMoney money = new MutableMoney(0); facts.addAll(orderLines); ruleEngine.evalute("discountCalculation", facts); return money.toImmutableMoney(); } } ``` 위와같은 Service 가 있으면 해당 Service 는 `DroolsRuleEngine` 에 의존적인데 이를 아래와 같이 변경하는것이다. ```java public interface RuleDiscounter { public Money applyRules(Customer customer, List orderLines); } ``` 추상화를 시켜서 RuleDiscounter 를 만들고 ```java public class CalculateDiscountService { private CustomerRepository customerRepository; private RuleDiscounter ruleDiscounter; public Money calculateDiscount(OrderLine orderLines, String customerId) { Customer customer = customerRepository.findCusotmer(customerId); return ruleDiscounter.applyRules(customer, orderLines); } } ``` service 코드를 위와같이 변경하고 기존의 `DroolsRuleDiscounter` 가 `RuleDiscounter` 를 의존하게 변경을하면 된다. ```java public class DroolsRuleDiscounter implements RuleDiscounter{ private KieContainer kContainer; @Override public void applyRules(Customer customer, List orderLines) { ... } } ``` 그리고 기존의 `DroolsRuleDiscounter` 가 `RuleDiscounter` 을 상속받게 하면 된다. 이렇게되면 `CalculateDiscountService` 는 `Drools` 에 의존하는 코드가 없고 단지 `RuleDiscounter` 가 룰을 적용한다는 사실만 알고만 있으면 되게 만든다. 즉 고수준의 모듈이 `RuleDiscounter` 와 `CalculateDiscountService` 가 되는거다. 다시한번 정리하면 기존의 고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야하는데 이를 반대로 저수준 모듈이 고수준 모듈을 의존한다고 해서 이를 DIP 라고 부른다. ![module](https://user-images.githubusercontent.com/88470887/262571355-79959f16-3f7c-4615-8b3f-376353173d52.png ) ### DIP 주의사항 DIP를 잘못 생각하면 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아들일 수 있다. DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함인데 DIP를 적용한 결과 구조만 보고 아래 그림과 같이 저수준 모듈에서 인터페이스를 추출하는 경우가 있다. ![interface](https://user-images.githubusercontent.com/88470887/262579110-893c601b-b855-4511-8ec1-e8408d0b1a91.png ) 위와같은 경우가 잘못된 경우라고 볼 수 있다. 이유는 위의 구조는 구현 기술을 다루는 인프라스트럭처 영역에 의존하고 있다. 여전히 고수준 모듈이 저수준 모듈에 의존하고 있는것이다. 정리하자면 DIP 를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출하고 `CalculateDiscountService` 에서는 할인 금액을 구하기 위해 룰 엔진을 사용하는지 직접 연산을 하는지는 중요하지 않아진다. 단지 규칙에 따라 할인 금액을 계산하는것이 중요하다. 즉 ‘**할인 금액 계산**’을 추상화한 인터페이스는 저수준 모듈이 아닌 고수준 모듈에 위치한다. ![high-value](https://user-images.githubusercontent.com/88470887/262580000-c7f3403f-9ed1-40b0-9424-d469f9f0ea5b.png ) ## 4. 도메인 영역의 주요 구성요소 > 앞에서 네 영역에 대해 설명하면서 도메인 영역은 도메인의 핵심 모델을 구현한다고 말했는데 도메인 영역의 모델은 도메인의 주요 개념을 표현하며 핵심 로직을 구성한다. 이제 도메인 영역의 주요 구성요소에 대해 알아보고자 한다. > 1. **Entity (엔티티 or 엔터티) :** - 도메인 모델의 핵심 요소로 유일한 식별자(ID)를 가지고 있는 객체 - 데이터베이스에 영속적으로 저장되며, 비즈니스 규칙과 데이터를 함께 포함 - 주문(Order) , 고객(Customer) 등이 예시 2. **Value (Value Object (밸류, 밸류 오브젝트 )) :** - **Immutable** 하며, 여러 속성을 하나로 묶어 표현하는 객체 - Entity 와 달리 식별자가 없고, 동등성(Equality) 비교를 통해 동일성을 판단. - 주소(Address), 화폐 금액(Money) 등이 예시. 3. **Aggregate( 애그리거트 ) :** - 관련된 Entity 와 Value Object 의 그룹을 하나의 단위로 취급하는 개념 - 하나의 Aggregate 는 Root Entity 를 통해 내부 구조를 다룬다. - 즉 **Order entity, OrderLine Value, Orderer Value** 를 **‘Order’ Aggregate** 로 묶을 수 있다. 4. **Repository(리포지터리 or 레포지토리) :** - 도메인 객체(Domain 및 값 객체) 를 저장하고 조회하는 Interface. - 데이터베이스와 상호작용을 캡슐화하며, 도메인 로직과 infrastructure 을 분리. - OrderRepository 가 예시일수 있다. 5. **Domain Service (도메인 서비스) :** - 특정 Entity 에 속하지 않은 도메인 로직을 제공 - 즉 ‘할인 금액 계산’ 은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하는데 이렇게 되면 여러 Entity 와 Value 를 필요로 하면 Domain Service 에서 로직을 구현 - 복잡한 비즈니스 규칙이나 계산을 다루는데 사용됩니다. Layer 로 보면 아래처럼 볼 수 있다. ```java [ Domain Layer ] │ │ ┌─────────────────────────┐ │ │ Entity │ │ ├─────────────────────────┤ │ │ - id │ │ │ - properties │ │ └─────────────────────────┘ │ ▲ ▲ │ │ │ │ ┌─────────┐ ┌───────────┐ │ │ Value │ │ Aggregate │ │ │ Object │ ├───────────┤ │ │ │ │ - id │ │ └─────────┘ │ - entity │ │ │ - value │ │ └───────────┘ │ ▲ │ │ │ ┌─────────────────┐ │ │ Repository │ │ ├─────────────────┤ │ │ - save() │ │ │ - findById() │ │ └─────────────────┘ │ │ │ ┌─────────────────┐ │ │ Domain Service │ │ ├─────────────────┤ │ │ - processOrder()│ │ │ - calculateTotal│ │ └─────────────────┘ │ ``` 위에서는 주요구성 요소를 간단하게 알아봤는데 이제 세부적으로 한번 쪼개보고자 한다. ### 엔티티와 벨류 > 우리가 Entity 하면 흔히 말하는 DB 에서의 Entity 라고 많이들 생각하는것 같다 하지만 실상은 다르다고 볼 수 있다. > **DB Entity 와 Domain Entity 같나?** **다르다** → 이유는 Domain 모델의 Entity는 데이터와 함께 Domain 기능을 함께 제공하는 점이 있기때문이다. ```java public class Order { // 주문 도메인 모델의 데이터 private OrderNo number; private Orderer orderer; private ShippingInfo shippingInfo; ... // 도메인 모델 엔티티는 도메인 기능도 함께 제공 public void changeShippingInfo(ShippingInfo newShippingInfo) { verifyNotYetShipped(); setShippingInfo(newShippingInfo); Events.raise(new ShippingInfoChangedEvent(number, newShippingInfo)); } } ``` 상단의 코드를 보면 도메인 모델의 Entity 는 데이터와 함께 Domain 기능을 함께 제공한다. 즉 단순히 데이터를 담고있는것이 아니라 데이터와 함께 기능을 제공하는 객체라고 할 수 있다. 두 번째는 도메인 모델의 엔티티는 두 개이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다. 상단의 코드를 보면 Orderer 는 Value 임으로 다음과 같이 주문자 이름과 이메일을 데이터를 표현할 수 있다. 아래와 같이 말이다. ```java public class Orderer { private String name; private String email; ... } ``` 그리고 Value Object 경우 불변으로 구현할 것을 권장하는데 이는 Value Type 데이터를 변경할 때는 객체 자체를 완전히 교체한다는 것을 의미한다. ### 애그리거트 > 도메인이 커질수록 개발할 도메인 모델도 커지면서 많은 Entity 와 Value 가 나온다. 이때 Entity 와 Value 가 많아질수록 모델은 더 복잡해 진다. 이를 해결하기위한게 Aggregate 이다. > 애그리거트는 관련된 객체를 하나로 묶은 군집이다. 주문 이라는 도메인 개념은 `주문` , `배송지 정보` , `주문자`,`주문 목록`, `총 결제 금액`의 하위 모델로 구성된다. 이 하위 개념을 표현한 모델을 하나로 묶어서 `주문`이라는 상위 개념으로 표현할 수 있다. ![aggregate](https://user-images.githubusercontent.com/88470887/262591717-8140cd9f-6aab-4750-9ecb-193ce2193754.png ) 즉 군집에 속한 객체를 관리하는 Root Entity 를 같는다고 볼 수 있다. 이때 Root Entity 는 애그리거트에 속해 있는 Entity 와 Value 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다. ### 리포지터리 > 도메인 객체를 지속적으로 사용하려면 RDBMS, NOSQL, File 과 같은 물리적인 저장소를 활용해서 도메인 객체를 저장해야 한다. 이를 위한 모델이 Repository 이다. > Repository 는 일반적으로 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다. 예시는 아래와 같다. ```java public interface OrderRepository { Optional findById(OrderNo id); void save(Order order); } ``` `OrderRepository` 의 메서드를 보면 대상을 찾고 저장하는 단위가 애그리거트 루트인 `Order` 인 것을 알 수 있다. `Order` 는 애그리거트에 속한 모든 객체를 포함하고 있으며 결과적으로 애그리거트 단위로 저장 및 조회한다. 이때 도메인 모델 관점에서는 `OrderRepository` 는 도메인 객체를 영속화하는 데 필요한 기능을 추상화한 것으로 고수준 모듈에 속한다. 특정 기술을 이용해서 OrderRepository 를 구현한 클래스는 저수준 모듈로 인프라스트럭처 영역에 속한다. ### 인프라스트럭처 개요 > 인프라스트럭처(infrastructure) 는 표현 영역, 응용 영역, 도메인 영역을 지원한다. 도메인 객체의 영속성 처리, 트랜잭션, SMTP 클라이언트, REST 클라이언트 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원한다. 앞서 DIP 에서 도메인 영역과 응용 영역에서 인프라스트럭처의 기능을 직접 상요하는것 보다 이 두 영역에서 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는 것이 시스템을 더 유연하고 테스트하기 쉽게 만든다. > **그럼 위에서 말한대로 무조건적으로 인프라스트럭처에 대한 의존을 없애야할까 ? ? ?** → 아니다. 구현의 편리함을 버리고서 DIP 가 주는 장점을 갖는 다면 모순인게 DIP의 장점을 해치지 않는 범위 에서 기술에 대한 의존을 가져가는것은 나쁘지 않다고 본다. 뭐 큰 예시가 있다면 `@Transactional` 일 것이다. `@Transcational` 로 처리하면 한 줄로 트랜잭션을 처리할 수 있는데 스프링에 대한 의존성을 없애려고 복잡한 설정을 사용할 필요가 없는것이다. ### 모듈 구성 > 패키지 구성 규칙에 정답이 있는것은 아니지만 영역별로 모듈이 위치할 패키지를 구성할 수 있다. > 한단의 그림을 보면 특정 도메인 별로 기능들을 분리한것을 볼수있다. ![domain-1](https://user-images.githubusercontent.com/88470887/262599127-27747bb5-912a-415b-ad65-8519df9b6af7.png ) 다른 예시의 경우에는 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성할 경우. 예를 들어 카탈로그 하위 도메인이 상품 애그리거트와 카테고리 애그리거트로 구성될 경우 구성될 수 있다. ![domain-2](https://user-images.githubusercontent.com/88470887/262599904-cc76070c-f373-422b-8bf5-281991685a97.png ) 도메인이 복잡해지면 도메인 모델과 도메인 서비스를 다음과 같이 별도 패키지 위치시킬 수 있다. - com.myshop.order.domain.order : 애그리거트 위치 - com.myshop.order.domain.service : 도메인 서비스 위치 응용 서비스도 다음과 같이 구분할 수 있다. - com.myshop.catalog.application.product - com.myshop.catalog.application.category 정리하면 모듈 구조를 얼마나 세분화해야 하는지에 대한 정해진 규칙은 없지만 한 패키지에 너무많은 타입이 몰려 코드를 찾을때 불편할 정도만 아니면 된다. 화자의 생각은 10~15 개 미만으로 타입 개수를 유지하려면 좋다고하는데 아직까지 15 개를 넘어갈 정도는 경험해보지 못한것 같다.