Backend

[도메인 주도 개발 시작하기] Ch2. 아키텍처 개요

anxi 2024. 9. 10. 15:10

4개의 영역

표현, 응용, 도메인, 인프라스트럭처 는 아키텍처를 설계할 때 출현하는 전형적인 4가지 영역이다.

  • 표현 : 사용자의 요청을 받아 응용 영역에 전달 및 처리 결과를 사용자에게 보여주는 역할
    • 스프링 MVC 프레임워크가 표현 영역을 위한 기술에 해당
  • 응용 : 시스템이 사용자에게 제공해야 할 기능 구현
    • 기능을 구현하는데 도메인 영역의 도메인 모델을 사용한다.
public class CancelOrderService {
	
	@Transactional
	public void cancelOrder(String orderId) {
		Order order = findOrderById(orderId);
		if (order == null) throw new OrderNotFoundException(orderId);
		// 도메인 메서드 사용
		order.cancel();
	}
	...
}

 

상기 코드에서 보다시피, Service의 cancelOrder 메소드에서는 로직을 직접 수행하지 않고 도메인 모델에 로직 수행(도메인 메소드)을 위임한다.

 

응용 -> 도메인 계층

  • 도메인
    • 도메인 모델을 구현한다. 이때 도메인의 핵심 로직을 구현한다.
  • 인프라스트럭처
    • 구현 기술에 대한 것을 다룬다.
    • DB연동, 메시징 큐에 메시지 전송, 수신 등 실제 구현을 다룬다.

 

인프라스트럭처

표현 → 응용 → 도메인 → 인프라스트럭처의 계층 구조를 따른다.

계층 구조를 엄격하게 적용하면 상위 계층은 바로 아래의 계층에만 의존을 가져야 하지만 구조의 편리함을 위해 계층 구조를 유연하게 적용하기도 한다.

public class GetOrderService {

  private OrderRepository orderRepository;
  
  public GetOrderService(OrderRepository orderRepository) {
	  this.orderRepository = orderRepository;
	}
	
  ...
  public Order getOrder(String orderId) {
    ...
	Order order = orderRepository.findById(orderId);
  }
 }

 

위처럼 인프라 단에서의 repository를 가지고 도메인 단에서 가져오는 것이 아닌 인프라 단에서 가져온다.

  • 하지만 인프라스트럭처 단에서 외부 시스템 연동을 통해 가져온 기능을 응용 계층에서 사용하면 어떨까?
    • 응용 계층에서 동작은 하겠지만, 해당 서비스만을 테스트하기 어렵다.
      • 인프라 단에서의 외부 시스템에 대한 테스트도 같이 이뤄져야 한다.
    • 구현 방식을 변경하기 어렵다.

DIP(Dependency Inversion Principle)

고수준 모듈과 저수준 모듈

 

  • 고수준 모듈 : 의미 있는 단일 기능을 제공하는 모듈
    • CalculateDiscountService는 가격 할인 계산 이라는 기능 구현
    • 고수준 모듈의 기능을 구현하려면 여러 하위 기능 필요 → 저수준 모듈
  • 고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야 한다.
  • 고수준 모듈이 저수준 모듈을 사용하면 응용 → 인프라로 계층 구조에서의 문제점(구현 변경, 테스트 어려움)이 발생한다.

DIP는 위 문제를 해결하기 위해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다.

 

저수준 모듈이 고수준 모듈에 의존하기 위해서 추상화한 인터페이스를 사용한다.

  • 이전 코드 (계층 구조의 문제가 발생했던)
public class CalculateDiscountService {
	private DroolsRuleEngine ruleEngine;
	
	public Money calculateDiscount(OrderLine orderLines, String customerId) {
		Customer customer = findCustomer(customerId);
		
		// Drools 의존
		MutableMoney money = new MutableMoney(0);
		List<?> facts = Arrays.asList(customer, money);
		facts.addAll(orderLines);
		ruleEngine.evalute("discountCalculation", facts);
		return money.toImmutableMoney();
	}
	...
}
  • DIP 적용 코드
public interface RuleDiscounter {
	Money applyRules(Customer customer, List<OrderLine> orderLines);
}

public class DroolsRuleDiscounter implements RuleDiscounter {
	private KieContainer kContainer;
	
	...
	
	@Override
	public Money applyRules(Customer customer, List<OrderLine> orderLines) {
		...
	}
}
public class CalculateDiscountService {
	private RuleDiscounter ruleDiscounter;
	
	public Money calculateDiscount(List<OrderLine> orderLines, String customerId) {
		Customer customer = findCustomer(customerId);
		return ruleDiscounter.applyRules(customer, orderLines);
	}
	...
}

 

DIP 적용 서비스에서는 Drools에 의존하는 코드가 없다.

즉, Drools 관련 코드를 이해할 필요 없이 "할인 계산하는 기능이 구현하는구나" 라고 인지하고 넘어갈 수 있다.

 

 

DIP를 적용한 고수준 모듈과 저수준 모듈

 

따라서 룰을 이용한 할인 금액 계산 의 기능을 가진 RuleDiscounter 인터페이스는 고수준 모듈에 속한다.

DroolsRuleDiscounter는 고수준의 하위 기능인 RuleDiscounter를 구현한 것이므로 저수준 모듈에 속한다.

위와 같이 저수준 모듈이 고수준 모듈에 의존하게 되는데, 이를 DIP(의존 역전 원칙)라고 부른다.

DIP 주의사항

  • 단순히 인터페이스와 구현 클래스를 분리하는 것이 아니다.

올바르게 적용된 DIP와 이에 맞는 계층 구조


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

  • 엔티티 (ENTITY)
    • 고유의 식별자를 갖는 객체, 라이프 사이클을 가진다.
    • 도메인의 고유한 개념을 표현한다.
    • 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.
  • 밸류 (VALUE)
    • 고유의 식별자를 갖지 않는 객체
    • 개념적으로 하나인 값을 표현할 때 사용
    • 엔티티의 속성 뿐만 아니라 다른 밸류 타입의 속성으로도 사용된다.
  • 애그리거트 (AGGREGATE)
    • 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것
    • 주문과 관련된 Order 엔티티, OrderLine 밸류, Orderer 밸류를 주문 애그리커트로 묶을 수 있다.
  • 리포지터리 (REPOSITORY)
    • 도메인의 영속성 처리
  • 도메인 서비스 (DOMAIN SERVICE)
    • 특정 엔티티에 속하지 않은 도메인 로직 제공
    • 할인 금액 계산 은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하는데 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현한다.

도메인 모델의 엔티티 VS DB 모델의 엔티티

차이

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

애그리거트

  • 여러 도메인과 밸류 객체가 많아져 도메인 모델이 복잡해지면 개발자는 전체 구조가 아닌 한 개 엔티티와 밸류에만 집중하는 상황 발생
  • 상위 수준에서 모델을 관리하지 않고 개별 요소에만 초점을 맞추면 큰 틀에서 모델을 관리할 수 없는 상황에 처하기 쉽다.
  • 관련 객체를 하나로 묶은 군집
  • 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다.
    • 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다.

 리포지토리

  • 엔티티, 밸류는 요구사항에서 도출되는 도메인 모델 ↔ 구현을 위한 도메인 모델 : 리포지토리
  • 리포지토리는 애그리거트 단위로 도메인 객체를 저장하고 조회
  • 만약 응용 계층에서 사용하는 코드는 애그리거트 단위로 저장, 조회하고 기능을 실행한다.
  • 리포지토리 인터페이스는 도메인 모델 영역에 속하며, 실제 구현 클래스는 인프라스트럭처 영역에 속한다.


요청 처리 흐름

  • 표현 영역
    • 사용자가 전송한 데이터 형식이 올바른지 검사하고, 문제가 없다면 데이터를 이용해서 응용 서비스에 기능 실행을 위임한다.
    • 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달한다.
  • 응용 영역
    • 응용 서비스는 도메인 모델을 이용해서 기능을 구현한다.
    • 구현에 필요한 도메인 객체 리포지터리에서 가져와 실행하거나 신규 도메인 객체를 생성해서 리포지터리에 저장한다.
    • @Transactional 애너테이션을 이용해 트랜잭션을 처리할 수 있다.
  • 인프라스트럭처 영역
    • 표현, 응용, 도메인 영역을 지원한다.

모듈 구성

아키텍처의 각 영역은 별도 패키지에 위치한다.

  • 표현 계층 : ui
  • 응용 계층 : application
  • 도메인 계층 : domain
  • 인프라스트럭처 계층 : infrastructure