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

Jermaine ·2023년 9월 14일
0
post-thumbnail

아키텍쳐 개요

네 개의 영역

Layerd Architecture

  • 표현, 응용, 도메인, 인프라스트럭쳐는 아키텍쳐를 설계할 때, 등장하는 전형적인 4가지 영역이다.
    1. 표현 계층은 컨트롤러가 위치하며, 사용자(웹 브라우저, 다른 서비스 등)의 요청을 받아 응용 계층에 전달하고, 전달받은 반환 값을 통해 사용자에 응답하는 역할을한다.
      • 이 때, 응용 계층의 메서드의 호출 인자로 전달하는 역할을 수행하고, 메서드의 반환 값을 사용자가 원하는 형태(ex JSON)로 변환하여 전달하는 역할을 수행한다.
    2. 응용 계층은 서비스가 사용자에게 제공해야할 기능을 구현한다. 이러한 기능을 구현하기 위하여, 도메인 계층의 도메인 모델을 사용한다.
      • 여기서 중요한 점은 응용 계층의 서비스는 직접 도메인 모델의 로직을 수행하는 것이 아닌 도메인 모델에 정의된 로직을 호출하여, 도메인 로직의 수행을 도메인 모델에 위임한다.
    3. 도메인 계층은 도메인 모델을 구현하며, 해당 도메인 모델이 제공해야하는 핵심 도메인 로직을 메서드로 제공한다.
      • 응용 계층은 이러한 핵심 도메인 로직을 호출한다.
    4. 인프라스트럭쳐 계층은 구현 기술에 대한 것을 다룬다. 논리적인 개념을 표현하기보다는 기존에 제공되는 실제 구현(ex 라이브러리, 모듈 등)을 통해 외부 시스템과의 연동을 지원한다.
      • 예를 들어, DB연동, 메세징 큐에 연동하는 기능 등 을 구현한다.
  • 레이어드 아키텍쳐에서 이 4가지 영역(계층)의 역할과 관심사를 분리하는 것이 중요하며, 이를 적절히 설계하는 것은 확장하는 서비스에 대한 유지보수성을 매우 높인다.

계층 구조(레이어드, Layered) 아키텍쳐

  • 레이어드 아키텍쳐 에서는 상위 계층에서 하위 계층으로의 의존만이 존재한다.
    • 표현 계층은 응용 계층에 의존하고, 응용계층이 도메인 계층에 의존한다. 그리고, 도메인 계층은 인프라스트럭쳐 계층에 의존한다.
    • 반대로 인프라스트럭쳐 계층이 도메인에 의존하거나, 도메인이 응용 계층에 의존하지는 않는다.
      • A계층이 B계층에 의존한다의 의미는, A계층에서 B계층의 구성 요소를 사용한다고 생각하면 될 것 같다.
    • 원칙적으로는 상위 계층은 바로 아래의 계층에만 의존해야한다. 하지만, 원칙이 있다면 예외는 존재하는 법이다.
      • 구현의 편리함을 위해서 계층 구조를 유연하게 적용하는 형태도 가능하다. 응용 계층에서는 도메인 계층만을 의존해야하지만, 외부 연동을 위해 인프라스트럭쳐 계층을 의존하기도 한다.
        • 예외상황 답게, 이런식의 유연함은 상세 기술을 다루는 인프라스트럭쳐 계층응용 계층이 종속될 수 있다는 부작용이 존재한다.
      • 이러한 부작용은 각 계층의 단독적인 단위 테스트를 어렵게 할 뿐만아니라, 구현 방식 변경에 어려움을 유발한다.
        • 의존하는 하위 계층의 구성요소를 사용하여 단위 테스트를 수행해야하고, 다른 구현 기술(ex. JPAElasticSearch)을 사용하기 위해서 특정 계층의 코드를 대거 수정해야한다.
      • 이러한 문제는 부작용을 해소하기 위해 우리는 DIP를 사용한다.

DIP

  • 고수준 모듈은 의미가 있는 단일 기능을 제공하는 모듈을 의미한다.

    • 예를 들어, CalculateDiscountService 가격 할인 계산이라는 도메인 기능을 구현 및 제공하는 고수준 모듈이다.
  • 저수준 모듈, 즉, 고수준 모듈에서 필요로하는 여러 하위 기능을 제공하는 모듈을 의미한다.

    • 예를 들어, JPARepositoryRDBMS에서 특정 데이터를 읽고 쓰는 기능을 구현 및 제공하는 저수준 모듈이다.
    • 무조건 저수준 모듈인프라스트럭쳐 계층의 구성요소만을 의미하지는 않는다. 저수준, 고수준은 계층 간의 상대적인 관점에서 발생하는 차이일 뿐이다.
  • 고수준 모듈의 기능을 구현하기 위해서는 저수준 모듈의 여러 하위 기능을 적절히 조합해야한다.

    • 하지만, 고수준 모듈이 직접적으로 저수준 모듈을 직접 의존한다면, 위에서 언급한 단위 테스트의 어려움과 유연성의 악화를 야기한다.
  • 이런 상황에서는 DIP(Dependancy Inversion Principle, 의존성 역전 원칙)를 활용하여, 저수준 모듈고수준 모듈에 의존하도록 바꾸면된다.

    • 어떻게 할 수 있을까라는 질문에는 인터페이스라는 답이 있다.

      • 아래의 그림과 같이, 저수준 모듈이 제공해야하는 하위 기능을 정의한 인터페이스고수준 모듈과 같은 계층에 선언하고, 저수준 모듈이 해당 인터페이스를 상속받아 기능을 구현하는 것이다.

      • 즉, 고수준 모듈저수준 모듈이 어떻게 구현되어있는지 알 필요가 없다. 그냥 원하는 기능만 수행해주면 된다.

      • 또한, 응용 계층에서 인프라스트럭쳐 계층을 의존하고 싶을 때에도, 인터페이스응용 계층에 선언하고, 해당 인터페이스인프라스트럭쳐 계층에서 구현하면 된다.

        하지만, 위의 의존-상속의 그림을 위해 철저하게 의존 관계를 지키는 것 또한, 오히려 우리의 최종 목표인 확장 가능한 유연한 개발과 멀어지는 경우도 있다. 유연한을 너무 뽐내버린 통 밖에서도 고통받는 통아저씨를 상상하자.
        현재 상황을 보고, DIP를 지켰을 때, 얻는 이점을 고려하여 적당히 잘 검토해야한다.

        통을 나와도 유연함을 뽐내고 있다.

    • 이를 통해, 프로그래머는 해당 인터페이스를 구현한 대역 객체를 생성하여 단위테스트를 구현 객체 없이 수행할 수 있으며, 구현 객체를 변경하고 싶을 때, 새로운 구현 객체를 변경하고, 코드의 변경을 최소화한 상태로 의존성만 새로이 주입해주면 된다.

      • 하지만, 인터페이스를 구현 객체의 관점에서 도출하는 바보 같이 개썰매를 사람이 끄는 주객전도 상황을 만들지 말자. DIP 관점에서 추상화된 인터페이스는 고수준 모듈 관점에서 필요로 하는 기능을 중심으로 도출되어 한다.

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

  • 도메인 영역은 도메인의 핵심 도메인를 구현하는 영역으로 주요 개념핵심 로직을 구현한다. 아래는 도메인 영역의 주요 구성요소다.
  1. 엔티티: 고유의 식별자를 갖는 객체로 스스로의 라이프사이클을 가진다. 도메인 고유의 개념을 표현하며, 이와 관련된 기능을 제공한다.
  2. 밸류: 식별자가 없는 객체로, 개념적으로 하나의 값을 표현할 때 사용한다(주소, 금액). 엔티티의 속성뿐만 아니라, 다른 밸류의 속성으로 사용될 수 있다.
  3. 애그리거트: 연관된 엔티티밸류 객체를 하나의 개념적인 단위로 묶은 것이다.
  4. 레포지터리: 도메인 모델의 영속성을 처리하며, 도메인 모델의 읽기/쓰기와 같은 기능을 제공한다.
  5. 도메인 서비스: 특정 엔티티에 속하지 않는 도메인 로직을 제공하며, 여러 엔티티밸류를 조합하여 하나의 기능으로 구현한다.

엔티티와 밸류

  • 지난 포스트에서 대부분의 내용은 언급했다.(귀찮은 것이 아니다.) 다만, 중요한 것은 DB 테이블의 엔티티와 도메인 모델의 엔티티를 같은 것으로 취급하지 말아야한다. 연관성을 수 있더라도 100% 같은 개념이 아니다.
    • 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다. 또한, 복합적인 데이터를 밸류로 처리한다.
      • 사실, JPA의 @Entity를 사용하여, JPA에 의존하는 도메인 엔티티로 사용하는 경우가 많긴하며, 이러한 밸류@Convert를 사용하여 처리하기도 한다.
      • 이때도, 해당 엔티티 클래스의 메서드로 도메인 기능을 제공하는 형태를 띈다.
    • 즉, 도메인 모델의 엔티티데이터와 기능을 함께 제공하는 객체다.
  • 도메인 관점에서 이러한 엔티티는 기능을 구현하고 캡슐화하여 외부로부터 데이터가 임의로 변경되는 것을 막는다.
  • 밸류는 불변한 객체로 구현할 것을 권장하는 편이다. 변경을 원할 때는 밸류 객체 자체를 교체하는 것을 의미한다.

애그리거트(Aggregate)

  • 도메인의 규모가 커질수록, 많은 엔티티밸류가 관여된다. 이는 도메인 모델의 복잡성을 높인다.
    • 도메인 모델이 복잡해지면, 전체 구조를 파악해야하는 개발자는 하나의 엔티니와 밸류에만 집중하는, 숲보다 나무에 집중하는 상황이 올 수 있다.
  • 이러한 상황에서 상위 수준에서 도메인 모델을 관망하게 해주고, 이를 통해 전체 숲과 나무의 협력 및 조화를 파악할 수 있게 해주는 것이 애그리거트다.
  • 애그리거트 : 관련 객체를 하나로 묶은 논리적 집합으로 예를 들어 주문이라는 상위 개념(애그리거트)에는 주문, 배송지 정보, 주문자, 주문 목록 등이 하위 모델로 포함된다.
    • 애그리거트는 군집 내에 속한 객체들을 관리하는 루트 엔티티를 가지고, 루트 엔티티애그리거트에 속한 엔티티밸류에 접근하거나 활용하여 애그리거트가 구현해야할 기능을 제공한다.
      • 이를 통해 애그리거트 내의 객체들은 루트 엔티티를 통해 캡슐화된다.
    • 애그리거트를 단위로 이들의 관계를 파악하고, 이를 통해 도메인 모델을 이해하고 구현하게 된다면, 도메인 모델의 큰 그림을 파악하고 관리할 수 있다.
  • 그렇기 때문에 애그리거트를 어떻게 구현하는지에 따라, 구현의 복잡도, 트랜잭션의 범위, 기술에 대한 제약 등이 변경될 수 있기 때문에, 충분히 많은 고려가 이루어져야한다.

레포지터리(Repository)

  • 레포지터리는 도메인 객체의 영속성을 물리적 저장소를 통해 관리하는 도메인 모델이다.
    • 엔티티밸류가 도메인의 요구사항을 통해 도출된 도메인 모델이라면, 레포지터리는 해당 요구사항의 구현을 위한 도메인 모델이다.
  • 통상적으로 레포지터리는 도메인 객체에 대한 읽기/쓰기에 대한 기능을 정의한다. 일반적으로 사용하는 findById, save, delete 등과 같은 메서드를 선언한다.
    • 이러한 메서드들은 도메인 객체(엔티티, 루트 엔티티) 단위로 동작한다.
  • 도메인 모델을 활용하는 코드는 레포지터리를 통해 얻은 도메인 객체를 통해 기능을 실행한다.
  • 여기서 중요한 점은 도메인 계층의 레포지터리는 메서드를 구현하는 것이 아니라 추상화된 기능을 정의하는 고수준 모듈이라는 점이다.
    • 이를 구현한 객체는 인프라스트럭쳐 계층에 속하는 저수준 모듈이다.
    • 응용 계층의 서비스의존성 주입을 통해 레포지터리 구현 객체에 접근한다.
      • 응용 계층의 서비스트랜잭션을 활용하여 도메인 객체를 읽거나 쓰는데, 트랜잭션은 보통 구현 기술에서의 처리에 영향을 받는다.

요청 처리 흐름

* 위에서 언급했지만, 다시 한 번 간단하게 짚고 넘어간다. 아래에서 나타낸 그림과 같이 계층 구조의 웹 어플리케이션은 동작한다.
1. 표현 계층의 컨트롤러는 사용자가 요청(ex. HTTP request)할 때 전달한 데이터 형식을 검증하고, 이를 응용 계층의 서비스 메서드의 전달인자로 변환하여 서비스 로직을 수행하는 메서드를 호출한다.
2. 응용 계층의 서비스 메서드는 로직을 수행하면서, 레퍼지토리를 활용하여 도메인 모델을 읽어 도메인 로직을 호출하거나, 도메인 객체를 영속적인 물리 저장소에 쓰는 역할을 한다.
* 올바르게 영속적인 물리 저장소에 반영되게 하기 위해 트랜잭션응용 계층에서 관리한다.
3. 응용 계층의 서비스 메서드의 반환 값을 표현 계층의 컨트롤러로 전달하여 사용자의 요청에 응답한다(HTTP response).

Layerd Architecture


인프라스트럭쳐 개요

  • 인프라스트럭쳐는 상위의 모든 계층들을 지원한다. 현재의 시스템 외부의 다른 시스템 구성요소와의 연결을 담당하며 이를 위한 구현 기술, 프레임워크, 보조기능 등을 구현한다.
    • 다른 서비스와의 HTTP 통신, Kafka와 같은 메세징 큐와의 연동, RDBMS 혹은 NoSQL과 같은 데이터베이스와의 연결, Redis와 같은 캐시와의 연결 등을 지원한다.
  • 앞서 언급했듯이, 이러한 구현 기술을 다루는 인프라스트럭쳐저수준 모듈도메인 계층에서 정의된 고수준 인터페이스를 구현하는 것이 유연하고 테스트-용이한 개발에 도움이 된다.
    • 그러나, 무조건적으로 인프라스트럭쳐 계층에 대한 의존을 없애는 것이 마냥 좋은 것은 아니다.
    • 살을 내어주고 뼈를 때린다는 생각으로 @Entity, @Table과 같은 JPA 전용 애노테이션을 직접 도메인 모델 클래스에 작성해주는 것이 오히려 빠르고 쉬운 개발에 더욱 도움이 될 경우도 있다.
      • 즉, DIP의 장점인 유연함과 테스트-용이함 만큼 구현의 편리함도 중요하기 때문에, 서로의 장점에 있어서 손해보지 않는 선으로 의존성을 가져가는 것이 좋다.

모듈 구성

  • 아키텍쳐의 각 계층은 별도의 패키지에 위치하는데, 이에 대한 뚜렷한 정답은 없다(눈치가 좋고 경험이 많으면 편하겠지만, 적당히 잘 하면 된다) .
  • 정답은 없지만... 위와 같이, 책에서는 크게 두 가지 방법을 소개한다...
    1. 하나의 애그리거트가 크면, 여러 하위 도메인 별로 모듈을 나누어 각 모듈 별로 계층 구조로 관리할 수 있다.
    2. 하나의 도메인 모듈이 크다면, 도메인 계층을 또 여러 도메인 패키지로 나누어서 관리할 수 있다.
profile
저메인 주도 개발

0개의 댓글