2장 - 아키텍처 개요

alsdl0629·2023년 9월 20일
0

도메인 주도 개발

목록 보기
2/2
post-thumbnail

최범균님의 도메인 주도 개발 시작하기를 읽고 관련 내용과 느낀 점을 정리해 보려고 합니다!


네 개의 영역

표현 영역

사용자의 요청을 받아 응용 영역에 전달하고 응용 영역의 처리 결과를 사용자가 이해할 수 있는 형식으로 변환하여 응답한다.

HTTP 요청을 서비스가 요구하는 형식의 객체 타입으로 변환해 전달하고, 결과를 JSON 형식으로 변환해서 HTTP 응답으로 웹 브라우저에 전송한다.

응용 영역

응용 영역은 사용자에게 제공해야 할 기능을 구현한다.
ex) 주문 등록, 주문 취소, 상품 조회 등

기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다.

public vlass CancelOrderService {
	
    @Transactional
    public void cancelOrder(String orderId) {
    	Order order = findOrderById(orderId)
        	.orElseThrow() // 예외 처리
        order.cancel(); // 주문 도메인 모델을 사용
    }
}

응용 영역은 도메인 모델을 이용해서 사용자에게 제공할 기능을 구현한다.
실제 도메인 로직 구현은 도메인 모델에 위임한다.

➡️ 도메인 모델은 SRP 지킴, 응집도 🆙

도메인 영역

도메인 영역은 도메인 모델을 구현한다.
도메인 모델 시작하기에서 도메인 모델이 도메인 영역에 위치한다.

도메인 모델은 도메인의 핵심 로직을 구현한다.
ex) 배송지 변경, 결제 완료, 주문 총액 계산

인프라스트럭처 영역

인프라스트럭처 영역은 구현 기술을 다룬다.
ex) RDBMS 연동, 메시징 큐에 메시지를 전송 및 수신 등

논리적인 개념을 표현하기보다는 실제 구현을 다룬다.


계층 구조 아키텍처


상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다.


계층 구조 아키텍처의 문제점

표현, 응용, 도메인 계층이 구현 기술을 다루는 인프라스트럭처 계층에 종속된다는 점이다.

public class DroolsRuleEngine {
	private KieContainer kContainer;
    
    public DroolsRuleEngine() {
    	KieService ks = KieService.Factory.get();
        kContainer = ks.getKieClasspathContainer();
    }
    
    public void evalute(String sessionName, List<?> facts) {
    	KieSession kSession = kContainer.newKieSession(sessionName);
        try {
        	facts.forEach(x -> kSession.insert(x));
            kSession.fireAllRules();
        } finally {
        	kSession.dispose();
        }
    }
}
public class CalculateDiscountService {
	private DroolsReluEngine reluEngine;
    
    public CalculateDiscountService() {
    	reluEngine = new DroolsReluEngine();
    }
    
    public Money calculateDiscount(List<OrderLine> orderLines, String customerId) {
    	Customer customer = findCustomer(customerId);
        
        MutableMoney money = new MutableMoney(0);
        List<?> facts = Arrays.asList(customer, money);
        facts.addAll(orderLines);
	- CalculateDiscountService를 테스트하려면 ReluEngine이 완벽하게 동잭해야지만 CalculateDiscountService를 올바르게 동작하는지 확인할 수 있다.
        reluEngine.evalute("discountCalculation", facts); 
        return money.toImmutableMoney();
    }
    ...
}

응용 영역은 가격 계산을 위해 인프라스트럭처 영역의 DroolsReluEngine를 사용한다.


해당 코드는 두 가지 문제가 있다.

  1. CalculateDiscountService 테스트하기 어렵다.
    • CalculateDiscountService를 테스트하려면 ReluEngine이 완벽하게 동작해야지만 CalculateDiscountService를 올바르게 동작하는지 확인할 수 있다.
  1. 구현 방식을 변경하기 어렵다.
    • 해당 코드는 "discountCalculation", MutableMoney 등 Drools라는 인프라스트럭처 영역의 기술에 완전하게 의존하고 있다.

인프라스트럭처에 의존하면 테스트 어려움기능 확장의 어려움이라는 두 가지 문제가 발생한다.
➡️ 해답은 DIP에 있다!


DIP (의존 역전 원칙)

고수준 모듈은 의미 있는 단일 기능을 제공하는 모듈로 CalculateDiscountService는 가격 할인 계산라는 기능을 구현한다.

고수준 모듈의 기능을 구현하려면 여러 하위 기능이 필요하다.
저수준 모듈은 하위 기능을 실제로 구현한 것이다.
ex) JPA를 사용해 고객 정보 가져오는 모듈과 Drools(규칙을 정해 연산을 수행)로 룰을 실행하는 모듈

DIP는 테스트 어려움, 기능 확장의 어려움을 해결하기 위해 인터페이스를 사용해서 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다.


DIP는 쉽게 이야기해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다.


CalculateDiscountServic는 Drools로 구현했는지 자바로 직접 구현했는지 중요하지 않다.
고객 정보와 구매 정보에 룰을 적용해서 할인 금액을 구한다라는 것만 중요할 뿐이다.

public interface RuleDiscounter {
	Money applyRules(Customer customer, List<OrderLine> orderLines);
}
public class CalculateDiscountService {
	private RuleDiscounter ruleDiscounter;
    
    // 의존성 주입
    public CalculateDiscountService(RuleDiscounter ruleDiscounter) {
    	this.ruleDiscounter = ruleDiscounter;
    }
    
    public Money calculateDiscount(List<OrderLine> orderLines, String customerId) {
    	Customer customer = findCustomer(customerId);
        return ruleDiscounter.applyRules(customer, orderLines);
    }
}

CalculateDiscountService는 더 이상 구현 기술에 의존하지 않고 구현 기술을 추상화한 인터페이스에 의존한다.


구현 기술 교체 문제

고수준 모듈은 구현을 추상화한 인터페이스에 의존하기 때문에 실제 저수준 객체는 의존 주입을 이용해 전달받을 수 있다.
구현 기술을 변경하더라도 고수준 모듈은 변경되지 않는다. ➡️ OCP 지킴


테스트하기 어려움

저수준 모듈을 의존했을 때는 직접 구현 기술을 만들기 전까지는 테스트할 수 없었지만 인터페이스가 중간에 생겼으므로 Mock(가짜) 객체를 사용해서 테스트가 가능하다. ➡️ 구현 객체가 없어도 테스트 가능


DIP 주의사항

Bad


DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해야 한다.

Good



DIP를 적용하면 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존(상속)한다.
이런 구조는 응용 영역과 도메인 영역에 영향을 최소화하면서 구현체를 변경하거나 추가할 수 있다.
ex) JPA(구현체)에서 MyBatis(구현체)로 갈아끼우기 쉽다.


이런 장점이 있는 DIP이지만 항상 적용할 필요는 없다. 무조건 적용하려고 시도하지 말고 DIP의 이점을 얻는 수준에서 적용 범위를 생각해보는게 좋다.


도메인 영역의 주요 구성요소

엔티티

고유의 식별자를 갖는 객체로 라이프 사이클을 갖는다.
ex) 상품, 주문, 회원 등

도메인 모델 엔티티와 DB 테이블의 엔티티는 다르고, 2가지 차이점이 있다.

  1. 도메인 모델 엔티티는 데이터도 담고 데이터와 함께 기능을 제공하는 객체이다.
  2. 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해 표현할 수 있다. 도메인을 잘 이해할 수 있도록 돕는다.

밸류

고유의 식별자를 갖지 않는 객체로 개념적으로 하나인 값을 표현할 때 사용된다.
ex) 주소, 금액 등


애그리거트

연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다.
예를 들어 주문, 배송지 정보, 주문자, 주문 목록, 총 결제 금액의 하위 개념을 하나로 묶어서 주문이라는 상위 개념으로 표현할 수 있다.


개별 객체 간의 관계가 아닌 애그리거트 간의 관계로 도메인 모델을 이해하고 구현하며, 이를 통해 큰 틀에서 도메인 모델을 관리할 수 있다. ➡️ 복잡한 도메인 모델을 관리하는 데 도움이 된다.

애그리거트는 객체를 관리하는 루트 엔티티를 갖는다.
루트 엔티티는 애그리거트가 구현해야 할 기능을 제공한다.

애그리거트를 사용하는 코드는 애그리거트 루트가 제공하는 기능을 실행하고 루트엔티티를 통해 간접접으로 애그리거트 내의 다른 엔티티나 밸류 객체에 접근한다.


리포지터리

도메인 모델의 영속성을 처리한다.
엔티티나 밸류가 요구사항에서 도출된 도메인 모델이라면 리포지터리는 구현을 위한 도메인 모델이다.

애그리거트 단위로 저장하고 조회한다.
도메인 모델 관점에서 리포지터리는 도메인 객체를 영속화하는 데 필요한 기능을 추상화해 고수준 모듈에 속한다.

리포지터리 인터페이스는 도메인 모델 영역에 속하며, 실제 구현 클래스는 인프라스트럭처 영역에 속한다.

응용 서비스와 리포지터리는 밀접한 연관이 있다.

  • 응용 서비스는 필요한 도메인 객체를 구하거나 저장할 때 리포지터리를 사용한다.
  • 응용 서비스는 트랜잭션을 관리하는데, 트랜잭션 처리는 리포지터리 구현 기술의 영향을 받는다.

도메인 서비스

특정 엔티티에 속하지 않은 도메인 로직을 제공한다.
예를 들어 할인 금액 계산은 상품, 쿠폰, 회원 등급, 구매 금액 등 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현한다.


요청 처리 흐름


표현 영역은 사용자로부터 받은 데이터를 형식이 올바른지 검사하고 응용 서비스에 기능 실행을 위임한다.

응용 서비스는 도메인 모델을 이용해서 기능을 구현한다.
응용 서비스는 도메인의 상태를 변경하므로 변경 상태가 저장소에 올바르게 반영되도록 트랜잭션을 관리해야 한다.


인프라스트럭처 개요

구현의 편리함은 DIP의 장점(변경과 테스트하기 쉬움)만큼 중요하다.
DIP의 장점을 해치지 않는 범위에서 응용 영역과 도메인 영역에서 구현 기술에 대한 의존을 가져가는 것도 괜찮다.

@Transactional은 한 줄로 트랜잭션 처리 가능하다.
여기서 스프링 의존을 없애 @Transactional을 사용하지 못하면 테스트하기 더 어려워지고 유연하지 못한 코드를 작성하게 될 것 이다.
결국, 설정만 복잡해지고 개발 시간만 늘어난다.

표현 영역은 항상 인프라스트럭처 영역과 쌍을 이룬다.


모듈 구성


영역별로 별도 패키지로 구성한다.


도메인이 크면 하위 도메인 별로 패키지를 나눈다.


도메인 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성한다.

애그리거트, 모델, 리포지터리는 같은 패키지에 위치한다.
Order, OrderLine, OrderRepository 등은 com.myshop.order.domain

도메인이 복잡하면 도메인 모델과 도메인 서비스를 별도 패키지에 위치시킬 수도 있다.

  • com.myshop.order.domain.order : 애그리거트
  • com.myshop.order.domain.service : 도메인 서비스

모듈 구조 세분화에 대해 정해진 규칙은 없다. 한 패키지에 너무 많이 몰려 코드를 찾을 때 불편한 정도만 아니면 된다. 10 ~ 15개 미만으로 개수가 적당하다.


느낀점

DIP의 개념만 알고 있었는데 사용하는 이유를 알게 되었습니다.
DIP가 무적인 줄 알았지만, 상황에 맞게 사용해야 한다는 걸 깨달았습니다.
좋은 설계(변경, 테스트하기 쉬움)에 대해 다시 한번 고민하게 되었습니다.

profile
인풋보다 아웃풋

0개의 댓글