본문 바로가기
Book/도메인 주도 개발 시작하기

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

by 프로그래 밍구 2023. 1. 13.

 

2.1 네 개의 영역

 '표현', '응용', '도메인', '인프타스트럭처'는 아키텍처를 설계할 때 출현하는 전형적인 네 가지 영역이다. 표현 영역은 사용자의 요청을 받아 응용 영역에 전달하고 응용 영역의 처리 결과를 다시 사용자에게 보여주는 역할을 한다. 스프링 MVC 프레임워크가 표현 영역을 위한 기술에 해당한다. 웹 애플리케이션의 표현 영역은 HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 적달하고 응용 영역의 응답을 HTTP 응답으로 변환하여 전송한다.

 

 표현 영역을 통해 사용자의 요청을 전달받은 응용 영역은 시스템이 사용자에게 제공해야 할 기능을 구현하는데 '주문 등록', '주문 취소', '상품 상세 조회'와 같은 기능 구현을 예로 들 수 있다. 응용 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다. 

 

 도메인 영역은 도메인 모델을 구현한다. 예를 들어 주문 도메인은 '배송지 변경', '결제 완료', '주문 총액 계산'과 같은 핵심 로직을 도메인 모델에서 구현한다.

 

 인프라스트럭처 영역은 논리적인 개념을 표현하기보다는 실제 구현 기술에 대한 것을 다룬다. 이 영역은 RDBMS 연동을 처리하고, 메시징 큐에 메시지를 전송하거나 수신하는 기능을 구현하고, 몽고DB나 레디스와 같은 데이터 연동을 처리한다. 이 영역은 SMTP를 이용한 메일 발송 기능을 구현하거나 HTTP 클라이언트를 이용해서 REST API를 호출하는 것도 처리한다. 

 도메인, 응용, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.

 

2.2 계층 구조 아키텍처

 네 영역을 구성할 때 많이 사용하는 아키텍처가 다음과 같은 계층 구조이다.

 

 

 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다. 예를 들어 표현 계층은 응용 계층에 의존하고 응용 계층이 도메인 계층에 의존하지만, 반대로 인프라스트럭처 계층이 도메인에 의존하거나 도메인이 응용 계층에 의존하지는 않는다. 상위 계층은 바로 아래의 계층에만 의존을 가져야 하지만 구현의 편리함을 위해 계층 구조를 유연하게 적용하기도 한다. 예를 들어 응용 계층은 외부 시스템과의 연동을 위해 더 아래 계층인 인프라스트럭처 계층에 의존하기도 한다. 응용 영역과 도메인 영역은 DB나 외부 시스템 연동을 위해 인프라스트럭처의 기능을 사용하므로 이런 계층 구조를 사용하는 것은 직관적으로 이해하기 쉬우나 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속된다는 점을 짚고 넘어가야 한다. 인프라스트럭처에 의존하면 '테스트 어려움'과 '기능 확장의 어려움(구현 방식을 변경하기 어려움)'이라는 두 가지 문제가 발생한다.

 

2.3 DIP

 가격 할인 계산을 하려면 고객 정보를 구해야 하고, 구한 고객 정보와 주문 정보를 이용해서 룰을 실행해야 한다. 여기서 CalculateDiscountService는 고수준 모듈이다. 고수준 모듈은 의미 있는 단일 기능을 제공하는 모듈로 CalculateDiscountService는 '가격 할인 계산'이라는 기능을 구현한다. 가격 할인 계산 기능을 구현하려면 고객 정보를 구해야 하고 룰을 실행해야 한다. 오른쪽 두 기능이 하위 기능이다. 저수준 모듈은 하위 기능을 실제로 구현한 것이다.

 그런데 고수준 모듈이 저수준 모듈을 사용하면 앞서 언급했던 구현 변경과 테스트가 어렵다는 문제가 발생한다. DIP(의존 역전 원칙)는 이 문제를 해결하기 위해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다. 바로 추상화한 인터페이스를 상속받아 구현하는 것이다.

 

 CalculateDiscountService는 '룰을 이용한 할인 금액 계산'을 추상화한 RuleDiscounter라는 고수준 모듈의 인터페이스를 의존하며 DIP를 적용할 수 있다. DIP를 적용하면 앞서 다른 영역이 인프라스트럭처 영역에 의존할 때 발생했던 두 가지 문제를 해소할 수 있다. 먼저 구현 기술 교체 문제를 보면, 고수준 모듈은 더 이상 저수준 모듈에 의존하지 않고 구현을 추상화한 인터페이스에 의존한다. 실제 사용할 저수준 구현 객체는 의존 주입을 이용해서 전달받을 수 있다. 그리고 구현 기술을 변경하더라도 저수준 구현 객체를 생성하는 코드만 변경하면 된다. 테스트 문제에서는 CalculateDiscountService는 저수준 모듈에 직접 의존하지 않기 때문에 Drools를 이용한 RuleDiscounter 구현 클래스가 없어도 스텁이나 모의 객체와 같은 테스트 목적의 대역을 사용하여 거의 모든 상황을 테스트할 수 있다.

 

2.3.1 DIP 주의사항

 DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함인데 DIP를 적용한 결과 구조만보고 저수준 모듈에서 인터페이스를 추출하는 경우가 있다. 하위 기능을 추상화한 인터페이스는 고수준 모듈에 위치하여야 한다.

 

2.3.2 DIP와 아키텍처

 인프라스트럭처 영역은 구현 기술을 다루는 저수준 모듈이고 응용 영역과 도메인 영역은 고수준 모듈이다. 아키텍처에 DIP를 적용하면 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존(상속)하는 구조가 된다. 인프라스트럭처에 위치한 클래스가 응용 영역에 정의한 인터페이스를 상속받아 구현하는 구조가 되므로 도메인과 응용 영역에 대한 영향을 주지 않거나 최소화하면서 구현 기술을 변경하는 것이 가능하다.

 

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

 

 도메인 영역을 구성하는 요소는 위와 같다.

 

2.4.1 엔티티와 밸류

 실제 도메인 모델의 엔티티와 DB 관계형 모델의 엔티티는 다르다. 이 두 모델의 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점이다. 예를 들어 주문을 표현하는 엔티티는 주문과 관련된 데이터뿐만 아니라 배송지 주소 변경을 위한 기능을 함께 제공한다. 그리고 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화해서 데이터가 임의로 변경되는 것을 막는다. 또 다른 차이점은 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다는 것이다. 예를 들어 주문자를 표현하는 Orderer는 밸류 타입으로 주문자 이름과 이메일 데이터를 포함할 수 있다. 반대로 RDBMS 같은 관계형 데이터베이스는 밸류 타입을 제대로 표현하기 힘들다.

 

2.4.2 애그리거트

 도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 애그리거트이다. 애그리거트는 관련 객체를 하나로 묶은 군집이다. 예를 들어 '주문', '배송지 정보', '주문자' 등 하위 모델을 하나로 묶어서 '주문'이라는 상위 개념으로 표현한 것이 애그리거트이다. 애그리거트는 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다. 애그리거트를 사용하는 코드는 애그리거트 루트가 제공하는 기능을 실행하고 애그리거트 루트를 통해서 간접적으로 애그리거트 내의 다른 엔티티나 밸류 객체에 접근한다. 이것은 애그리거트의 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화할 수 있도록 돕는다.

 

2.4.3 리포지터리

 리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다. 리포지터리 인터페이스는 도메인 모델 영역에 속하며, 실제 구현 클래스는 인프라스트럭처 영역에 속한다.

 

2.5 요청 처리 흐름

 사용자가 애플리케이션에 기능 실행을 요청하면 그 요청을 처음 받는 영역은 표현 영역이다. 스프링 MVC를 사용하여 웹 애플리케이션을 구현했다면 컨트롤러가 사용자의 요청을 받아 처리한다. 표현 영역은 사용자가 전송한 데이터 형식을 검사하고 데이터를 이용해서 응용 서비스에 기능 실행을 위임한다. 응용 서비스는 도메인 모델을 이용해서 기능을 구현한다. 기능 구현에 필요한 도메인 객체를 리포지터리에서 가져와 실행하거나 신규 도메인 객체를 생성해서 리포지터리에 저장한다. '예매하기'나 '예매 취소'와 같은 기능을 제공하는 응용 서비스는 도메인의 상태를 변경하므로 변경 상태가 물리 저장소에 잘 반영되도록 트랜잭션을 관리해야 한다.

 

2.6 인프라스트럭처 개요

 DIP에서 언급한 것처럼 도메인, 응용 영역에 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는 것이 시스템을 더 유연하고 테스트하기 쉽게 만들어준다. DIP의 장점을 해치지 않는 범위에서 응용 영역과 도메인 영역에서 구현 기술에 대한 의존을 가져가는 것은 나쁘지 않다. 좋은 예가 스프링의 @Transactional 애너테이션이다. 코드에서 스프링에 대한 의존을 없애는 것보다 @Transactional 한 줄로 트랜잭션을 처리하는 것이 좋을 수가 있다.

 

2.7 모듈 구성

 아키텍처의 각 영역은 별도 패키지에 위치한다. 도메인이 크면 하위 도메인으로 나누고 도메인 마다 별도 패키지를 구성한다. 도메인 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성한다. 모듈 구조를 얼마나 세분화해야 하는지에 대해 정해진 규칙은 없다.

댓글