728x90
전글: [만들면서 배우는 클린 아키텍처] 2. 의존성 역전하기
코드를 보는 것만으로도 어떤 아키텍처인지 파악할 수 있다면 좋지 않을까?
지금부터 코드를 구조화하기 위해 여러 가지 방법을 살펴보자
계층으로 구성하기
buckapl
|--- domain
| |----- Account
| |----- Activity
| |----- AccountRepository
| |----- AccountService
|--- persistence
| |----- AccountRepositoryImpl
|--- web
| |----- AccountController
코드를 구조화하는 첫 번째 접근법은 계층을 이용하는 것으로 위처럼 코드를 구성할 수 있다. 간단한 구조의 계층은 가장 적합한 구조가 아닐 수 있으므로 의존성 역전 원칙을 적용해서 의존성이 domain 패키지에 있는 도메인 코드만을 향하도록 했다.
하지만 적어도 세 가지 이유로 이 패키지 구조는 최적의 구조가 아니다.
문제점
애플리케이션의 기능 조각(functional slice)이나 특징(feature)을 구분 짓는 패키지 경계가 없다.
- 이 구조에서 사용자를 관리하는 기능을 추가해야 한다면?
- web 패키지에 UserController를 추가
- domain 패키지에 UserService, UserRepository, User를 추가
- persistence 패키지에 UserRepositoryImpl을 추가
- 추가적인 구조가 없다면, 아주 빠르게 서로 연관되지 않은 기능들끼리 예상하지 못한 side effect를 일으킬 수 있는 클래스들의 묶음으로 변모할 수 있다.
애플리케이션이 어떤 유스케이스들을 제공하는지 파악할 수 없다.
- 특정 기능을 찾기 위해서는 어떤 서비스가 이를 구현했는지 추측해야 하고, 해당 서비스 내의 어떤 메서드가 그에 대한 책임을 수행하는지 찾아야 한다.
패키지 구조를 통해서는 우리의 목표로 하는 아키텍처를 파악할 수 없다.
- 어떤 기능이 웹 어댑터에서 호출되는지, 영속성 어댑터가 도메인 계층에 어떤 기능을 제공하는지 한눈에 알아볼 수 없다.
- 인커밍 포트와 아웃고잉 포트가 코드 속에 숨겨져 있다.
기능으로 구성하기
buckpal
|-- account
|-- Account
|-- AccountController
|-- AccountRepository
|-- AccountRepositoryImpl
|-- SendMoneyService
계층 패키지들을 모두 없애고 계좌와 관련된 모든 코드를 최상우의 account 패키지에 넣었다.
“계층으로 구성하기” 방법의 몇 가지 문제를 해결해보자
- 기능을 묶은 그룹은 account와 같은 레벨의 패키지로 들어가고, 패키지 외부에서 접근하면 안 되는 클래스들에 대해 package-private 접근 수준을 이용해 패키지 간의 경계를 강화할 수 있다.
- 패키지 경계를 package-private 접근 수준과 결합하면 각 기능사이의 불필요한 의존성을 방지할 수 있다.
- 책임을 좁히기 위해 SendMoneyService와 같이 송금하기 기능을 구현한 클래스를 클래스명으로 바로 찾을 수 있다.
- 소리치는 아키텍처: 애플리케이션의 기능을 코드를 통해 볼 수 있게 만드는 것
- 물론 계층형 패키지 구조 방식에서도 가능하다.
문제점
- 계층의 의한 패키징 방식보다 아키텍처의 가시성을 훨씬 더 떨어뜨린다.
- 어댑터를 나타내는 패키지명도 없다
- 인커밍 포트, 아웃 고잉 포트도 없다.
- SendMoneyService가 AccountRepository인터페이스만 알고 구현체를 알 수 없게 했지만 패키지 내부 package-private 접근 수준을 이용해 영속성 코드에 의존하는 것을 막을 수 없다.
아키텍처적으로 표현력 있는 패키지 구조
buckpal
|-- account
|-- adapter
| |-- in
| | |-- web
| | |-- AccountController
| |-- out
| | |-- persistence
| | |-- AccountPersistenceAdapter
| | |-- SpringDataAccountRepository
|-- domain
| |-- Account
| |-- Activity
|-- application
|-- SendMoneyService
|-- port
|-- in
| |-- SendMoneyUseCase
|-- out
| |-- LoadAccountPort
| |-- UpdateAccountStatePort
헥사고날 아키텍처에서 구조적 핵심요소는 엔티티, 유스케이스, 인커밍/아웃고잉 포트, 인커밍/아웃고잉 어댑터다.
- account 패키지: 최상위에 Account 관련 유스케이스를 구현한 모듈임을 나타냄
- domain 패키지: 유스케이스 다음 레벨로 도메인 모델이 속한 패키지
- application 패키지: 도메인 모델을 둘러싼 서비스 계층을 포함하는 패키지
- SendMoneyService
- 인커밍 포트 케이스 SendMoneyUseCase를 구현
- 아웃고잉 포트 인터페이스이자 영속성 어댑터에 의해 구현된 LoadAccountPort와 UpdateAccountStatePort를 사용
- SendMoneyService
- adapter 패키지: 애플리케이션 계층의 인커밍 포트를 호출하는 인커밍 어댑터와 애플리케이션 계층의 아웃고잉 포트에 대한 구현을 제공하는 아웃고잉 어댑터 포함하는 패키지
패키지 접근 수준
- 모두 package-private 가능
- adapter 패키지
- adapter 패키지 내의 모든 클래스들은 application 패키지 내에 있는 포트 인터페이스를 통하지 않고는 바깥으로 호출되지 않기 때문
- 일부 public 지정 필요
- application 패키지의 포트 클래스
- 어댑터에서 접근 가능해야 하는 포트들은 public 이어야 하기 때문
- domain 패키지의 도메인 클래스
- 도메인 클래스들은 서비스, 또는 어댑터에서도 접근 가능하도록 public 이어야 한다.
- application 패키지의 포트 클래스
- package-private 가능
- application 패키지의 서비스 클래스
- 서비스는 인커밍 포트 인터페이스 뒤에 숨겨질 수 있기 때문에 public일 필요가 없다.
- application 패키지의 서비스 클래스
장점
- 이러한 패키지 구조는 모델-코드 갭(아키텍처-코드 갭)을 효과적으로 다룰 수 있다.
- 패키지 구조가 아키텍처를 반영할 수 없다면 결국 관심사의 분리가 불가능해질 것이다.
- 어댑터 코드를 자체 패키지로 이동시키면 필요할 경우 하나의 어댑터를 다른 구현으로 쉽게 교체 가능하다.
- SQL 데이터베이스에서 NoSQL 데이터베이스로 교체해야 하는 경우 아웃고잉 포트들만 새로운 어댑터 패키지에 구현하고 기존 패키지를 지우면 된다.
- DDD 개념을 직접적으로 대응시킬 수 있다.
- account 같은 상위 레벨 패키지는 다른 바운디드 컨텍스트와 통신할 전용 진입점과 출구(포트)를 포함하는 바운디드 컨텍스트에 해당한다.
모델-코드 갭(model-code gap) 아키텍처 모델에는 항상 코드에 매핑할 수 없는 추상적인 개념, 기술 선택 및 설계 결정이 혼합되어 있다. 최종 결과는 모델이 정한 구성 요소의 배열과 반드시 일치하지 않는 소스 코드가 될 수 있다.
의존성 주입의 역할
클린 아키텍처의 가장 본질적인 요건은 애플리케이션 계층이 인커밍/아웃고잉 어댑터에 의존성을 갖지 않는 것이다.
인커밍 어댑터(ex 웹 어댑터)
- 제어의 방향이 어댑터와 도메인 코드 간의 의존성 방향과 같아서 쉽다.
- 어댑터는 단순히 애플리케이션 계층에 위치한 서비스를 호출할 뿐이다.
- 그럼에도 불구하고 계층 진입점(경계 구분) 구분 짓기 위해 실제 서비스를 포트 인터페이스를 구현하도록 한다.
아웃고잉 어댑터(ex 영속성 어댑터)
- 제어 흐름이 반대 방향으로 의존성을 돌리기 위해 의존성 역전 원칙을 이용
- 애플리케이션 계층에 포트 인터페이스를 만들고, 어댑터에 해당 포트 인터페이스를 구현한 클래스(아웃 고잉 어댑터)를 둔다.
- 포트 인터페이스를 구현한 실제 아웃 고잉 어댑터 객체를 누가 애플리케이션 계층에 제공해야 할까?
- 애플리케이션 계층에서 수동 초기화는 어댑터에 대한 의존성 추가
- 의존성 주입 활용
- spring ioc
728x90
'design & development' 카테고리의 다른 글
[만들면서 배우는 클린 아키텍처] 5. 웹 어댑터 구현하기 (1) | 2024.11.17 |
---|---|
[만들면서 배우는 클린 아키텍처] 4. 유스케이스 구현하기 (0) | 2024.11.16 |
[만들면서 배우는 클린 아키텍처] 2. 의존성 역전하기 (4) | 2024.11.13 |
[만들면서 배우는 클린 아키텍처] 계층형 아키텍처의 문제점 (0) | 2024.11.12 |
DDD? 클린 아키텍처? 헥사고날 아키텍처? (1) | 2024.11.11 |