Tech

제목은 DDD로 하겠습니다, 근데 이제 헥사고날을 곁들인 part 1

2025. 08. 09

작성자 : 백엔드팀 지찬규님, 김소윤님, 손수정님, 김형준님

 

“DDD와 헥사고날 아키텍처가 모든 문제를 해결해주는 마법의 은탄환이니 여러분도 써보세요” 라는 말은 하지 않습니다. 레거시 시스템에서 겪었던 이슈와 함께 DDD와 헥사고날 아키텍처를 도입한 목적을 소개드립니다. 아키텍처를 고민하시는 분들께 화해 마케팅센터 구조가 도움되길 바랍니다.

 

 

 

들어가기에 앞서


개발자라면 언젠가 더 나은 서비스를 위한 아키텍처를 설계하고 결정해야 되는 시기를 맞이합니다.

아키텍처마다 장단점이 명확하기에 어느 하나 선택하는 게 쉽지 않다며 딜레마에 빠져 고민합니다.

 

“이정도로 복잡한 서비스는 MSA로 분리하는 게 나으려나?”

“도메인 간 관심사를 분리하여 개발하고 싶으니 DDD를 도입할까?”

 

아키텍처에 정해진 정답은 없습니다. 현재 우리 팀 상황과 서비스에 어울리는 방향으로 설계하는 게 기본이죠. 화해도 기존 레거시 시스템을 운영하며 겪었던 이슈를 해소하고 향후 확장성까지 고려한 새로운 아키텍처를 신규 마케팅센터에 도입했습니다.

 

DDD(Domain Driven Design)!

헥사고날 아키텍처(Hexagonal architecture)!

 

개발자에 따라 단어만 봐도 굉장히 어렵고 어디서부터 시작해야 할지 난해하다 느낄지 모르겠습니다. 오늘은 DDD와 헥사고날 아키텍처를 기반으로 신규 서비스를 런칭하여 겪은 경험담을 공유하고자 펜을 들었습니다.

이 글은 “DDD와 헥사고날 아키텍처가 모든 문제를 해결해주는 마법의 은탄환이니 여러분도 써보세요”라는 말은 하지 않습니다. 그저 화해에서 무엇을 해결하고자 신규 서비스를 만들면서까지 새로운 아키텍처를 도입했는지, 그 구조는 어떻게 구성되었는지 소개하는 자리입니다.

 

자, 그럼 복싱계 전설인 타이슨의 어록과 함께 글을 시작해볼까요?

“누구나 그럴싸한 계획을 세운다. 처맞기 전까지는.”
“Everyone has a plan until they get punched in the mouth.”

 

 

 

1. 기존 레거시 시스템에서 겪은 불편한 진실(?)


1-1. 여러 정책이 하나로 뒤섞인 코드 뭉치들

 

다음은 흔하게 보이는 트랜잭션 스크립트 패턴 기반 예시 코드입니다.

 

 

우리가 실물 상품 주문을 개발한다면 1번 재고 검증 로직3번 재고 차감 코드는 반드시 지켜야 되는 정책이라는 걸 충분히 이해할 겁니다. 현실에서 흔하게 접하는 정책이니 크게 어려운 비즈니스 요건은 아닙니다. 이 상태에서 비즈니스 요건 추가로 다른 개발자가 뒤이어 개발한다면 기존 코드를 보고 앞서 만들어진 1, 3번 정책을 파악할 겁니다. 예시 코드는 한 손으로도 셀 수 있는 정책만 지나지 않지만, 화해 마케팅센터에 존재하는 계약 도메인만 하더라도 20~30개에 달하는 정책을 가집니다. 정책이 한두 개일 때야 어떻게 개발하든 크게 문제 없지만, 그 숫자가 늘어나면 늘어날수록 설계 시 신경써야 하는 영역이 기하급수적으로 증가합니다.

광고를 다루는 마케팅센터는 계약 도메인이 가지는 정책 사항 말고도 신경쓸 영역이 많습니다. 계약이 생성되면 광고 집행, 정산, 검수 등 다양한 도메인이 함께 유기적으로 생성되고 관리되어야 합니다. 각 도메인마다 정책을 관리하고 호출하는 함수가 서비스 레이어 한 곳에 모이면 관리가 언뜻 쉬워보이지만, 현실은 그렇지 않습니다. 정책이 복잡해질수록 로직 파악이 힘들어지고, 기존 정책을 바꿔달라는 요청이 올 때마다 개발자는 어떤 로직을 바꿔야하는지 찾는 데에 많은 시간을 할애하였습니다. 이는 곧 개발생산성이 저하되었다는 말과 동일한 이야기죠.

 

 

1-2. 애매하게 나뉘어진 경계들

화해는 파이썬과 장고 프레임워크를 이용해 서비스를 개발합니다. 그 중 django ORM은 액티브 레코드 패턴을 사용하는데요. 장고 프레임워크 특성 상 모델이 라우터, 서비스 레이어를 넘나들며 어플리케이션 어디서든 DB를 접근할 수 있는 구조를 용인합니다. 즉, 코드가 동작하는 모든 곳에서 django ORM을 통해 데이터베이스 쿼리가 발생 가능합니다.

 

 

토이 프로젝트라면 초기 생산성 측면에서 장점으로 다가올 수 있지만, 광고 도메인은 비즈니스 로직이 굉장히 복잡합니다. 그와 더불어 언제든지 다양한 광고 상품이 추가되기에 임기응변으로 대응하면 광고 별로 분기해야 할 로직이 뒤섞입니다. 만약 액티브 레코드 패턴을 사용한다면 비즈니스 로직과 DB 로직이 혼재되어 어떤 데이터가 어디서 변경이 되는지, 그 이유는 무엇이었는지 추적 관찰하는 게 힘들어 유지보수에 큰 어려움을 겪습니다.

‘그럼 레포지토리 패턴을 도입해 그 안에서만 ORM을 다루면 되는 거 아니야?’라고 생각할 수 있는데요. 맞습니다! DTO를 사용하면 장고 ORM 특성도 제한 가능하죠. 하지만 마케팅센터는 정산 등 복잡한 쿼리를 다뤄야 하는 상황이 빈번하여 ORM보다 직접 쿼리를 작성하고 관리하는 게 옳다고 판단하였습니다.

 

 

갑자기 프레임워크 이야기로 흘러갔지만, 장고 프레임워크가 가진 철학과 구조는 저희가 마케팅센터에 원하는 방향과 일치하지 않았다고 이해해주시면 감사하겠습니다.

 

장고 프레임워크가 안좋다는 이야기는 아니니 오해하진 말아주세요😃  We Love Django!

 

 

조금만 더 프레임워크 이야기를 해볼게요. 장고는 MTV 기반 프레임워크로 웹 페이지 렌더링을 위한 구조 및 편리한 어드민 기능을 가졌지만, 마케팅센터는 API 서빙 자체가 목적이기에 모두 불필요한 기능이었습니다. 물론 장고에서 설정하는 admin이나 auth, permission 같은 기능은 분명 초기 생산성을 증가시키지만, 프레임워크에 종속되어 원하는 방식으로 구현하는 데 한계로 적용해 오히려 발목을 잡는다고 여겼습니다.

이 글에서 크게 중요한 이야기는 아니지만, 여러 이유로 마케팅센터는 장고 대신 FastAPI와 SQLAlchemy 조합을 선택했습니다. FastAPI는 장고와 달리 기능이 많지 않아 가벼운 API 서버 구현이 가능합니다. SQLAlchemy는 프레임워크에 종속되지 않고 개발자가 원하는 형태로 영속성 로직을 제어할 수 있습니다. SQLAlchemy는 ORM뿐 아니라 쿼리 빌더 형태로 사용 가능하다는 장점을 지녔습니다.(SQLAlchemy에서 Core방식이라고 부릅니다) 저희는 raw SQL 대신 쿼리 빌더 형태로 SQLAlchemy를 활용해, 직접 쿼리를 작성하면서도 유지보수 측면에서 이점을 얻었습니다.

 

Core, ORM insert 비교 예시


SQLAIchemy Core ㅡ SQLAIchemy 2.0 Documentation

SQLAIchemy ORM ㅡ SQLAIchemy 2.0 Documentation

 

여기까지 요약해볼까요? 화해 마케팅센터는 아래와 같은 문제점을 해소하고자 시작하였습니다.

  • 트랜잭션 스크립트 패턴에서 구현되는 비즈니스 정책을 도메인 단위로 집약하여 로직을 효율적으로 개선
  • 서비스 레이어와 영속성 레이어 등 각 레이어들을 명확히 분리하여 사용

 

 

2. 그래서 시작한 Domain-Driven Design!


화해 광고 도메인에서 앞서 말씀드린 내용을 고민하게 된 계기가 있었습니다. 서론에서 살짝 언급했지만, 작년 초부터 화해 광고를 집행하는 파트너사를 위한 새로운 광고 어드민 시스템을 개발하는 프로젝트가 시작되었습니다.

 

 

“신규 마케팅 센터 개발 프로젝트!!”

 

 

기존 레거시 시스템 한계로 현 상태를 유지하며 개발을 지속할 경우, 향후 유지보수에 더 많은 비용이 들 수밖에 없다는 데 의견이 모아졌습니다. 특히 레거시 시스템은 다양한 광고 로직이 뒤섞여 신규 광고 상품을 추가하고 확장하는데 많은 어려움이 존재했습니다. 오랜 시간이 흐르더라도 뛰어난 확장성과 더불어 유지보수를 용이하게 만들기 위해 개발자들끼리 모여 치열한 논의를 거쳤고, DDD를 도입하게 되었습니다.

 

 

2-1. DDD는 무엇인가?

개발자라면 한 번쯤 DDD를 들어보고, 실제로 활용하시는 분들도 많이 계실텐데요.

 

 

여기서 ‘도메인’이란 무엇일까요? 도메인은 개발자가 구현해야 하는 대상, 즉 광고 계약 및 관리 어드민 시스템을 이야기합니다. DDD는 비즈니스 로직을 코드에 잘 녹여내기 위한 보편적인 패턴을 바탕으로 구현하고, 일반적으로 네 계층 구조를 갖습니다.

 

 

DDD 아키텍처

 

 

  • 표현 계층 presentation 사용자 요청을 받아 응답을 내려주는 계층입니다. 주로 API 엔드포인트를 제공하는 역할을 수행합니다.
  • 응용 계층 application 표현 계층 요청을 받아 비즈니스 기능을 구현하는 레이어입니다.
  • 도메인 계층 도메인 모델이 정의되고 핵심 로직이 구현되는 영역입니다. 예를 들면 계약 생성, 계약서 검수, 광고 소재 설정 이 해당 계층에서 정의됩니다.
  • 인프라스트럭처 계층 실제 데이터베이스 연동이나 메시지 전송 등 인프라와 관련된 내용을 담당하는 계층입니다.

이 네 가지 계층은 상위 계층에서 하위 계층을 사용하면서 다양한 기능을 수행합니다. DDD는 각 계층에서 DDD 고유한 개념과 객체를 사용합니다. 이 글에서는 그 중 핵심 개념 두 가지를 짚고 넘어가겠습니다.

 

 

바운디드 컨텍스트 Bounded Context

바운디드 컨텍스트는 DDD에서 중요한 개념입니다. 복잡한 도메인을 관리 가능한 단위로 분리하는 목적을 지닙니다. 각 바운디드 컨텍스트는 특정한 도메인 모델을 정의하고, 해당 모델은 그 컨텍스트 안에서 유효합니다. 서로 다른 바운디드 컨텍스트 간 도메인 모델이 충돌하지 않고 독립적으로 유지됩니다.

 

 

 

 

마케팅센터는 기능 단위 즉, 계약 흐름을 바탕으로 먼저 도메인을 구분했습니다. 광고 관리, 광고 준비, 광고 집행, 계정 과 같은 도메인으로 나누고, 각 도메인 내에서 특정 기능을 제공하는 하위 도메인으로 세분화했습니다.

예를 들어 Ad Manager는 광고를 관리하는 도메인입니다. Ad Manager는 광고를 생성하기 위한 계약과 구좌 및 정산과 같은 서브 도메인을 가집니다. 이 컨텍스트 안에서 계약 생성부터 광고 부킹, 취소 등과 관련된 모든 규칙과 데이터 모델이 포함됩니다.

바운디드 컨텍스트를 설정함으로써 각 영역은 독립적으로 발전하며, 여러 바운디드 컨텍스트가 한 시스템 내에서 동작해도 각 컨텍스트는 명확한 경계로 분리되어 전체 시스템이 가진 복잡성을 관리하기 쉬워집니다.

 

 

엔티티 Entity

엔티티는 고유한 식별자를 가진 객체입니다. 도메인 고유 개념을 나타내며, 실제 데이터가 포함한 중요 객체입니다. 엔티티는 시스템 내 특정 도메인을 나타냅니다. 예를 들어 아래 SlotEntity는 부킹된 구좌 수를 속성으로 가지며, ‘구좌 수 업데이트’라는 행동을 수행할 수 있습니다.

 

 

 

 

2-2. DDD를 통해 개선된 점

 

 

도메인 로직에 집중

.레이어가 명확히 분리되어 데이터베이스 연동, 클라이언트 요청이 도메인 로직 안으로 들어올 수 없습니다. 개발자는 순수 도메인과 비즈니스 로직만 구현된 코드에 집중할 수 있습니다! 더이상 유지보수를 할 때 “왜 이 비즈니스 로직은 왜 쿼리를 호출하지?”라는 말이 나올수 없습니다. 오로지 정책과 비즈니스만을 고려하여 코드를 유지보수합니다.

 

도메인 간 낮은 결합도(Coupling)

도메인 간 의존성을 최소화하여 낮은 결합도를 유지합니다. 낮은 결합도는 시스템 유연성과 확장성을 높입니다. 각 도메인은 독립적으로 동작하며, 다른 도메인에 영향을 주지 않고 변경 가능하죠.

 

정책 집약

엔티티에 정책을 집약하여 코드 가독성을 높이고 중요 정책 파악이 용이합니다. 도메인 간 정책이 섞일 일도 없습니다. 엔티티와 VO를 함께 사용하여 정책과 비즈니스 로직을 집약합니다. 정책과 비즈니스 로직은 어떤 차이가 있을까요? 저희 마케팅센터는 다음과 같이 구분했습니다.

  • 로직을 통해 DB를 수정하는 작업은 정책으로 바라보고 엔티티 내 메서드로 집약
  • 직접 DB를 수정하진 않지만, 특정 기능을 하는 작업은 비즈니스 로직으로 VO에 집약

 

 

 

 

2-3. DDD… 좋지만 쉽지는 않았다

DDD를 도입한 목적에 부합하여 다양한 효과를 보았지만, 힘들었던 부분도 있었습니다.

 

 

높은 러닝커브

많은 개발자가 DDD를 이야기하지만, 기존 뼈대를 뒤집으며 DDD를 적용하는 건 어려운 일입니다. DDD에 필요한 개념과 접근 방식을 학습하는 건 당연하고, 기존 시스템을 DDD에 걸맞게 쪼개어 적용하는 과정이 필요합니다. 특히 도메인 모델링과 관련된 작업에서 많은 논의와 시간이 소요될 수밖에 없습니다. 도메인 이해도가 낮다면 고심 끝에 적용한 모델링일지라도 잘못될 가능성도 존재합니다. 시행착오를 줄이려면 아래 네 가지를 챙기는 걸 권해드립니다

  • 바운디드 컨텍스트를 어디 수준으로 나눌 것인가?
  • 도메인과 서브도메인을 구분이 올바른가?
  • 해당 정책이 이 도메인에 존재하는 게 맞는가? 정책 집약을 잘 했는가?
  • 네이밍이 도메인과 정책을 잘 표현하는가?

 

부족한 레퍼런스

DDD 개념과 방법론은 언어에 종속되지 않는 아키텍처이기에 시중에 판매되는 여러 서적과 블로그를 보면서 익히는 게 가능합니다. 다만 주로 자바와 같은 정적 타입 언어에서 사용되어 화해가 사용하는 파이썬으로 구현된 레퍼런스는 많이 부족했습니다. 마케팅센터를 만드는 인원끼리 매일 1시간은 논의 시간을 가졌고, DDD를 도입하기 위한 여러 문제를 고민하고 협의하는 과정을 거쳤습니다. 화해 마케팅센터만의 고유한 DDD 구조를 만들어 나갔습니다.

 

이벤트 스토밍 이미지 (from ChatGPT)

 

 

물론 아쉬웠던 점도 존재합니다. 방법론 중 하나인 DDD를 코드베이스 개념과 기술로만 적용했다는 점입니다. 단순히 도메인만을 나누는 게 DDD는 아닙니다. 이벤트 스토밍과 같은 활동을 통해 도메인 전문가 그리고 함께 서비스를 만드는 PO, 디자이너, 개발자가 모여 도메인을 정의하고 바운디드 컨텍스트를 나누는 과정도 DDD 안에 포함됩니다.

저희도 이벤트 스토밍, 유비쿼터스 언어를 맞추는 등 다양한 활동을 하고 싶었지만, 기존 레거시 시스템 전체를 이관하는 개발만으로도 많은 시간이 필요하였기에 현실적으로 어려웠습니다. 그나마 다행이었던 건 레거시 시스템 정책을 기반으로 신규 시스템을 설계하면서 PO, 디자이너, 개발자 간에 도메인과 용어에 대한 이해가 대부분 일치했고, 동일한 컨텍스트에서 소통할 수 있었다는 점입니다.

 

 

저희가 정의한 DDD는 완벽하진 않지만 내부에서 치열한 논의를 지속적으로 해왔기에 소기의 목적은 달성했다고 생각합니다.

 

 

3. 헥사고날 아키텍처Hexagonal Architecture 경계를 나눠줘!


서론을 통해 경계에 대한 이야기를 잠깐 했습니다. DDD를 넘어서 도메인, 서비스, 영속성 간 경계를 명확히 나누고자 헥사고날 아키텍처를 도입하기로 결정했습니다.

헥사고날 아키텍처는 애플리케이션 도메인 로직을 외부 요소로부터 분리, 유지보수와 확장성을 높이는데 중점을 둡니다. 핵사고날 아키텍처는 포트와 어댑터를 사용하여, 포트 앤 어댑터Ports and Adapters 아키텍처로도 불립니다.

 

 

출처 : https://reflectoring.io/spring-hexagonal/

 

 

  • 애플리케이션 application
    • 비즈니스 로직과 도메인 규칙을 포함합니다. 외부 요인이 없다면 순수한 비즈니스 로직만 포함합니다.
  • 포트 port
    • 애플리케이션과 외부 영역 간 인터페이스를 정의합니다.
    • 애플리케이션으로 들어오는 요청을 처리하는 입력 포트와 애플리케이션이 외부 영역과 상호작용하는 출력 포트로 나뉩니다.
      • 포트에 대한 용어가 여러가지인데 저희팀은 inbound, outbound 포트로 정의했습니다.
  • 어댑터 adaptor
    • 포트에서 정의한 인터페이스를 구현하는 외부 영역과 연결을 담당합니다.

 

 

3-1. 헥사고날 아키텍처가 가진 장점

 

도메인 로직과 외부 시스템 간 철저한 분리

  • 비즈니스 로직이 DB, API, 프레임워크 등 외부 기술에 의존하지 않음
  • 기술이 변경되도 도메인 로직은 그대로 유지 가능
  • 외부 구현체가 도메인에 의존

마케팅센터를 새롭게 만들며 가장 원했던 게 바로 이 부분입니다. 레이어가 명확히 나뉘고 인터페이스를 통해 의존성이 역전(Dependency Inversion Principle, DIP)**이 되어 외부 기술에 의존하지 않는 순순한 도메인 로직을 구현하도록 도와줍니다.

 

유지보수성과 테스트 용이성 향상

  • 외부 의존성이 없는 도메인 로직은 단위 테스트 가능
  • 빠르고 안정적인 테스트 수행

단위 테스트 시 mocking하거나 Stub을 만들어서 외부 의존성 없는 테스트가 가능합니다.

 

 

입출력 채널 확장에 유리

  • REST API, 메시지 큐, CLI, 배치 등 다양한 입출력 방식 지원
  • 어댑터만 추가하면 확장 가능

인터페이스에 맞게 외부 시스템을 사용해 구현하므로 도메인 로직에 영향을 주지도 않습니다. 필요하다면 어댑터 기반으로 확장 가능합니다.

 

 

프레임워크 종속 최소화

  • 도메인은 프레임워크와 독립적으로 유지
  • 프레임워크 교체에도 유연하게 대응 가능

 

 

설계 명확성 향상

  • 포트/어댑터 역할 분리로 책임 명확
  • 낮은 결합도로 협업과 유지보수에 유리

 

 

3-2. 헥사고날을 구현해보자!

 

 

 

저희팀이 정의한 헥사고날 아키텍처 구조

 

헥사고날 아키텍처를 검색해보면 사람마다 사용하는 용어도 제각각이고 디렉토리 구조도 달랐습니다. 처음 구조를 잡을 때 많은 고민과 토론을 바탕으로 다음과 같은 프로젝트 구조를 확정 지었습니다. 헥사고날 아키텍처 프로젝트 구조에 고민되신다면 이 자료가 도움될 거예요.

 

       {domain} # 바운디드 컨텍스트로 구분된 도메인 ex) order
	├── adaptor
	│    ├── inbound
	│    │    ├── procedure # 다른 바운디드 컨텍스트에서 접근하는 라우터
	│    │    │    ├── {sub_domain}_procedure.py
	│    │    │    ├── request_schema.py
	│    │    │    └── response_schema.py
	│    │    ├── router # API 호출
	│    │    │    └── {component} # 화면단위
	│    │    │           ├── api.py
	│    │    │           ├── request_schema.py
	│    │    │           └── response_schema.py
	│    │    └── task # 비동기 작업 (필요하다면 sub domain 별로 디렉토리를 나눈다)
	│    │          ├── task.py
	│    │          ├── request_schema.py
	│    │          └── response_schema.py
	│    └── outbound
	│          ├── proxy
	│          │    ├── {domain}_proxy.py # 다른 바운디드 컨텍스트로 접근하기 위한 프록시
        │          │    └── {외부기술 혹은 써드파티}_proxy.py
	│          ├── cache
	│          └── database
	│                 ├── migration
	│                 ├── read
        │                 │    └── {sub_domain}_read_repository.py
	│                 ├── write
        │                 │    └── {sub_domain}_write_repository.py
	│                 └── table # DB 모델
	├── application
	│    ├── dataclass # 레이어별 DTO를 모아두는 곳
	│    ├── enum
	│    ├── exception
	│    ├── port
	│    │    ├── inbound
	│    │    │    ├── {sub_domain}_query_port.py
	│    │    │    ├── {sub_domain}_command_port.py
	│    │    │    └── {sub_domain}_procedure_port.py
	│    │    └── outbound
	│    │          ├── {sub_domain}_read_persistence_port.py
	│    │          ├── {sub_domain}_write_persistence_port.py
	│    │          └── {domain}_proxy_port.py
	│    ├── domain
	│    │    ├── entity
	│    │    │    └── {sub_domain}_entity.py
	│    │    ├── service
	│    │    │    └── {sub_domain}_domain_service.py
	│    │    └── value_object
	│    │          └── {sub_domain}_value.py
	│    └── service # inbound port 구현체
	│          ├── {sub_domain}_command_service.py
	│          └── {sub_domain}_query_service.py
        ├── fixture # 테스트용 픽스처
        └── container # 의존성 주입 컨테이너
  
* 테스트 코드는 각 구현체 디렉토리의 동일 레벨에 구현

 

핵사고날 아키텍처를 도입하면서 고민했던 몇 가지 결정사항을 나열해보겠습니다.

  • enum을 각 레이어별로 정의해서 사용할 것인가?
    • 처음에는 enum을 레이어별로 지정해서 사용하다가 enum은 레이어 별로 달라지는 경우가 거의 없어 함께 사용하도록 변경했습니다. 다만 특정 레이어만 사용하는 enum은 별도 분리로 관리했습니다.
  • DTO를 각 레이어 별로 정의해서 사용할 것인가?
    • enum과 비슷한 고민일 수 있지만, DTO를 중복으로 사용하면 어느 레이어에서 가져온 데이터인지 정확히 구분하기 어렵다는 의견으로 DTO는 레이어 별로 구현해서 사용했습니다.
  • 도메인 서비스, 쿼리 서비스, 커맨드 서비스 역할
    • 쿼리 서비스는 조회를 위한 서비스입니다. 프레젠테이션 레이어에서 필요한 데이터를 조회하고 응답합니다.
    • 커맨드 서비스는 도메인 서비스에 필요한 데이터를 가져오거나 도메인 서비스로부터 도출한 결과를 저장하는 로직으로 구현합니다.
      • 데이터를 조회하고 저장하는 로직들과 도메인 로직들을 분리하고 순수 도메인 로직들로만 구성하여 가독성 향상
    • 도메인 서비스는 커맨드 서비스안에서 동작하며 도메인과 관련된 정책, 비즈니스 로직을 처리합니다.

 

class ContractDomainService:
    @classmethod
    def update_contract(cls, res_data: ContractWriteResData, update_data: UpdateContractData):
        # 도메인 로직 실행
        contract_entity = ContractEntity.create(res_data)
        contract_entity.update_ad(update_data)
        ...
        return ContractWriteReqData.from_entity(contract_entity)

class ContractCommandService(ContractCommandPort):
    def __init__(self, contract_write_persistence: ContractWritePersistencePort):
        self.contract_write_persistence = contract_write_persistence

    def update_contract(self, contract_id: int, update_data: UpdateContractData):
        # 계약 데이터 조회
        res_data: ContractWriteResData = (
            self.contract_write_persistence.get_contract(contract_id)
        )
        # 계약 업데이트 도메인 서비스 메서드 호출
        req_data: ContractWriteReqData = ContractDomainService.update_contract(
            res_Data, update_data
        )
        # 업데이된 정보 저장
        self.contract_write_persistence.manipulate(req_data)


커맨드서비스, 도메인서비스 구현 예시

 

 

2-2. 헥사고날 아키텍처 그래서 완벽한가?

 

완벽한 은탄환은 존재하지 않습니다!

헥사고날 아키텍처를 도입하고 개발하면서 힘든 부분도 많았습니다.

 

명확한 Best practice의 부재, 높은 러닝커브

 

파이썬으로 구현된 헥사고날 아키텍처 레퍼런스는 거의 없는 수준이었고, 레퍼런스마다 용어가 달라 도입 시 러닝커브가 높았습니다. 확실한 구조를 잡기 위해 팀원들과 DDD, 헥사고날 관련 서적들을 살펴보며 스터디를 진행했습니다. 스터디를 하더라도 정답이 딱 나오는 게 아니었기에 토론과 결정을 반복하며 저희만의 헥사고날 아키텍처를 정의해 나갔습니다.

 

DDD, 헥사고날 아키텍처 스터디하면서 봤던 책들

 

비대해진 코드량

레이어 별로 DTO를 구현하다보니 속성이 중복되더라도 매번 비슷한 DTO를 구현해야 했습니다. 레이어 별로 DTO를 전달할 때마다 각 레이어에 맞게 변환해주는 로직들도 추가하다보니 당연히 코드량도 늘어났습니다. DTO뿐만 아니라 Port별 인터페이스를 매번 구현하는 과정도 자연스럽게 코드량이 증가할 수 밖에 없었습니다. 코드량이 많아지는 건 아키텍처가 그만큼 복잡해졌기에 당연한 일이라고 여겼습니다. 대신 조금이라도 작업을 편하게 도와주는 모듈들을 구현하여 반복적인 코드를 쉽게 처리하도록 개선 중입니다.

 

 

마치며


이번에 신규 마케팅 센터를 만들면서 하고 싶었던 이야기들이 훨씬 많지만, 이 글에 모두 담기는 힘들었습니다. 조금 더 상세한 내용들은 다음 글을 통해 전해드리겠습니다.

화해 마케팅센터에 DDD와 헥사고날 아키텍처를 도입하면서 많은 고민과 어려움이 있었는데요. 처음 그렸던 청사진대로 프로젝트가 완성되었습니다. 저희가 구현한 아키텍처가 정답은 아니지만 처음 DDD, 헥사고날을 시작하시는 분들께 조금이나마 도움이 되었으면 좋겠습니다.

화해는 결과도 물론 중요하게 여기지만, Why로부터 시작을 하려고 노력합니다. Why가 잘 정의 되었다면 방향성과 결과는 올바른 방향으로 흘러간다고 믿습니다. 설령 잘못되었다 하더라도 적절한 시기에 방향성을 빠르게 다잡아 나갈 겁니다.

긴 글 읽어주셔서 감사드리며 화해 광고를 만들어가는 백엔드 공통 파트에 많은 응원과 관심 부탁드립니다!

 

 

 

  • 백엔드
  • DDD
  • 파이썬
  • 아키텍처
  • 백엔드팀
  • 헥사고날 아키텍처
  • fast api

연관 아티클