• https://engineering.linecorp.com/ko/blog/port-and-adapter-architecture/

  • 의존성을 없애려고하니 계층이 계속 생기는데..?

    • 제한을 어떻게 나눌것인가?

    • 이것이 도메인에 대한 지식 수준 차이일까?

    • Service/Component/Repository/Controller와 같은 계층으로 나눌수는 없을까?

      • 이것으로 나누면 결국 계층형 아키텍처이지 않을까?

  • 익숙하지 않는 '포트-어댑터' 아키텍처를 팀원 모두 학습하는 비용은?

    • 도메인별로 git repository 자체를 나누고, msa로 가면서 잘게 나눠진 계층형 아키텍처(처럼 보이는) 구조가 더 빠른 생산성을 가지진 않을까?

    • '포트-어댑터' 아키텍처는 팀에 종속되는 아키텍처 구조가 아니므로 학습하는 것 자체가 개발자 역량에 도움이 되지 않을까

      • 설정보다 관례(CoC, convention over configuration) (ref)

  • 헥사고날 아키텍처를 잘못 이해한다면 core를 만들고, 외부 인터페이스만 별도로 만든다고 생각할수도 있을 것 같음

    • 아키텍처를 다시보면 도메인에 접근/도메인으로부터 반환되는 모든 것을 별도 객체로 만들어야 함(DB, Web 모두 동일한 추상화단계)

  • 특정 API에 비지니스는 어디에 둬야하는가? 컨트롤러? 웹용 서비스를 따로?

    • ex) 생성 API는 이미 있을 경우 에러에 기존 리소스를 포함해야 할 경우

  • 캐시를 두는 이유?

    • 로컬 캐시는 왜 둘까?

      • 각 비지니스의 연관관계를 끊고자 클라이언트나 서비스를 로컬에서 캐싱할 수도 있다고 봄

      • 다만 무분별한 추상화하기 않고, 개발자가 인지하고 캐싱할 수 있도록 분리해야함. 즉 entity, dao, repository 단에서의 캐싱이 아닌 서비스단에 있어야 함.

대략적인 유스케이스 구조

@startuml
component adapter
component usecase
component "persistence\nadapter" as persistence
component "outgoing\nadapter" as outgoing

adapter -> usecase: (1) 입력
usecase -> usecase: (2) 비지니스 규칙 검증
usecase --> persistence: (3) 상태 저장
usecase --> outgoing: (3)
outgoing ..> usecase: (4) output
usecase .> adapter: (5) output 변환 후 반환

@enduml
@startuml
hide empty field
hide empty method

package application.port.in {
  interface SendMoneyUseCase
  class SendMoneyCommand
}

package application.port.out {
  interface UpdateAccountStatePort
}

package application.service {
  class SendMoneyService
}

package domain {
  class Account
}

SendMoneyService -left-|> SendMoneyUseCase
SendMoneyUseCase -down-> SendMoneyCommand
SendMoneyService -down-> Account
SendMoneyService -> UpdateAccountStatePort

@enduml
// package application.port.in
// 인커밍 포트 인터페이스
interface SendMoneyUserCase {
    fun sendMoney(command: SendMoneyCommand): Boolean
}

// package application.port.in
// 입력 모델(input model)
data class SendMoneyCommand(
    val sourceAccountId: AccountId,
    val targetAccountId: AccountId,
    val money: Money,
) {
    init {
        require(money > 0)
    }
}

class AccountId
data class Money(
    val value: BigDecimal,
) {
    operator fun compareTo(i: BigDecimal): Int = value.compareTo(i)

    operator fun compareTo(i: Int): Int = compareTo(i.toBigDecimal())
}

interface LoadAccountPort // 아웃고잉 포트 인터페이스
class AccountLock
class UpdateAccountStatePort // 영속성 어댑터?

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

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

읽어볼 자료