Hexagonal Architecture - Persistence Adapter

육각형 아키텍처에서 영속성 어뎁터를 구현해보자.

의존성 역전

다음은 영속성 어뎁터가 애플리케이션 서비스에 영속성 기능을 제공하기 위해, 의존성 역전 원칙을 적용했다.

애플리케이션 서비스에서 영속성 기능을 사용하기 위해, 포트 인터페이스를 호출한다. 이 포트는 영속성 작업을 수행하고 DB 와 통신할 책임을 가진 영속성 어뎁터 클래스에 의해 구현된다.

육각형 아키텍처에서의 영속성 어뎁터는 outgoing 어뎁터이다. 애플리케이션에 의해 호출된 뿐, 호출되지 않기 때문이다.

중요한 것은, 영속성 계층에 대한 의존성을 없애기 위해 간접 계층인 포트를 사용하고 있다는 것이다. 그래서, 영속성 코드를 수정하더라도 코어 코드를 변경하지 않을 수 있다.

영속성 어뎁터의 책임

  1. 입력을 받는다. (포트 인터페이스를 통해)
  2. 입력을 DB 포맷으로 매핑한다. (DB 를 쿼리하거나 변경하는데 사용할 수 있는 포맷으로)
  3. 입력을 DB 에 보낸다.
  4. DB 의 출력을 애플리케이션 포맷으로 매핑한다.
  5. 출력을 반환한다.

포트 인터페이스 나누기

Read more

Hexagonal Architecture - Web Adapter

육각형 아키텍처에서 웹 어뎁터를 구현해보자.

의존성 역전

웹 어뎁터는 incoming adapter 이다. 외부에서 요청을 받아 애플리케이션 코어를 호출해서 무슨 일을 해야하는지 알려준다.

애플리케이션 계층은 웹 어뎁터가 통신할 수 있는 포트를 제공한다. 서비스는 이 포트를 구현하고, 웹 어뎁터는 이 포트를 호출한다.

웹 어뎁터와 유스케이스 사이에 간접 계층 (port) 를 두는 이유는, 애플리케이션 코어가 외부 세계와 통신하는 명세가 포트이기 때문이다. 포트를 적절한 곳에 두면, 외부와 어떤 통신을 하고 있는지 정확히 알 수 있다.

웹 어뎁터의 책임

웹 어뎁터는 다음 일을 한다.

  1. HTTP 요청을 객체로 매핑: HTTP 요청의 컨텐츠와 파라미터를 객체로 직렬화
  2. 권한 검사
  3. 입력 유효성 검증
  4. 입력을 유스케이스 입력 모델로 매핑
  5. 유스케이스 호출
  6. 유스케이스의 출력을 HTTP 로 매핑
  7. HTTP 응답을 반환

컨트롤러 나누기

Read more

Hexagonal Architecture - Usecase

육각형 아키텍처에서 유스케이스를 구현해보자.

Domain Model

육각형 아키텍처는 도메인 모델의 중심 아키텍처이다.
도메인 엔티티를 먼저 만들자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.example.buckpal.domain

data class Account(
private val id: AccountId,
private val baselineBalance: Money,
private val activityWindow: ActivityWindow
) {

fun calculateBalance(): Money =
Money.add(
this.baselineBalance,
this.activityWindow.calculateBalance(id)
)

fun withdraw(money: Money, targetAccountId: AccountId): Boolean {
if (!mayWithdraw(money)) {
return false
}

val withdrawal = Activity(
id,
id,
targetAccountId,
LocalDateTime.now(),
money
)
activityWindow.addActivity(withdrawal)

return true
}

private fun mayWithdraw(money: Money): Boolean =
Money.add(
calculateBalance(),
money.negate()
)
.isPositiveOrZero()

fun deposit(money: Money, sourceAccountId: AccountId): Boolean {
val deposit = Activity(
id,
sourceAccountId,
id,
LocalDateTime.now(),
money
)
activityWindow.addActivity(deposit)

return true
}
}

Usecase

이제 입금, 출금을 할 수 있는 Account 엔티티가 있으므로, 바깥 방향으로 나가자.
일반적으로 유스케이스는,

  1. 입력을 받음 (from Incoming Adapter)
  2. 비즈니스 규칙을 검증
  3. 모델 상태를 조작
  4. 출력을 반환

유스케이스는 일반적으로 비즈니스 규칙을 충족하면,
도메인 객체의 상태를 바꾼 뒤에 영속성 어뎁터를 통해 구현된 포트로 상태를 전달해서 저장될 수 있게 한다.
또한, 다른 outgoing 어뎁터를 호출할 수도 있다.

마지막으로 outgoing 어뎁터에서 온 출력 값을, 유스케이스를 호출한 어뎁터로 반환할 출력 객체로 변환해서 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.buckpal.application.service

class SendMoneyService(
private val loadAccountPort: LoadAccountPort
private val accountLock: AccountLock
private val updateAccountStatePort: UpdateAccountStatePort
) : SendMoneyUseCase {

fun sendMoney(command: SendMoneyCommand): Boolean {
// TODO: 비즈니스 규칙 검증
// TODO: 모델 상태 조작
// TODO: 출력 값 반환
}
}
Read more

Hexagonal Architecture

육각형 아키텍처의 패키지 구조를 정리한다.
본인의 계좌에서 다른 계좌로 송금을 하는 “송금하기” usecase 를 예로 들어보자.

계층으로 구성한 패키지 구조

우선, 계층으로 코드를 구성해보자.

  • buckpal
    • domain에
      • Account
      • Activity
      • AccountRepository
      • AccountService
    • persistence
      • AccountRepositoryImpl
    • web
      • AccountController

웹, 도메인, 영속성 계층에 대해 전용 패키지를 구성했다.
그리고, 의존성 역전 원칙을 이용해서 의존성이 domain package 에 있는 도메인 코드만을 향한다.
domain package 에 AccountRepository 를, persistence package 에 AccountRepositoryImpl 를 두어서 의존성을 역전시켰다.

계층으로 구성한 패키지 구조의 문제

첫 번째로, application 의 기능 조각을 구분 짓는 package 경계가 없다.
사용자를 관리하는 기능이 추가되면, 서로 연관되지 않은 기능들이 부수효과를 일으킬 수 있다.

  • buckpal
    • domain
      • Account
      • Activity
      • AccountRepository
      • AccountService
      • UserService <—- HERE
      • UserRepository <—- HERE
      • User <—- HERE
    • persistence
      • AccountRepositoryImpl
      • UserRepositoryImpl <—- HERE
    • web
      • AccountController
      • UserController <—- HERE

두 번째로, 애플리케이션이 어떤 usecase 를 제공하는지 파악할 수 없다.
AccountService, AccountController 가 어떤 usecase 를 구현하는지 파악할 수 없다.

세 번째로, 패키지 구조를 통해서 아키텍처를 파악할 수 없다.

Read more

의존성 역전

계층형 아키텍처의 대안에 대해 정리한다.

단일 책임 원칙

단일 책임 원칙이란, 컴포넌트를 변경하는 이유는 오직 하나 뿐이어야한다는 것이다.

변경할 이유는, 컴포넌트 간 의존성을 통해 쉽게 전파된다.
컴포넌트 A 는 다른 컴포넌트 들에 의존하고, 컴포넌트 B 는 의존하는 것이 전혀없다.
컴포넌트 B 를 변경할 유일한 이유는 새로운 요구 사항에 의해 B 의 기능을 바꿔야할 때 뿐이다.
컴포넌트 A 는 다른 컴포넌트가 바뀌면 같이 바뀐다.
하나의 컴포넌트를 바꾸는 것이, 다른 컴포넌트의 실패 원인이 될 수 있다.

의존성 역전 원칙

계층형 아키텍처에서 계층 간 의존성은 항상 다음 계층인 아래 방향을 가리킨다.
그래서, 영속성 계층을 변경할 때마다, 도메인 계층도 변경해야한다.
이 의존성을 제거해보자.

  1. 도메인 코드는 엔티티의 상태를 변경하는 일을 중심으로 하기 때문에 엔티티를 도메인 계층으로 올리자.
  2. 그런데 이렇게 하니, 영속성 계층의 repository 가 도메인 계층의 엔티티에 의존하게 되어 circular dependency 가 발생한다.
  3. 이 부분에 DIP 를 적용하자. 도메인 계층에 repository interface 를 두는 것이다.

클린 아키텍처

클린 아키텍처에서는, 비즈니스 규칙의 테스트를 쉽게 하고, 비즈니스 규칙이 프레임워크, DB, UI, 그 밖의 외부 애플리케이션이나 인터페이스로부터 독립적이다.
이것은, 도메인 코드가 바깥으로 향하는 의존성이 없어야 가능하다.
대신, 의존성 역전 원칙으로 모든 의존성이 도메인 코드를 향한다.

Read more