본문 바로가기
👨‍💻 프로그래밍/Architecture

핵사고날? 클린 아키텍처? DDD?

by 개발자 진개미 2024. 1. 28.
반응형

🐜 취준생들의 단골멘트

취준생들의 이력서에 단골로 등장하는 단어가 있습니다. 이름도 멋진 헥사고날 Architecture입니다.

처음 헥사고날 Architecture류의 용어들 (헥사고날 Architecture, DDD, Clean Architecture)를 접했을 때 저는 쓸데없이 복잡하고 혼란스러운 이것들을 왜 써야 하는지 도저히 이해할 수가 없었습니다.

그도 그럴게, 헥사고날 Architecture가 왜 좋은지 알려면 Business Logic이 꽤 복잡한 애플리케이션을 다뤄야 하기 때문입니다. 간단한 CRUD 애플리케이션에 핵사고날 아키텍처를 써 봤자 쓸데없이 복잡할 뿐이죠.

그럼 헥사고날 Architecture가 왜 태어났고 왜 유용한지 여태까지 이해하고 경험한 내용들을 써 보겠습니다.


🐜 용어부터 정리하자

모든 학문은 이름짓기부터 시작합니다. 이름을 지으면 분류할 수 있게 되고, 분류할 수 있으면 분석할 수 있게 됩니다. 이것저것 혼란스러운 용어부터 정리하고 본격적으로 달려보겠습니다!

아키텍처 (Architecture) 소프트웨어를 어떻게 만들까?의 규칙을 정해 논 겁니다. 
계층형 아키텍처(Layered Architecture) 계층에 따라 각각 역할을 부여해서 만드는 방식입니다.
보통 표현 계층, 서비스 계층, 영속성 계층 등으로 나뉩니다.
도메인 주도 설계 (DDD) 소프트웨어를 도메인을 위주로 만들자는 생각입니다.
복잡한 도메인 여러개가 상호작용하면 계층형 아키텍처의 서비스 계층이 너무 비대해지고, 복잡성을 감당하기 힘들어집니다. 그리고 도메인 정책을 서비스로 푸는 번역 비용도 너무 비싸집니다. 이러느니 차라리 도메인 용어를 코드에서도 사용하고 (유비쿼터스 언어) 도메인을 풍부하게 만들자는 방법론입니다.
클린 아키텍처 (Clean Architecture) DDD와 비슷하게 도메인 모델을 풍부하게 만드는걸 목적으로 하는 아키텍처입니다.
핵사고날 아키텍처 (Hexagonal Architecture) Clean Architecture를 구현하기 위한 구체적인 방법론이라 볼 수 있습니다.

🐜 본질부터 생각해 보자

헥사고날류 아키텍처의 본질은 도메인을 지키자입니다. 도메인이 망가지면 애플리케이션은 끝입니다.

자주 쓰는 예시로 송금이 있는데요. “계좌”라는 도메인이 있다고 해 봅시다. 이 도메인을 잘 지키지 않고 간단한 외부 API 호출로 쉽게 잔액을 바꿀 수 있다고 해 봅시다. 재앙이 따로 없겠죠?

그래서 도메인을 함부로 바꾸지 못하게 해야 합니다. 오직 바꿔야 하는 경우에만 모든 검증을 거치고 바꾸게 해야 합니다. 이 바꿔야 하는 경우를 UseCase이라고 부릅니다.

그럼 UseCase에 Business Logic이 있겠구나! 생각하실 수 있습니다. 그건 아닙니다. UseCase에 Business Logic이 있으면 같은 코드를 반복해야 하는 경우가 있고, 반복되는 코드는 실수를 야기하고, 실수는 도메인을 해치기 때문입니다.

계좌 도메인의 예를 이어가 봅시다 들어봅시다. 계좌 도메인에서 흔히 일어나는 일은 출금이겠죠? 근데 출금도 여러 UseCase에 의해 일어날 수 있습니다.

  • 직접 돈을 보냄
  • 지로 (전기료, 가스료, 세금 등)을 냄
  • 자동이체

각각의 UseCase는 결국 똑같은 동작인 계좌 잔액을 특정 금액만큼 줄인다를 하고, 이 동작은 계좌 잔액이 줄일 금액보다 같거다 많아야 한다는 Business Logic이 존재합니다. 이 경우 각각의 UseCase에서 Business Logic을 구현하는게 아니라, 도메인에서 Business Logic을 구현하고 UseCase는 여러 상호작용을 하는 역할입니다. Port를 통해 API를 호출하거나 DB를 통해 영속화를 진행하고, Web/App에서 온 요청을 검증해 도메인에게 안전한 정보를 전달해 Business Logic의 수행을 돕습니다.

class 지로Service(
	private val accountPort: AccountPort,

	private val 정부24: 정부24Port,
	private val 한전: 한전Port,
) : 지로UseCase {

    @Transactional
    override fun payElectricBills(
    	user: User
        now: LocalDateTime,
    ) {
        val billsToPay = 한전.paymentAmount()
        val account = accountPort.getPrimaryAccount(user.userNo)

        if (!account.canPay(billsToPay)) {
            return
        }

        val transactionId = account
                    .pay(billsToPay)
                    .let { accountPort.update(it) }

        한전.notifyPayment(transactionId)
        정부24.notifyPayment(transactionId)
    }
}

 

위 코드를 보면 UseCase에 사실상의 Business Logic은 없습니다. 단지 우리가 큰 그림에서 상상하는 흐름이 있을 뿐이고, 이게 핵심입니다. 소프트웨어는 결국 복잡성을 인간의 뇌가 감당할 수 있을 때 까지 추상화를 진행해 복잡한 시스템을 다루는 예술이니깐요.

전기세를 내려면 당연히 아래의 과정을 거치겠죠?

  1. 낼 전기세가 있는지 확인하고
  2. (낼 전기세가 있으면) 우선 내 계좌에서 전기세를 낼 수 있는지 확인하고
  3. (낼 수 있으면) 내고 (계좌 잔액을 전기세만큼 줄이겠죠?)
  4. 마지막으로 전기세를 냈다고 한전에 알림

이 과정에서 사실 어마어마한 Business Logic이 들어가 있을 겁니다. 예를 들어 내 계좌에서 전기세를 낼 수 있는지를 확인하는 과정에서 있는 Business Logic을 감히 상상만 해 보자면

  • 잔액이 충분한지 확인
  • 보이스피싱 계좌가 아닌지 확인
  • 출금동의를 했는지 확인
  • 은행이 점검중인지 확인
  • 공휴일에 처리할 수 있는지 확인

등등이 있을 수 있겠죠. 이 모든 동작이 만약 1개의 함수에 있었다면 (심지어는 함수를 나누더라도) 너무 복잡해서 파악하기 어려울 겁니다. 또 간단한 실수로 도메인이 망가져 버리겠죠!


🐜 도메인을 보호하기 위한 방법

최대한 외부와 격리

 

  • 외부에서 직접 조작 불가능하게 (UseCase를 통해 조작)
  • 외부의 변화에 반응하지 않게 (의존성이 단방향으로, 외부에서 내부로 흐름. Port를 통해서만 소통함)

모든 애플리케이션은 결국 유저가 규칙/상태에 따라 데이터를 조작하는 것입니다. 여기서 유저의 데이터를 저장하기 위해 DB를 쓸 수도 있고, 속도를 빠르게 하기위해 Redis를 쓸 수도 있고, 상태를 가져오기 위해 외부 API를 호출할 수도 있지만 결국 가장 핵심은 규칙/상태와 데이터입니다. 여기서 모든 도메인 조작 (데이터 조작)은 규칙/상태의 검증을 거쳐서 이루어 져야 하기 때문에 도메인을 직접 접근할 수 없고 Port를 통해 소통하고 UseCase를 통해 도메인을 조작합니다.


🐜 현실적인 한계

이걸 다 지킬려면
코드량이 너무 많아지고
아키텍처가 너무 복잡해진다

 

이렇게 핵사고날 아키텍처 류의 용어들과 개념을 알아봤지만 저는 여전히 크게 마음에 들지는 않았습니다. 취지에는 공감이 갔지만, 이를 위해 너무 큰 복잡성을 감수해야 하는 느낌이였습니다. 마이크로서비스를 볼 때와 비슷한 느낌이 들었습니다. 분명 유용하지만, 정말 큰 규모의 프로젝트가 아니면 득보다는 실이 많다는 생각입니다. 다행히 저만 그렇게 느끼는건 아니고 핵사고날 아키텍처를 그대로 적용하는건 한계가 있다고 많은 분들이 인정하십니다.

하지만 그렇다고 본질적인 취지까지 모두 버릴 필요는 없기에, 아래 원칙들을 지키는 연습을 해 볼 생각입니다.

  • 도메인 모델을 풍부하게 하기
  • 외부 의존성은 Port를 통해 간접적으로 소통하기
  • 비대해진 Service 계층을 Use Case를 통해 잘게 나눠서 도메인 조작하기

반응형

댓글