본문 바로가기
Design pattern/DDD

DDD, Hexagonal, Onion, Clean, CQRS를 조합한 아키텍쳐(사실상 Hexagonal)

by 코딩공장공장장 2023. 12. 18.

이 글은 아래 링크에 나와있는 내용을 참조하여 정리한 글이다.

https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/#fundamental-blocks-of-the-system

 

 

DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together

In my last posts I’ve been writing about many of the concepts and principles that I’ve learned and a bit about how I reason about them. But I see these as just pieces of big a puzzle.  …

herbertograca.com

 

원문의 주제는 Explicit Architecture로 DDD, Hexagonal, Onion, Clean, CQRS를 조합한 아키텍쳐이다.

원문의 저자가 만든 개념인데 DDD와 Hexagonal 아키텍쳐가 주인것 같다.

(헥사고날 아키텍쳐 하면 많이 보이는 위 육각형 도식화 이미지가 나온 원문이다.)

 

글의 내용은 원문에 최대한 충실하며 원문에서 사용하는 표현, 용어를 최대한 그대로 사용하며 부가적인 설명을 더하였다. 부가적인 설명은 이해되지 않았던 개념 또는 관련 예제이다. (내가 이해하기 위해)  

 

 

Explicit Architecture

 

1. 시스템의 기본 구성요소

EBI(Entity Boundary Interactor)와  Port&Adapter(aka Hexagonal) 아키텍쳐를 기반으로 한 아키텍쳐

두 아키텍쳐 모두 명시적으로 application 내부와 외부 그리고 외부와 내부를 연결하는 코드로 이루어져있다.

[잠깐]
EBI(Entity Boundary Interactor) Architecture
- Entity : 데이터와 그와 관련된 로직을 갖고 있음(Domain)
- Boundary : 외부 툴(웹서버, DB etc)과 연동하는 책임을 가지고 있다. (Controller, Repository와 같은 역할)
- Interactor : DDD의 Application Service, Domain Service와 같은 역할, Entity와 Boundary가 하지 못하는 역할 담당

Port&Adapter
- Port : 1) application core의 출입구 역할
            2) application core로 들어오는 inbound Port와 application core에서 나가는 outbound port 존재
            3) 대부분의프로그래밍 언어에서 interface에 해당
- Adapter : 1) 외부 툴과 연동하는 역할을 담당하는 Port의 구현체
                  2) Driver Adapter : inbound Port를 감싼다. (합성을 통해 application service를 의존 주입 받음)
                  3) Driven Adapter : outbound Port의 구현체

 

 

3개의 큰 블록의 구성

  • user interface : 어떤 유저인터페이스 환경에서라도 실행될 수 있게 하는 것
    (controller는 웹과 모바일 유저 인터페이스 환경에서 실행을 가능하게 하고, console command는 cli 유저 인터페이스 환경에서 실행을 가능하게 한다.)
  • application core(business logic) : user interface에 의해 호출됨
  • infrastrure core : application core와 연결, db, 검색엔진, 써드파티 API와 같은 툴

 

 

application core의 역할

  • 우리가 실질적인 제공하려는 서비스를 동작하게 한다. 
  • web, mobile, cli, api와 같은 유저 인터페이스들을 사용할 수 있다.
  • 각각에 대해 모두 같은 동작을 수행하여 결과를 제공해야한다.

=> 즉, applicatio core는 여러 유저 인터페이스와 연결될 수 있고 동일한 결과를 제공해줘야 한다.

 

 

위의 그림처럼 전형적인 애플리케이션은 user interface에서 application core를 거쳐 infra structure에 진입하고 다시 user interface로 돌아와 응답 결과를 반환한다.

 

2. Tools

application이 사용하는 DB, 검색엔진, 웹서버, CLI 콘솔과 같은 외부 시스템

 

 

위 그림에서 웹서버와 CLI 콘솔와 같이 user interface에 위치한  toolapplication core에게 어떤 행동을 요 한다면,

DB와 같은  infrastructure에 위치한 tool은 application core에게 어떤 행동을 하도록 요청 받는다.

 

=> 이는 user interface와 infrastructure의 뚜렷한 차이로 어떻게 코드를 설계할지에 대한 뚜렷한 차이를 가져온다.

=> 뒤에서 설명하겠지만 user interface는 합성을 통해 application core를 감싸고 infrastructure는 port를 구현하는 방식을 통해 설계한다.

 

3. Tool의 연결과 application core로 전달되는 메카니즘

 위에서 설명한 웹서버, DB와 같은 툴과 appllication core를 연결하는 코드 유닛을 adapter라고 부른다.

adapter는 비즈니스 로직이 툴을 연결 하고, 또한, 툴이 비즈니스 로직에 연결할 수 있도록 한다.

 

※ 참고. 비즈니스 로직이 adapter릍 통해 tool 연결하는 경우는 service layer에서 persistence layer(adapter)를 통해 DB에 연결되는 경우이고, 툴이 adapter를 통해 비즈니스 로직에 연결하는 경우는 웹서버가 controller를 통해 service layer와 연결되는 경우이다.

 

요청의 주체/대상에 따른 adapter의 분류

Driving/Driver Adapter(Primary Adapter) : application core에 행위를 요청하는 adapter

Driven Adapter(Secondary Adapter) : application core에 의해 행위를 요청받는 adapter

 

 

Ports

  • Adapters는 무작위로 마음대로 생성할 수 있는게 아니다. 
  • Adapters는 application core의 진입지점인 port와 연결되어야 한다.
  • port는 단지 application core와 툴의 연결에 관한 명세서이다.
    - Driving Adapter에 연결된 툴이 application core에 어떤 비즈니스 요청을 할지,
    - application core가 Driven Adapter에 연결된 툴을 어떻게 사용할지를 나타내야한다.
  • port는 대부분의 언어에서 인터페이스이고 dto(application core layer의 dto)를 포함할 수 있다.

중요한 것은 Port는 비즈니스 로직에 맞게 설계되야 한다. 단순히 툴 사용에 대한 API가 아니다.

 

위에 빨간색으로 표시한 두 문장이 굉장히 상반되는 문장으로 느껴질 것이다.

원문을 최대한 그대로 가져오려고 하다보니 우리말로 번역했을 때 모순된 문장 처럼 느껴지지만,

저자가 얘기하는 내용은 "Port는 application core와 tool의 단순한 연결에 관한 명세서이다.
하지만 여기서 연결이 마치 application core가 tool을 알고 있다고 생각하고 연결 명세서를 만들면 안된다.
포트는 비즈니스 로직에 맞는 요청 명세서이다. 따라서 tool과 application core의 기술적 연결이 아닌
비즈니스 요청 연결로 생각해야한다." 라고 생각하면 될 것 같다. (내 생각)

 

 

Driving or Primary Adapters

  • Driving adapter는 port를 감싼다.(합성)
    컨트롤러를 예로 들면 생성자를 통해 port의 구현체(application core의 service)를 의존주입 받는다.
  • 물론  Driving  Adapter는 포트라는 추상화 상위계층에 의존한다.(application core의 service 구현체에 의존X)
  • 외부(tool)에서 오는 요청을 application core와 연결하기 위해 포트에서 제공하는 형식으로 변환처리 후 연결하는 역할을 한다.
  • application core에게 어떤 행위를 하도록 요청한다.



 

Driven  or Secondary Adapters 

  • Driven Adapters는 application core에 주입되는 port의 구현체
    => OOP의 OCP와 관련.
    ex) DB를 사용하기 위한 포트의 adapter가 Mysql 영속화 구현체라면
          우리는 PostgreSQL이나 MongoDB 영속화 구현체를 만들어 기존의 Mysql 구현체의 소스 코드
          변경없이 대체 할 수 있다. (흔히 말하는 갈아 끼운다.)



IOC

  • adapter는 툴(db, web server 등)과 port에 의존한다는 것이다.
  • 또한 비즈니스 로직도 port에 의존한다는 것이다.
    => 즉 비즈니스 로직은 어떠한 adapter와도 의존하지 않는다.
  • 의존의 방향의 application core 쪽으로, 중앙 쪽으로 향한다.
  • 즉, 헥사고날 아키텍쳐는 ioc 원칙을 지향한다.

 

하지만, 무엇보다 가장 중요한 것은 port가 마치 tool의 api인 것 처럼 흉내내는게 아니라 application core의 요구사항이 드러나도록 설계하는 것을 잊어서는 안된다.

(IOC도 반드시 지켜야 하지만 port 설게시 application core가 어떤 행동을 할지, 어떤 행동을 요구할지를 명심해야한다.)

 

[잠깐] 일반적으로 IOC 개념을 이야기할 떄, 프로그램의 실행 제어권을 개발자가 아닌 외부(프레임워크, 컨테이너 등)에서 가져간다고 많이 설명한다. 하지만 여기서 IOC는 실행 제어권의 주체를 바꾼다는 의미가 아니라, 실행 제어권을 중앙에 있는 Applicatio core에 의존하도록 한다는 것이다. 보통 프로그램은 호출 순서대로 실행되어 실행 제어권 또한 순서대로 가지게 된다. 헥사고날 아키텍쳐는 실행 제어권을 중앙에 있는 Application Core가 갖는다. 이는 외부 어댑터가 모두 Port에 의존하여 실행되기 때문이다. 중앙에 위치한 Application Core는 이러한 Port들을 통해 외부 어댑터들을 연결하고, 실행 흐름을 제어함으로써 유연한 시스템을 형성한다.

즉, DIP를 통해 어댑터가 포트에 의존하여 연결될 수 있도록 하고 포트를 Application core에 위치시킴으로써 외부 요소가 모두 포트에 의해 제어 받게 하였다.

 

 

4. Application core의 구성

Application layer

  • Application Layer에선 유저 인터페이스에 의해 application core의 행위가 실행되는 유스케이스가 존재한다.
  • DDD에서 제공되는 첫번째 레이어이고 Onion Architecture에서 사용된다.
  • 이 레이어에서는 서비스를 포함한다. 또한 port&adapter의 port를 포함한다
  • Command Bus와 Query Bus를 사용하는 환경에서 각각의 핸들러를 이 레이어에서 포함한다.

Application Service와 Command Handler는 유스케이스와 비즈니스 프로세스를 처리하며 아래와 같은 역할을 한다.

1. 한개 또는 한개 이상의 entity를 찾기 위해 repository를 사용한다.

2. 이 엔티티를 통해 몇몇 도메인 로직을 실행한다.

3. 그리고 다시 entity를 영속화하기 위해 repository를 사용한다.

이런식으로 효과적으로 데이터의 변화를 영속화 한다.

 

(여기서, entity는 ORM의 entity라기 보다는 식별가능한 개체로 이해하면 될 것 같다. 만약 ORM의 entity와 같은 개념이라면 내부에서는 entity를 domain layer의 domain model로 변환하여 사용한다고 생각해야할 것이다. layer 분리 되어있으므로)

 

Command Handler는 두가지 방식으로 사용될 수 있다.

1. 실제 유스케이스의 실행 로직을 포함할 수 있다.

2. 단순히 Command를 받고 Application Service에 로직을 실행시키는 한 부분으로 사용될 수 있다.

 

이 레이어에서는 또한 Application Events를 발생시키는 로직을 포함할 수 있다.

(이메일, 써드파티 API, 푸시알림과 같은 이벤트를 발생시키는 로직, 발생시킨다는 것이지 여기서 이벤트를 실행한다는 것이 아니다. 단순히 이벤트를 요청하는 정도)

 

Domain layer

application layer 보다 안쪽에 있는domain layer는 도메인에 구체적인 데이터와 데이터를 다루는 로직을  포함한다.

비즈니스 로직과 독립적이며 application layer를 알지 못한다.

즉, domain layer는 application layer를 의존하지 않는다.

 

Domain Service

우리는 몇몇 엔티티(ORM 엔티티X, 물리적 또는 논리적으로 식별 가능한 고유한 개체)를 다뤄야 하는 도메인 로직을 만날 수 있다. 물론 도메인 로직은 domain model의 책임이 아니라고 할 수 있다.

 

이런 상황에서는 도메인 로직을 application layer에서 처리하려고 할 수도 있다.

그러나 이렇게 짜여진 코드는 다른 유스케이스에서 재사용하기 힘들다.(특정 유스케이스 처리 과정에서 도메인 로직을 구현하여 사용한다면 다른 유스케이스 처리과정에서 복붙이든 커스터마이징이든 똑같이 진행해야함)

 

이러한 문제의 해결책은 domain service를 생성하는 것이다. domain servicedomain layer에 존재한다.

domain service는 여러 엔티티를 받을 수 있고 몇몇 비즈니스 로직을 수행할 수 있다.

(이때, 비즈니스 로직은 도메인 객체를 처리하거나 도메인 객체의 속성에 접근할 수 있는 로직, 단순한 getter 뿐만 아니라 어떠한 처리를 통해 값을 얻어야 하는 경우, application layer의 비즈니스 로직과 다르다. 오로직 도메인 객체와 그 속성 집중된다.)

 

Domain Service의 특징
상태없음 : 로직 중심 : 엔티티의 상태변경과 같은 처리 하지 않음
독립성 : domain layer에 속하면 외부 레이어와 의존하지 않음
재사용성 : 여러 유스케이스에서 사용될 수 있음

 

 

예제

// Product 인터페이스
interface Product {
    val name: String
    val price: Double
}

// 신발 상품
data class ShoeProductVo(override val name: String, override val price: Double) : Product

// 바지 상품
data class TrouserProductVo(override val name: String, override val price: Double) : Product

class ProductWeeklyDiscountService {
    fun applyDiscount(product: Product, date: LocalDate): Double {
        return when {
            // 매주 금요일 신발 20% 할인
            isFriday(date) && product is ShoeProductVo -> applyPercentageDiscount(product.price, 0.2)
            // 매주 토요일 바지 10% 할인
            isSaturday(date) && product is TrouserProductVo -> applyPercentageDiscount(product.price, 0.1)
            else -> product.price
        }
    }

    private fun applyPercentageDiscount(price: Double, percentage: Double): Double {
        return price - (price * percentage)
    }

    private fun isFriday(date: LocalDate): Boolean {
        return date.dayOfWeek == DayOfWeek.FRIDAY
    }

    private fun isSaturday(date: LocalDate): Boolean {
        return date.dayOfWeek == DayOfWeek.SATURDAY
    }
}

 

위는 신발, 바지 product의 매주 요일별 할인율을 계산하는 로직이다.

매주 금요일은 신발 20%, 토요일은 바지 10%를 할인한다. 위 로직을 application layer가 아닌 domain layer에 위치시키면 여러 application layer에서 재사용할 수 있다. 도메인 로직은 위와 같이 상태가 존재하지 않고 도메인 자체에 집중한 로직을 domain layer에 둠으로써 여러 application layer에서 재사용할 수 있도록 도와준다.

 

 

Domain model

  • 가장 중앙에 위치한 domain model은 아무것도 의존하지 않는다.
  • domain model에는 비즈니스 객체, 엔티티(ORM 엔티티X), vo, enum 등

 

5. Components

  • 레이어 단위의 분리도 중요하지만 좀 더 큰 범위에서 코드 분리도 중요하다.
  • 이 개념에서 코드 분리는 서브 도메인과 bounded contexts와 관련되어있다.
    ('레이어 단위 패키지'와 반대되는 '기능 단위 패키지', '컴포넌트 단위 패키지'로 언급되기도 한다.)
 

 

 

우리는 Package by Component에 주목할 것이다.

 

 

위의 그림을 보면 이전 이미지와 달리 컴포넌트를 포함하는 레어어에서 data access부분이

Infrastructure 부분으로 분리됬다. 

 

Package by component

컴포넌트의 예시로는 인증과 인가, 정산, 청구, 회원, 계정(계좌) 등이 될 수 있다.

하지만 이것들은 항상 도메인과 관련되어 있다.

(인증과 인가는 port 뒤에 숨어서 외부 틀과 연결하는 adater와 같다.)

 

※ 참고. "port는 adapter 또는 외부 툴을 숨기는 책임을 갖는다." 는 표현을 원문에서 자주 쓴다. 댓글에서도 다른 사람들이 이러한 표현을 쓰는 경우가 잦은데, '숨긴다'라는 표현이 '추상화를 통해 구현체를 알지 못하게 한다'라는 의미를 나타내는 것 같다.

 

Decoupling the components

레이어 분리처럼 컴포넌트 분리 또한 느슨한 결합과 높은 응집도의 장점을 준다.

레이어 분리에서 우리는 직접 인스턴스를 생성하지 않고 의존 주입(DI)을 통해 객체를 주입한다. 

그리고 의존 역전(DIP)을 통해 구체 클래스가 아닌 추상화에 의존한다.

이것은 의존받는 클래스에서 의존하는 구체 클래스에 대해 알지 못한다는 것을 의미한다.

 

같은 방식으로 완전한 컴포넌트 분리(Decouping the components)는 컴포넌트간 서로의 존재를

직접적으로 알지 못한다는 것을 의미한다.

구체클래스를 모르는 것 뿐만 아니라 인터페이스 조차 알지 못한다.

즉, 컴포넌트 분리는 di와 dip로는 충분하지 않다는 것을 의미한다.

이러한 경우 우리는 이벤트나 shared kernel, eventual consistency 등을 통해 완전한 분리를 이룰수 있다.

(컴포넌트간 분리가 두 컴포넌트가 서로 연결?통신? 될 일이 없다는 것은 아니다. 의존성은 완전히 분리 되지만 컴포넌트간 서로 필요한 정보는 주고 받아야 됨. 원문에서 마땅한 "연결?통신?"을 의미할 용어를 쓰지 않아서 나도 내 주관적으로 용어를 정의하지 않았다.)

 

[예제] 다른 컴포넌트의 로직 발생시키기

컴포넌트 A와 컴포넌트 B가 있다고 하자.

컴포넌트 A가 어떤 실행을 할 때마다 컴포넌트 B의 특정 행위가 실행되어야 한다.

의존성 분리를 위해 컴포넌트 A에서 이벤트 디스패처를 통해 이벤트를 리슨 하고 있는 컴포넌트 B에게 이벤트를 발생시켜 컴포넌트 B가 액션을 취할 수 있도록 한다.

이렇게 함으로써 컴포넌트 A는 이벤트 디스패처에 의존하고 컴포넌트 B와는 분리될 수 있다.

하지만 B는 A에 대한 의존성을 완전히 분리하지 못했다. B는 A가 제공하는 자료구조를 알아야한다.

A와 B의 완전한 분리를 위해서는 A에서 제공하는 데이터 형식과 B에서 제공받는 데이터 형식에 표준 규격을 정해야 한다.

 

이때 필요한 개념이 Shared Kernel 이라는 것이다.

 

예제 추가

 

Shared Kernel

모든 컴포넌트에서 의존하는 공유 자원

컴포넌트간 통신 시 각각의 컴포넌트가 Shared Kernel에 의존함으로써 컴포넌트간 분리 가능

Shared Kernel의 변경은 모든 컴포넌트에 영향을 미침

-> 따라서 Shared Kernel은 최대한 작게 구성해야함

 

예제

 

만약 polyglot(다양한 언어로 작성)한 MSA환경이라면 Shared Kernel은 언어에 상관없이 지원되야 한다.

따라서 프로그래밍 언어가 다르기에 이벤트 클래스를 Shared Kernel로 사용하는게 아니라 이벤트에 대한 정의(이벤트명, 속성, 메서드)를 Shared Kernel로 선언하여 언어에 상관없이 JSON객체로 변환(사용)할수 있도록 하는 것이 polyglot하며 MSA환경에서 더욱 적절하다.

 

예제

 

물론이는 Monolythic하며 non-polyglot 환경이나 polyglot MSA 환경 모두 적절한 접근일 수 있다.

 

하지만 이벤트 전달이 비동기로 이루어지고 이벤트를 받는 컴포넌트에서 즉시 로직을 수행해야하는 경우 위의 접근 방식은 적절하지 않다.

 

왜냐하면 이벤트 방식은 이벤트 디스패처가 이벤트 리스너에게 이벤트를 전달할 수는 있지만 리스너의 처리결과를 응답 받을 수는 없다. 만약에 이벤트방식으로 구현한다면 컴포넌트 B에도 이벤트 디스패처를 만들어야 하고 이는 결국 컴포넌트 A와 B가 통신해야하는 상황에서 두 컴포넌트 모두 이벤트 디스패처와 이벤트 리스너를 모두 구현하게 만들어 시스템의 복잡성을 증가시킨다.

 

discovery service는 이벤트 주고 받는것 가능한가?

 

만약, 컴포넌트 A가 B에게 HTTP 요청을 하고 B가 즉시 수행결과를 A에게 반환해야 하는 경우라면 

완전한 컴포넌트 분리를 위해 discovery service를 고려해야한다.

discovery service는 누가 요청을 보냈는지 요청을 어느 컴포넌트로 보냈는지를 알아야한다. 

그래야 요청을 적절한 컴포넌트에 전달하고 응답 결과를 다시 요청한 컴포넌트에 전달할 수 있다.

 

이러한 처리방식은 컴포넌트가 discovery service를 의존하게 하지만, 컴포넌트간 의존성은 분리시킬수 있게한다.

 

다른 컴포넌트로 부터 데이터 받기

 

다른 컴포넌트의 데이터를 변경하는 것은 적절하지 않다. 데이터를 가진 컴포넌트만이 변경을 할 수 있다.

청구 컴포넌트가 계좌 컴포넌트의 고객명이라는 데이터에 접근한다고 하자.

청구 컴포넌트는 쿼리를 포함하고 있을 것이다.



6. 제어의 흐름

제어의 흐름은 user에서 application core를 거쳐 infrastructure에 도달하고 다시 이 경로로 돌아온다.

그렇다면 클래스를 어떻게 구성할 것인가, 어떤 것이 어떤 것에 의존해야하는가?

 

i) Comman/Query Bus가 없는 경우

이 방식에서는 컨트롤러가 Application Service와 Query Object에 의존할 것이다.

 

  • Controller는 Application service의 인터페이스에 의존한다.
  • Query Object는 쿼리를 가지고 있으며 raw data를 사용자에게 그대로 전달 할 것이다.
  • Application Service는 유스케이스 로직을 포함한다.
    Application Service는 Repository에 의존한다.
    Application Service가 유스케이스 로직을 실행하며 구독자에게 알리고 싶은 이벤트는
    Event Dispatcher를 통해 제공할 수 있다.
    이런 경우에는 Event Dispatcher를 의존한다.
  • Repository는 Entity를 리턴한다.(이때 엔티티는 domain service에 속한 엔티티이다.)

 

여기서 흥미로운 부분은 우리가 Persistence와 Repository에 각각 interface를 뒀다는 것이다.
이는 굉장히 불필요해보일 수 있지만, 각각의 인터페이스는 서로 다른 목적이 있습니다.

  • Persistence interface는 ORM 추상화로 우리가 Application core의 변경없이 ORM을 바꿀 수 있도록 합니다.
  • Repository Interface는 Persistence engine의 높은 수준의 추상화이다.
    만약 Persistence API와 Persitence Adapter의 변화 없이 Mysql에서 MongoDB로 전환을 하는 경우
    쿼리 문법이 바뀌기 때문에 새로운 리포지토리를 만들어 변경시킬 수 있다.

 

* Persistence engine은 하이버네이트를 예로 들면 개발자가 작성한 리포지토리 명령어를 통해 DB에 데이터 액세스, 객체-데이터베이스 매핑을 처리하는 클래스 파일들, 즉 하이버네이트 라이브러리로 보면 된다.

 

위 글에서 빨간색으로 표시한 부분은 사실 이해가 잘 되지 않는 부분이다.

글의 저자는 Repository adapter를 application core안에 위치 시켰다.

 Repository adapter를 infra structure에 위치시키는 나의 경우 위 내용이 제대로 이해되지 않는다.

 

마침 관련된 내용이 댓글로 존재했다.

Q: The repository interface is an abstraction on the persistence engine itself. This definition confuses me greatly. What do you mean by the persistence engine? IMHO, repositories use the persistence interface to do its own job. Is hiding the switch from MySQL to MongoDB the responsibility of the persistence interface?

A: Well, you can take it as far as you want.
I’ve used different approaches that were all successful.
You can use the repository interface as a port to the persistence, which puts the repository implementations in the infrastructure. Or you can have a persistence port in front of an adapter to the ORM, which allows you to put the repository implementations in the application core, because they would then depend on the persistence port.
These are both valid approaches.

 

Mysql에서 MongoDB의 변화를 숨기는 책임이 PersistenceAPI에 있냐고 물어보는 질문이다.

(ORM adapter와 의존성을 분리 시키는 것이 persitence API Port의 역할이냐고 물어보는 것)

저자는 사용하기 나름이라고 답했다. Repository Interface를 Port로 사용하여 ORM Adapter를 숨겨도 좋다고 하였다.

두 접근 방법 보다 유효한 방식이라고 답하였다.

 

따라서 나의 경우 Repository interface를 Port로 사용하므로 위의 빨간색 내용을 아래와 같이 수정하면

 

  • Persistence interface는 ORM 추상화로 우리가 Application core의 Repository adapter의 변경없이 ORM을 바꿀 수 있도록 합니다.
    => JPA를 사용한다면 우리는 application.yml을 통해 설정만 바꾸면 코드 수정 없이 진행가능.
        (물론 native 쿼리 같은 것들 있으면 수정필요)
  • Repository Interface는 Persistence engine의 높은 수준의 추상화이다.
    만약 Persistence API와 Persitence Adapter의 변화 없이  Mysql에서 MongoDB로 전환을 하는 경우
    쿼리 문법이 바뀌기 때문에 새로운 리포지토리를 만들어 변경시킬 수 있다.
    따라서 Application Core의 변경없이 기능을 확장할 수 있다.

 

※ 참고로. "Persistence API와 Persitence Adapter의 변화 없이" 라는 부분은 논리적 가정이기에 안 그어도 되지만 내가 사용하는 JPA에서는 RDB와 Nosql 모두 지원하는 adapter 존재하지 않아 취소선 그었다. 저자의 의도에 대해 이해하려면 취소선 없다고 생각하고 읽는게 더욱 개념파악에는 도움 될 것이다.(나는 RDB와 Nosql 모두 지원하는 ORM 프레임워크를 본적이 없어 이해가 잘 되지 않는다.)

 

하지만 이 내용에서 강조하는 것은 DIP를 통한 Application Core를 지키는 것

(application core가 Repository Interface에 의존하므로 구현체 변경이 영향을 끼치지 않음)

OCP를 통한 Infrastructure의 유연한 변경을 얘기하는 것이다.



ii) Comman/Query Bus 방식

 

Command/Query Bus를 사용하더라도 아키텍쳐 구조는 거의 바뀌지 않는다. 다만 controller가 Bus와 Command, Query를 의존할 뿐이다. controller는 Command와 Query를 인스턴스화하고 bus를 통해 적절한 handler에게 전달할 것이다.

 

아래 아키텍쳐 구조를 보면 command handler는 결국 application service를 사용한다.

그러나, 이것은 항상 필요로 한 것은 아니다. 실제로 핸들러는 유스케이스의 모든 로직을 포함하는 경우가 대부분이다. 우리가 해야할 일은 다른 handler에서 재사용할 가능성이 높은 로직들을 Application Service로 분리시켜야하는 것이다.

 

아키텍쳐 구조를 보면 Bus와 Command, Query, Handler와 의존관계가 없는 것을 볼 수 있다.

이것은 의존성 분리를 위해 각각의 객체는 서로의 존재를 알면 안된다.

Bus가 어떤 handler를 사용해야하는지 아는 방법은 단순한 config 설정을 통해 이루어져야한다.

(의존관계 설정은 config로)

 



결론

 

목표는 느슨한 결합과 높은 응집도이다. 그래야 쉽고 빠르고 안정적인 변화를 만들 수 있다.

 

application은 우리의 도메인 지식이 적용될 필요가 있는 구체적인 유스케이스인 현실세계 영역이다. 

application은 실제 아키텍쳐가 어떻게 보일지 결정한다.

 

우리는 이제까지의 내용을 정확하게 이해할 필요가 있지만 우리의 application이 원하는 것이 무엇인지 알 필요가 있다.

또한 의존성 분리와 응집도를 위해 얼마나 희생할 수 있는지도 생각해야한다.

 

반응형