2장 : 아키텍처

네 개의 영역

  • 표현 영역 - 사용자의 요청을 받아 응용 영역에 전달하고 응용 영역의 처리 결과를 다시 사용자에게 보여줌

  • 응용 영역 - 시스템이 사용자에게 제공해야 할 기능을 구현한다.

  • 도메인 - 도메인 모델을 구현한다. 도메인 모델은 도메인의 핵심 로직을 구현한다.

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

계층 구조 아키텍처

인프라스트럭처에 의존하면 '테스트 어려움''기능 확장의 어려움'이라는 두 가지 문제가 발생한다.

DIP(Dependency Inversion Principle, 의존 역전 원칙)

고수준 모듈은 의미 있는 단일 기능을 제공하는 모듈이고, 저수준 모듈은 하위 기능을 실제로 구현한 것이다. DIP를 적용하면 저수준 모듈이 고수준 모듈에 의존하게 된다.

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

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

  • 엔티티 (ENTITY) - 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 도메인의 고유한 개념을 표현한다. 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.

  • 밸류 (VALUE) - 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현할 때 사용된다. 엔티티의 속성으로 사용할 뿐만 아니라 다른 밸류 타입의 속성으로도 사용할 수 있다.

  • 애그리거트 (AGGREGATE) - 애그리거트는 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다.

  • 리포지터리 (REPOSITORY) - 도메인 모델의 영속성을 처리한다.

  • 도메인 서비스 (DOMAIN SERVICE) - 특정 엔티티에 속하지 않은 도메인 로직을 제공한다. 도메인 로직이 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현한다.

엔티티와 밸류

도메인 모델의 엔티티와 DB 모델의 엔티티의 가장 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 것이다. 즉, 도메인 모델의 엔티티는 단순히 데이터를 담고 있는 데이터 구조라기보다는 데이터와 함께 기능을 제공하는 객체이다. 도메인 관점에서 기능을 구현하고 기능 구현을 캠슐화해서 데이터가 임의로 변경되는 것을 막는다.

또 다른 차이점은 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다는 것이다. RDBMS와 같은 관계형 데이터베이스는 밸류 타입을 제대로 표현하기 힘들다.

애그리거트

돔도메인 모델이 복잡해지면 상위 수준에서 모델을 관리하지 않고 개별 요소에 초점을 맞추게 되는데, 이는 큰수준에서 모델을 관리할 수 없는 상황에 빠질 수 있다. 도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 바로 애그리거트 이다.

애그리거트는 관련 객체를 하나로 묶은 군집이다. 관련된 객체를 애그리거트로 묶으면 복잡한 도메인 모델을 관리하는 데 도움이 된다.

애그리거트는 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다. 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다.

리포지터리

도메인 객체를 지속적으로 사용하려면 RDBMS, NoSQL, 로컬 파일과 같은 물리적인 저장소에 도메인 객체를 보관해야 한다. 이를 위한 도메인 모델이 리포지터리 Repository이다.

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

리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.

public interface OrderRepository {
    Order findByNumber(OrderNumber number);
    void save(Order order);
    void delete(Order order);
}

도메인 모델을 사용해야 하는 코드는 리포지터리를 통해서 도메인 객체를 구한 뒤에 도메인 객체의 기능을 실행한다. 도메인 모델 관점에서 Repository는 도메인 객체를 영속화하는 데 필요한 기능을 추상화한 것으로 고수준 모듈에 속한다.

public class CancelOrderService {
    private OrderRepository orderRepository;
    
    public void cancel(OrderNumber number) {
        Order order = orderRepository.findByNymber(number);
        if (order == null) throw new NoOrderException(number);
        order.cancel();
    }
}

think

만들면서 배우는 클린 아키텍처에서는 port 부분을 application/port/out 에 위치시켰는데, 여기서는 domain에 있다. 나라면 어디에 위치 시킬..까 ..?

요청 처리 흐름

응용 서비스는 도메인 모델을 이용해서 기능을 구현한다. 두 개 이상의 도메인 객체를 사용해서 구현하기도 한다.

도메인의 상태를 변경하는 응용 서비스의 경우 변경 상태가 물리 저장소에 올바르게 반영되도록 트랜잭션을 관리해야 한다.

인프라스트럭처 개요

인프라스트럭처는 표현 영역, 응용 역역, 도메인 영역을 지원한다. DIP에서 언급한 것처럼 도메인 영역과 응용 영역에서 인프라스트럭처의 기능을 직접 사용하는 것보다 이 두 영역에 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는 것이 시스템을 더 유연하고 테스트하기 쉽게 만들어준다.

하지만 무조건 인프라스트럭처에 대한 의존을 없앨 필요는 없다. (@Transactional, 영속성 처리를 위해 JPA를 사용할 경우 @Entity나 @Table과 같은 JPA 전용 애너테이션)

구현의 편리함은 DIP가 주는 다른 장점만큼 중요하기 때문에 DIP의 장점을 해치지 않는 범위에서 응용 영역과 도메인 영역에서 구현 기술에 대한 의존을 가져가는 것이 나쁘지 않다고 생각한다.

모듈

✔️ 도메인이 크면 하위 도메인으로 나누고 각 하위 도메인마다 별도 패키지를 구성한다.

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

애그리거트, 모델, 리포지터리는 같은 패키지에 위치시킨다.

모듈 구조를 얼마나 세분화해야 하는지에 대해 정해진 규칙은 없다. 한 패키지에 너무 많은 타입이 몰려서 코드를 찾을 때 불편한 정도만 아니면 된다.

Last updated