# 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) 외부 시스템과의 연동을 위해 응용 계층이 인프라스트럭처에 의존하기도 함
  • 그러나 만약 다른 구현 기술을 사용하게 되면 코드의 많은 부분을 고치게 됨

# 인프라스트럭처에 의존하면?

  1. 테스트 어려워짐
  2. 기능 확장 어려워짐 → 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, 의존 역전 원칙)

# 장점

  1. 구현 기술 교체가 쉬워짐
  2. 테스트하기 쉬워짐 (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개 미만으로 타입 개수를 유지하고 넘어가면 패키지 분리