# 2. 아키텍처 개요
# 2.1 네 개의 영역
# 표현
- 웹 브라우저 ⇆ 표현
- 응용 영역이 필요로 하는 형식으로 HTTP 요청을 변환해서 전달
- 응용 영역의 응답을 HTTP 응답으로 변환하여 전송
# 응용
- 응용 ⇆ 도메인(엔티티, 값)
- 기능을 구현하기 위해 도메인 영역의 도메인 모델 사용
public class CancelOrderService {
@Transactional
public void cancelOrder(String orderId) {
Order order = findOrderById(orderId);
if (order == null) throw new OrderNotFoundException(orderId);
order.cancel();
}
}
- 위 코드처럼 도메인 모델에 로직 위임
# 도메인
- 도메인의 핵심 로직 구현
- ex) 배송지 변경, 주문 취소 등
# 인프라스트럭처
- 구현 기술 처리
- ex) RDBMS 연동, 메시징 큐에 메시지 송/수신 등
- 논리적인 개념 표현보다는 실제 구현
# 정리
- 각 계층은 구현 기술을 사용한 코드를 직접 만들지 않고
- 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능 개발
# 2.2 계층 구조 아키텍처
# 계층 구조의 아키텍처 구성
- 표현 → 응용 → 도메인 → 인프라스트럭처
- 상위 계층에서 하위 계층으로의 의존만 존재해야
- 하지만 구현의 편리함을 위해 계층 구조 유연하게 적용하기도 함
- ex) 외부 시스템과의 연동을 위해 응용 계층이 인프라스트럭처에 의존하기도 함
- 그러나 만약 다른 구현 기술을 사용하게 되면 코드의 많은 부분을 고치게 됨
# 인프라스트럭처에 의존하면?
- 테스트 어려워짐
- 기능 확장 어려워짐 → DIP로 해결
# 2.3 DIP
- 고수준 모듈 (응용 영역)이 저수준 모듈 (인프라스트럭처) 모듈에 의존하도록 바꾸려면 추상화한 인터페이스 사용
# ex
public interface RuleDiscounter {
Money applyRules(Customer customer, List<OrderLine> orderLines);
}
@AllArgsConstructor
public class CalculateDiscountService {
private RuleDiscounter ruleDiscounter;
public Money calculateDiscount(List<OrderLine> orderLines, String customerId) {
Customer customer = findCustomer(customerId);
return ruleDiscounter.applyRUles(customer, orderLines);
}
}
→ 룰 적용을 구현한 클래스는 RuleDiscounter 인터페이스를 상속받아 구현
- 저수준 모듈(RuleDiscounted의 구현체)이 고수준 모듈(RuleDiscounter 인터페이스)에 의존하게 됨 (DIP, Dependency Inversion Principle, 의존 역전 원칙)
# 장점
- 구현 기술 교체가 쉬워짐
- 테스트하기 쉬워짐 (Mock, Stub 등 주입)
# DIP 주의사항
- 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출하기
Q. 그럼 인프라스트럭처 계층을 추상화한 인터페이스의 정의 위치는 응용 패키지에 두어야 함?
# DIP와 아키텍처
- 아키텍처에 DIP를 적용하면

- 위 사진과 같이 인프라 스트럭처 영역이 응용 영역과 도메인 영역에 의존하게 됨 → 인프라스트럭처에 위치한 클래스가 도메인과 응용 영역에 영향을 주지 않거나 최소화하면서 구현 기술을 변경하는 것이 가능해짐
[OrderService -> Notifier(interface)] (응용) <- EmailNotifier
Q. 여기서(P.78) SMS로도 알림이 가야한다 라는 요구사항이 추가되면 Notifier 인터페이스를 인프라스트럭처에 구현하면 된다는걸 알겟는데 그럼 둘다 전송한다 이건 어떻게 구현하지? 여러 Notifier를 한 번에 다루는 Composite 패턴 적용 스프링 같은 경우는 그럼 어떤 빈을 받을 지 어케 알지 Qualifier나 Primary 사용 혹은 @Bean 사용?
→ 무조건 DIP 를 적용할 필요는 없고 구현 기술에 의존적인 코드를 도메인에 일부 포함하는게 효과적일 때도 있고, 추상화 대상이 잘 안떠오를 수도 있음. DIP의 이점을 얻는 수준에서 적용 범위 검토해보기. 오히려 오버 엔지니어링이 될 수도 있음
- 구현체가 한 종류만 존재하거나, 기술 변경 가능성이 극히 낮은 경우
- 추상화를 도입하면 오히려 코드가 과도하게 복잡해지고 생산성이 떨어지는 경우
- 추상화 대상의 인터페이스 자체가 명확하지 않거나, 불필요하게 느껴지는 경우
# 2.4 도메인 영역의 주요 구성요소
# 도메인 영역의 주요 구성요소
| 요소 | 설명 |
|---|---|
| 엔티티 (ENTITY) | 고유의 식별자를 갖는 객체. 도메인의 고유한 개념 표현. 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공 |
| 밸류 (VALUE) | 고유의 식별자를 갖지 않는 객체. 주소나 금액과 같은 개념적으로 하나인 값. 다른 밸류 타입의 속성으로도 사용할 수 있음 |
| 애그리거트 (AGGREGATE) | 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것. Order 엔티티 + OrderLine 밸류 + Orderer 밸류 객체를 '주문' 애그리거트로 묶을 수 있음 |
| 리포지터리 (REPOSITORY) | 도메인 모델의 영속성을 처리 |
| 도메인 서비스 (DOMAIN SERVICE) | 특정 엔티티에 속하지 않은 도메인 로직 제공. '할인 금액 계산'은 상품, 쿠폰, 회원 등금, 구매 금액 등 다양한 조건을 이용해서 구현하는데, 이때 도메인 서비스에서 로직 구현 |
# 2.4.1 엔티티와 밸류
# 도메인 모델의 엔티티
- 데이터와 함께 기능을 제공하는 객체
- 도메인 관점에서 기능 구현 및 기능 구현을 캡슐화
- 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현
# 애그리거트
- 관련 객체를 하나로 묶은 군집
- ex) 주문 = 주문 + 배송지 정보 + 주문자 + 주문 목록
- 도메인 모델에서 전체 구조를 이해하는데 도움이 됨
- 루트 엔티티가 애그리거트가 구현해야 할 기능 제공

- 애그리거트 루트인 Order가 애그리거트에 속한 객체 관리
- ex) 배송지 정보를 변경할 때에는 Order가 구현한 도메인 로직을 항상 따르도록 구현
# 리포지터리
- 애그리거트 단위로 도메인 객체를 저장 / 조회
public interface OrderRepository {
Order findByNumber(OrderNumber number);
}
public class ChangeOrderAddressService {
private OrderRepository orderRepository;
public void changeAddress(OrderNumber number, Address address) {
Order order = orderRepository.findByNumber(number);
if (order == null) throw new NoOrderException(number);
order.changeAddress(address);
}
}
- 응용 서비스는 의존 주입과 같은 방식으로 실제 리포지터리 객체에 접근

# 2.5 요청 처리 흐름

- 응용 서비스에서 도메인 상태 변경 시 변경 상태가 올바르게 반영되도록 트랜잭션을 잘 관리해야 함
# 2.6 인프라스트럭처 개요
- 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능 지원
- ex) 도메인 객체의 영속성 처리, 트랜잭션, SMTP, REST 클라이언트 등
- 응용 / 도메인 영역에서 정의한 인터페이스를 인프라스트럭처에서 구현하는게 더 유연하고 테스트하기 쉬워짐
- 그렇다고 무조건 인프라스트럭처에 대한 의존을 없앨 필요는 없음
- ex) 엔티티단에
@Entity,@Table과 같은 JPA 전용 애너테이션 사용 - 결국 (변경의 유연함) + (테스트 쉬움) + (구현의 편리함) 셋을 모두 고려해서 적당한 선에서 구현하기
- ex) @Transactional 애너테이션
# 2.7 모듈 구성
- 도메인이 크면 하위 도메인 별로 모듈 나누기
- 도메인 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지 구성

- 애그리거트, 모델, 리포지터리는 같은 패키지에 위치
- 도메인이 복잡하면 도메인 모델과 도메인 서비스를 별도 패키지에 위치시킬 수 있음
com.myshop.order.domain.order // 도메인 모델 위치
├── Order.java
└── OrderLine.java
com.myshop.order.domain.service // 도메인 서비스 위치
├── OrderService.java
└── DiscountService.java
# ex 도메인 모델
// com.myshop.order.domain.order.Order.java
public class Order {
private Long id;
private List<OrderLine> orderLines;
public int getTotalAmount() {
return orderLines.stream().mapToInt(OrderLine::getAmount).sum();
}
}
# ex 도메인 서비스
// com.myshop.order.domain.service.OrderService.java
public class OrderService {
public int calculateDiscount(Order order) {
if (order.getTotalAmount() > 100_000) {
return 10_000;
}
return 0;
}
}
- 응용 서비스도 도메인 별로 패키지 구분 가능
- 정해진 규칙은 없지만 저자는 한 패키지에 10~15개 미만으로 타입 개수를 유지하고 넘어가면 패키지 분리