ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] 헥사고날 아키텍처
    언어/파이썬 & 장고 2025. 8. 31. 22:00

    서론: 프레임워크 주도 설계를 넘어서

    소프트웨어 엔트로피라는 현대적 과제

    소프트웨어 시스템은 시간이 지남에 따라 자연스럽게 복잡해지고, 변경이 어려워지며, 깨지기 쉬워지는 경향이 있습니다. 이러한 현상을 '소프트웨어 엔트로피'라고 하며, 이는 모든 장기 프로젝트가 직면하는 근본적인 도전 과제입니다. 특히 특정 프레임워크나 기술에 강하게 결합된 아키텍처는 이러한 엔트로피를 가속화하는 경향이 있습니다.1 프레임워크는 초기 개발 속도를 높여주지만, 그 구조와 제약에 비즈니스 로직이 종속되면서 시스템의 유연성과 수명은 점차 잠식당합니다. 기술 스택의 변화, 비즈니스 요구사항의 진화, 새로운 통합 지점의 등장은 초기에 효율적이었던 구조를 유지보수의 악몽으로 바꿀 수 있습니다.

    헥사고날 아키텍처 소개

    이러한 문제에 대한 전략적 해법으로 등장한 것이 바로 헥사고날 아키텍처(Hexagonal Architecture), 또는 포트와 어댑터(Ports and Adapters) 아키텍처입니다. 이 아키텍처는 단순히 규칙의 집합이 아니라, 소프트웨어 설계에 대한 패러다임의 전환을 제안합니다. 핵심 아이디어는 애플리케이션의 본질, 즉 순수한 비즈니스 로직을 시스템의 중앙에 배치하고, UI, 데이터베이스, 서드파티 API와 같은 외부 기술의 변동성으로부터 이를 철저히 보호하는 것입니다. 이를 통해 기술에 종속되지 않고, 변화에 탄력적으로 대응하며, 오랫동안 유지보수 가능한 시스템을 구축하는 것을 목표로 합니다.

    보고서 로드맵

    본 보고서는 파이썬 개발자의 관점에서 헥사고날 아키텍처를 심도 있게 탐구하는 포괄적인 가이드입니다. 먼저 아키텍처를 구성하는 핵심 개념들을 상세히 분해하여 이론적 토대를 다지겠습니다. 이후 전통적인 계층형 아키텍처 및 Django의 MVT 패턴과 비교 분석하여 그 차이점과 장단점을 명확히 밝히겠습니다. 마지막으로, 실제 파이썬 코드를 통해 이 아키텍처를 어떻게 구현할 수 있는지 구체적인 예시를 제시하며 이론을 실체화하는 과정을 안내할 것입니다.


    섹션 1: 헥사고날 아키텍처 해부

    핵심 원칙: 비즈니스 도메인 보호

    헥사고날 아키텍처의 가장 근본적인 목표는 애플리케이션의 핵심 로직, 즉 '내부(inside)'를 UI, 데이터베이스, 외부 API와 같은 외부 요인, 즉 '외부(outside)'로부터 격리하는 것입니다. 이는 비즈니스 로직이 자신을 외부에 드러내는 방식(예: REST API)이나 상태를 저장하는 기술(예: PostgreSQL)에 대해 전혀 알지 못해야 함을 의미합니다. 이 철저한 분리는 시스템의 유연성, 테스트 용이성, 그리고 장기적인 유지보수성을 확보하는 열쇠가 됩니다.

    아키텍처의 구성 요소: 상세 분석

    헥사고날 아키텍처는 크게 세 가지 주요 구성 요소로 나눌 수 있습니다: 핵심(Core), 경계(Boundaries), 그리고 외부 세계(External World).

    핵심 (The "Hexagon")

    애플리케이션의 심장부로, 순수한 비즈니스 로직과 정책을 담고 있습니다. 이 영역은 다시 도메인 계층과 애플리케이션 계층으로 나뉩니다.

    • 도메인 계층 (Domain Layer)
      • 엔티티(Entities)와 값 객체(Value Objects): 도메인 주도 설계(DDD)의 핵심 개념으로, 고유한 식별자를 가지는 객체(예: User)와 속성으로 정의되는 불변 객체(예: Money)를 구분하여 모델링합니다.5 도메인 계층은 어떠한 외부 프레임워크나 라이브러리에도 의존해서는 안 되며, 순수한 Python 객체로 구성되어야 합니다.
      • 비즈니스 규칙: 애플리케이션의 핵심 가치를 구현하는 로직입니다. 예를 들어, 대출 이자 계산 로직이나 사용자의 특정 행위에 대한 유효성 검사 규칙 등이 여기에 해당합니다.
    • 이곳은 애플리케이션의 존재 이유이자 가치를 창출하는 비즈니스 규칙과 데이터 구조가 위치하는 가장 깊은 중심부입니다.
    • 애플리케이션 계층 (Application Layer)
      • 유스케이스 (Use Cases / Application Services / Interactors): 외부 세계의 요청과 도메인 엔티티 간의 데이터 흐름을 조정하는 클래스들입니다. 이들은 "무엇을 할 것인가(what to do)"에 대한 책임을 지며, 실제 "어떻게 할 것인가(how to do it)"는 도메인 계층에 위임합니다. 예를 들어, '사용자 등록' 유스케이스는 사용자 데이터를 받아 도메인 엔티티를 생성하고, 데이터베이스에 저장을 요청하는 흐름을 제어합니다.
    • 이 계층은 특정 유스케이스(Use Case)를 수행하기 위해 도메인 계층의 로직을 조율(Orchestrate)하는 역할을 합니다.

    경계: 포트(Ports)를 이용한 계약 정의

    포트는 핵심 애플리케이션이 외부 세계와 상호작용하는 방식을 정의하는 공식적인 계약(Contract)입니다. 이 계약은 기술 중립적이어야 하며, Python에서는 일반적으로 abc 모듈의 추상 기본 클래스(Abstract Base Classes, ABCs)를 사용하여 인터페이스로 구현됩니다.

    • 인바운드 (Driving) 포트: 외부 행위자(예: 웹 컨트롤러)가 애플리케이션을 '구동(drive)'하는 방법을 정의합니다. 이는 애플리케이션 계층에 있는 유스케이스의 API 역할을 합니다. 예를 들어, RegisterUserUseCase 인터페이스 자체가 인바운드 포트가 될 수 있습니다.
    • 아웃바운드 (Driven) 포트: 애플리케이션이 외부 세계에 요구하는 사항을 정의합니다. 예를 들어, 데이터를 영속화하거나 메시지를 전송하는 기능이 필요함을 명시합니다. 애플리케이션은 이 포트를 통해 필요한 기능을 제공'받습니다(driven)'. 가장 흔한 예는 데이터 영속성을 위한 Repository 인터페이스입니다.

    이 지점에서 흔히 발생하는 오해를 바로잡는 것이 중요합니다. 포트는 외부 세계의 일부가 아니라, 애플리케이션 핵심의 일부입니다. 즉, 포트는 핵심 애플리케이션이 자신의 경계를 스스로 정의하는 방식입니다. 예를 들어, 애플리케이션 코어는 "나는 사용자를 저장할 방법이 필요하다"고 선언하며 UserRepositoryPort라는 인터페이스를 정의합니다. 이 인터페이스가 바로 아웃바운드 포트입니다. 어떻게 저장할지에 대한 구체적인 구현은 외부의 어댑터가 책임집니다.

    외부 세계: 어댑터(Adapters)를 통한 기술 구현

    어댑터는 포트라는 인터페이스의 구체적인 구현체입니다. 특정 기술을 애플리케이션의 핵심 로직에 연결하는 '플러그'와 같은 역할을 합니다.

    • 인바운드 (Driving) 어댑터: 외부 세계로부터 입력을 받아 애플리케이션 코어가 이해할 수 있는 형식으로 변환한 후, 인바운드 포트를 호출하는 컴포넌트입니다. REST API 컨트롤러(예: FastAPI, Flask), CLI 핸들러, 메시지 큐 컨슈머 등이 대표적인 예입니다.
    • 아웃바운드 (Driven) 어댑터: 아웃바운드 포트를 구현하여 애플리케이션이 필요로 하는 기능을 제공합니다. 예를 들어, UserRepositoryPort를 구현한 SQLAlchemyUserRepositoryAdapter, AWS SES를 통해 이메일을 보내는 EmailSenderAdapter, 서드파티 API에서 데이터를 가져오는 어댑터 등이 있습니다.

    황금률: 의존성 역전 원칙의 실제

    헥사고날 아키텍처 전체를 관통하는 가장 중요한 규칙은 바로 SOLID 원칙 중 하나인 '의존성 역전 원칙(Dependency Inversion Principle, DIP)'입니다.

    모든 의존성은 반드시 '안쪽으로(inward)' 향해야 합니다. 즉, 저수준의 세부사항을 다루는 어댑터 계층이 고수준의 정책을 다루는 애플리케이션 핵심 계층에 의존해야 합니다. 이는 전통적인 계층형 아키텍처에서 비즈니스 로직이 데이터베이스 같은 저수준 계층에 직접 의존하던 방향을 완전히 뒤집는 것입니다. 이 의존성의 역전이야말로 헥사고날 아키텍처가 기술 변화로부터 비즈니스 로직을 보호하는 핵심 메커니즘입니다. 육각형이라는 시각적 은유는 단순히 여러 상호작용 지점을 나타낼 뿐이며, 이 아키텍처의 진정한 혁신은 의존성 흐름을 제어하여 핵심 도메인을 보호하는 데 있습니다.


    섹션 2: 패러다임의 전환: 헥사고날 아키텍처 vs. 전통적 계층형 아키텍처

    의존성 흐름의 대비: 선형적 사슬 vs. 내향적 의존성

    • 전통적인 계층형 아키텍처 (Traditional Layered Architecture)
    • 전통적인 N-tier 아키텍처는 일반적으로 프레젠테이션 계층, 비즈니스 로직 계층, 데이터 접근 계층으로 구성됩니다. 이 구조의 핵심 규칙은 각 계층이 오직 자신보다 바로 아래 계층에만 의존할 수 있다는 점입니다. 이는 UI에서 데이터베이스에 이르기까지 단방향의 선형적인 의존성 사슬을 형성합니다. 예를 들어, 비즈니스 로직 계층은 데이터 접근 계층의 구체적인 구현에 직접 의존하게 됩니다.

    • 헥사고날 아키텍처의 역전
    • 반면, 헥사고날 아키텍처는 의존성 역전 원칙(DIP)을 통해 데이터 접근 계층에 대한 의존성을 역전시킵니다. 비즈니스 로직(애플리케이션 코어)은 데이터 접근 계층에 의존하지 않습니다. 대신, 데이터 접근 계층(아웃바운드 어댑터)이 비즈니스 로직 내에 정의된 추상화(아웃바운드 포트)에 의존합니다. 이로써 의존성의 방향이 외부에서 내부로 향하게 되어, 핵심 로직이 외부 기술의 변화로부터 자유로워집니다.

    주요 아키텍처 특성 비교 분석

    • 의존성 관리 및 결합도 (Coupling)
    • 계층형 아키텍처는 데이터베이스 스키마의 변경이 데이터 접근 계층, 비즈니스 로직 계층, 심지어 프레젠테이션 계층까지 연쇄적으로 영향을 미치는 강한 결합(tight coupling)과 전이 의존성(transitive dependencies)을 야기하기 쉽습니다. 반면, 헥사고날 아키텍처는 핵심 로직이 오직 자신이 정의한 추상적인 포트에만 의존하므로 느슨한 결합(loose coupling)을 유지합니다.
    • 테스트 용이성 (Testability)
    • 계층형 아키텍처에서 비즈니스 로직을 테스트하려면, 직접적으로 결합된 데이터 접근 계층 때문에 실제 데이터베이스를 사용하거나 복잡한 모킹(mocking)이 필요합니다. 이는 테스트를 느리고 번거롭게 만듭니다. 헥사고날 아키텍처에서는 핵심 로직을 완벽하게 격리하여 테스트할 수 있습니다. 아웃바운드 포트에 대해 간단한 인메모리(in-memory) 가짜 어댑터를 제공하기만 하면 되므로, 단위 테스트가 매우 빠르고 안정적입니다.
    • 유연성 및 적응성 (Flexibility & Adaptability)
    • 계층형 아키텍처에서 데이터베이스를 Oracle에서 MongoDB로 교체하는 것과 같은 주요 기술 변경은 프로젝트 전체에 큰 파급 효과를 낳는 대규모 작업입니다. 헥사고날 아키텍처에서는 새로운 기술에 맞는 어댑터만 새로 작성하여 '플러그인'처럼 교체하면 됩니다. 이 과정에서 핵심 비즈니스 로직은 단 한 줄도 수정할 필요가 없습니다.
    • 유스케이스 가시성 (Use Case Visibility)
    • 계층형 아키텍처는 종종 여러 비즈니스 로직이 응집도 낮게 섞여 있는 거대한 '서비스(Service)' 클래스를 만들어, 애플리케이션이 제공하는 실제 유스케이스를 파악하기 어렵게 만들 수 있습니다. 헥사고날 아키텍처는 각 유스케이스를 명시적인 클래스로 모델링하도록 권장하므로, 시스템의 기능과 책임이 훨씬 명확해집니다.

    헥사고날 아키텍처를 계층형 사고의 완전한 부정이 아닌, 그 진화된 형태로 이해하는 것이 중요합니다. 이는 의존성 역전 원칙을 시스템 전반에 걸쳐 체계적으로 적용한, 보다 정교한 계층형 아키텍처(때로는 '어니언 아키텍처'로 불림)로 볼 수 있습니다. 전통적인 아키텍처가 데이터베이스를 시스템의 기반으로 삼는다면, 헥사고날 아키텍처는 비즈니스 도메인을 시스템의 중심으로 옮겨놓은 것입니다. 이 관점의 전환이 바로 핵심적인 진화입니다.

    계층형 아키텍처 vs. 헥사고날 아키텍처 요약 비교

    특징 전통적인 계층형 아키텍처 헥사고날 아키텍처
    결합도 계층 간 강한 결합 (Tightly Coupled) 포트와 어댑터를 통한 느슨한 결합 (Loosely Coupled)
    의존성 흐름 선형적/하향식 (예: UI → 서비스 → DAL → DB) 내향식 (예: UI → 코어 ← DAL)
    테스트 용이성 낮음 (하위 계층에 대한 의존성으로 인해 복잡한 Mock/Stub 필요) 높음 (핵심 로직을 완벽히 격리하여 테스트 가능)
    유연성 낮음 (기술 스택 변경 시 연쇄적인 수정 발생) 높음 (어댑터 교체를 통해 기술 스택 변경에 용이)
    주요 설계 동인 데이터베이스 또는 인프라 구조 비즈니스 도메인 로직
    적합한 사용 사례 간단한 CRUD 애플리케이션, 빠른 프로토타이핑 복잡한 비즈니스 로직, 장기 유지보수, 다중 인터페이스

    섹션 3: 장고의 딜레마: MVT와 헥사고날 원칙의 조화

    Django의 MVT(Model-View-Template) 패턴 입문

    Django는 MVT(Model-View-Template)라는 고유한 아키텍처 패턴을 따릅니다. 이는 널리 알려진 MVC 패턴의 변형입니다.

    • Model: 데이터의 구조와 행위를 정의하며, Django ORM을 통해 데이터베이스와 강하게 결합되어 있습니다. 이는 객체가 스스로를 데이터베이스에 저장하는 방법을 아는 액티브 레코드(Active Record) 패턴을 따릅니다.
    • View: HTTP 요청을 받아 비즈니스 로직을 처리하고 응답을 반환합니다. MVC 패턴의 '컨트롤러'와 유사한 역할을 수행합니다.
    • Template: 사용자에게 보여지는 UI를 담당하는 프레젠테이션 계층입니다.
    • 프레임워크의 역할: Django의 URL 디스패처(URL dispatcher)와 미들웨어는 요청을 적절한 View로 라우팅하는 등 제어 흐름의 상당 부분을 담당합니다.

    마찰 지점: Django의 "Batteries-Included" 철학의 도전

    Django의 강력하고 편리한 기능들은 역설적으로 헥사고날 아키텍처의 원칙과 충돌하는 지점을 만들어냅니다.

    • 액티브 레코드 패턴: 가장 큰 마찰 지점입니다. Django의 모델(models.Model)은 순수한 데이터 구조가 아니라, user.save(), user.delete()처럼 스스로의 영속성을 관리하는 메서드를 포함합니다. 이는 영속성에 대해 전혀 알지 못해야 하는 헥사고날의 순수한 도메인 계층 원칙을 정면으로 위배합니다. 즉, Django에서는 도메인 모델과 영속성 모델이 분리되지 않고 하나로 합쳐져 있습니다.
    • View와 Serializer의 강한 결합: 비즈니스 로직이 종종 views.py 파일 내에 Django의 HttpRequest, QuerySet, 그리고 Django REST Framework의 Serializer와 같은 프레임워크 특정 컴포넌트와 뒤섞이게 됩니다. 이로 인해 해당 로직을 HTTP 요청의 맥락 밖에서 테스트하거나 재사용하기가 매우 어려워집니다.
    • 프레임워크 편의 기능의 상실: 도메인을 ORM으로부터 분리한다는 것은 Django가 제공하는 강력한 기능들, 예를 들어 객체의 변경 사항을 자동으로 감지하여 UPDATE 쿼리를 날려주는 더티 체킹(dirty checking)이나 연관된 객체를 필요할 때 조회하는 지연 로딩(lazy loading) 등을 포기해야 함을 의미합니다. 이는 도메인 엔티티와 영속성 모델 간의 데이터 매핑을 위한 상용구 코드(boilerplate code) 증가로 이어질 수 있습니다.

    이러한 충돌의 근원을 파고들면, 이는 두 가지 근본적으로 다른 데이터 관리 패턴 간의 대립임을 알 수 있습니다. Django ORM은 객체가 스스로의 영속성을 책임지는 액티브 레코드 패턴을 기반으로 합니다. 반면, 헥사고날 아키텍처는 인메모리 객체와 데이터베이스 간의 매핑을 별도의 객체(리포지토리)가 책임지는 데이터 매퍼(Data Mapper) 패턴을 자연스럽게 요구합니다. 이 근본적인 패턴의 불일치를 이해하는 것이 Django에서 헥사고날 아키텍처를 적용할 때 발생하는 어려움의 본질을 파악하는 열쇠입니다.

    간극 메우기: 구현 전략

    Django의 철학에 정면으로 맞서 싸우는 것은 비생산적일 수 있으므로, 현실적인 절충안을 찾는 것이 중요합니다.

    • 리포지토리 패턴 도입: Django ORM을 추상화하는 리포지토리 계층을 도입합니다. 애플리케이션 계층은 Django의 models.ManagerQuerySet에 직접 접근하는 대신, 우리가 정의한 리포지토리 인터페이스(아웃바운드 포트)와만 상호작용합니다. 실제 DB 작업은 이 인터페이스를 구현한 어댑터 클래스 내에서 수행됩니다.
    • 서비스 계층(유스케이스) 분리: 모든 비즈니스 로직을 views.py에서 분리하여, 애플리케이션 유스케이스를 대표하는 별도의 서비스 클래스로 옮깁니다. 이렇게 하면 Django의 View는 HTTP 요청/응답 처리 및 서비스 계층 호출이라는 얇은 '드라이빙 어댑터'의 역할만 수행하게 됩니다.
    • 분리된 엔티티 vs. 현실적 타협: 두 가지 접근법을 고려할 수 있습니다.
      1. 순수한 접근법: 도메인 계층을 위해 Pydantic 모델이나 데이터 클래스(dataclasses)를 별도로 정의하고, 리포지토리 어댑터 내에서 Django 모델과 상호 변환하는 매핑 로직을 구현합니다. 이는 완벽한 격리를 달성하지만, 매핑을 위한 상용구 코드가 증가하는 단점이 있습니다.
      2. 현실적 접근법: Django 모델을 도메인 엔티티로 사용하되, 모든 영속성 관련 로직(예: .save(), .objects.filter())을 리포지토리 어댑터 내부에 철저히 가두는 규칙을 적용합니다. 이는 순수성은 떨어지지만 매핑 오버헤드를 줄여주며, 헥사고날 아키텍처의 핵심 이점 중 상당 부분을 얻을 수 있는 "라이트(lite)" 버전의 접근법입니다.

    헥사고날 아키텍처를 Django 프로젝트에 적용하는 것은 '전부 아니면 전무'의 결정이 아닙니다. 팀은 점진적으로 이 패턴을 도입할 수 있습니다. 예를 들어, 복잡한 비즈니스 로직을 가진 기능에 대해서만 먼저 서비스 계층과 리포지토리 패턴을 도입하고, 간단한 CRUD 기능은 표준 MVT 패턴을 유지하는 방식입니다. 이러한 전략적이고 점진적인 접근은 대규모 리팩토링 없이도 아키텍처 개선의 이점을 누릴 수 있게 해주는 강력하고 현실적인 방법입니다.

    Django MVT vs. 헥사고날 아키텍처 비교 분석

    특징 표준 Django MVT Django 내 헥사고날 아키텍처
    초기 개발 속도 매우 빠름 (프레임워크가 제공하는 관례와 기능 활용) 느림 (추가적인 계층, 인터페이스, DI 설정 필요)
    장기 유지보수성 중간 (로직이 View/Model에 섞이면 복잡도 증가) 높음 (관심사 분리가 명확하여 변경 및 확장이 용이)
    테스트 용이성 중간 (ORM 모킹 또는 테스트 DB 필요, View 로직 테스트 어려움) 높음 (핵심 로직이 Django와 무관하게 격리되어 테스트 가능)
    결합도 높음 (비즈니스 로직이 ORM, View 등 프레임워크에 강하게 결합) 낮음 (핵심 로직이 포트를 통해 외부 기술과 분리)
    학습 곡선 낮음 (Django 개발자에게 익숙함) 높음 (DIP, 포트, 어댑터 등 새로운 개념 학습 필요)
    프레임워크 통합 최적 (프레임워크의 모든 기능을 최대한 활용) 제한적 (ORM 편의 기능 일부 포기, 추가적인 추상화 계층 필요)
    최적 적용 대상 빠른 프로토타이핑, CRUD 중심 애플리케이션, Django 생태계 활용 극대화 복잡한 비즈니스 도메인, 장기 프로젝트, 기술 스택 변경 가능성 높은 시스템

    섹션 4: 실제 적용: 사용 사례와 구현 전략

    헥사고날 아키텍처를 도입해야 할 때: 이상적인 프로젝트 시나리오

    • 복잡하고 변동성이 큰 비즈니스 도메인: 금융, 헬스케어처럼 비즈니스 규칙 자체가 핵심 자산이고 자주 변경되는 애플리케이션에 이상적입니다. 비즈니스 모델이 빠르게 진화하는 스타트업에도 매우 유용합니다.
    • 다양한 진입/출력 지점: 동일한 핵심 로직을 REST API, gRPC 엔드포인트, CLI, 그리고 메시지 큐 등 다양한 채널로 노출해야 할 때 강력한 힘을 발휘합니다. 새로운 '드라이빙 어댑터'를 추가하는 것만으로 간단히 확장할 수 있습니다.
    • 다수의 외부 통합: 애플리케이션이 여러 종류의 데이터베이스, 결제 게이트웨이, 알림 서비스 등과 연동해야 하고, 이러한 외부 기술이 미래에 변경될 가능성이 높을 때 적합합니다.
    • 장기적인 유지보수성이 최우선 과제일 때: 유지보수 비용과 미래의 변경에 따른 위험 관리가 중요한 대규모 장기 엔터프라이즈 시스템에 적합합니다.

    단순함을 유지해야 할 때: 오버 엔지니어링 식별 및 회피

    • 간단한 CRUD 애플리케이션: 데이터베이스를 얇게 감싼 수준의 애플리케이션에 헥사고날 아키텍처를 적용하는 것은 불필요한 복잡성과 상용구 코드만 늘릴 뿐, 거의 이점을 제공하지 못합니다.
    • 빠른 프로토타이핑 및 MVP(Minimum Viable Product): 아이디어를 신속하게 검증하는 것이 목표일 때, 헥사고날 아키텍처의 구조와 절차는 오히려 개발 속도를 저해하는 요소가 될 수 있습니다. 이 단계에서는 표준 프레임워크 패턴이 훨씬 효율적입니다.
    • 경험이 부족한 팀: 의존성 주입, 인터페이스, 아키텍처 계층과 같은 개념에 익숙하지 않은 팀에게 이 패턴을 강요하면 생산성이 저하되고, 잘못된 구현으로 이어질 수 있습니다.

    결국 헥사고날 아키텍처의 도입 여부는 단순히 기술적인 선택이 아니라, 비즈니스 및 프로젝트 관리 차원의 전략적 결정입니다. 이는 변경과 관련된 장기적인 비용과 위험을 줄이기 위해 초기 개발의 복잡성을 감수하는 선행 투자와 같습니다. 따라서 이 결정은 프로젝트의 생명주기, 요구사항의 변동성, 그리고 팀의 역량과 같은 비기술적 요소를 종합적으로 고려하여 내려져야 합니다.


    섹션 5: 완전한 코드 예제: FastAPI를 이용한 헥사고날 웹 서비스

    이 섹션에서는 Django의 복잡성을 배제하고 패턴의 본질을 명확하게 보여주기 위해, 순수 Python과 FastAPI, SQLAlchemy를 사용하여 작지만 완전한 헥사고날 아키텍처 기반의 웹 서비스를 구축하는 과정을 단계별로 안내합니다. FastAPI는 '드라이빙 어댑터'로, SQLAlchemy는 '드리븐 어댑터'로 사용될 것입니다.

    청사진: 권장 프로젝트 구조

    헥사고날 아키텍처를 적용한 파이썬 프로젝트는 일반적으로 다음과 같은 구조를 가집니다.

    project/
    ├── domain/
    │   ├── models.py       # 도메인 엔티티, 값 객체 (Pydantic, Dataclasses)
    │   └── ports.py        # 추상 인터페이스 (예: UserRepositoryPort)
    ├── application/
    │   └── use_cases.py    # 애플리케이션 서비스 (예: RegisterUserUseCase)
    ├── infrastructure/
    │   ├── adapters/
    │   │   └── persistence.py # SQLAlchemyUserRepositoryAdapter 구현
    │   └── web/
    │       └── api.py         # FastAPI 라우터 및 엔드포인트
    └── main.py             # 의존성 주입 및 애플리케이션 시작점

    핵심 구현: 도메인 및 애플리케이션 계층

    domain/models.py

    Pydantic을 사용하여 순수한 도메인 엔티티를 정의합니다. 이 클래스는 데이터와 함께 순수한 비즈니스 로직을 포함할 수 있지만, 영속성 관련 코드는 절대 포함하지 않습니다.

    # domain/models.py
    from pydantic import BaseModel, EmailStr, Field
    import uuid
    
    class User(BaseModel):
        id: uuid.UUID = Field(default_factory=uuid.uuid4)
        email: EmailStr
        username: str
    
        def change_username(self, new_username: str):
            if len(new_username) < 3:
                raise ValueError("Username must be at least 3 characters long.")
            self.username = new_username
    

    domain/ports.py

    abc.ABC를 사용하여 아웃바운드 포트(인터페이스)를 정의합니다. 애플리케이션 코어는 이 인터페이스에만 의존합니다.

    # domain/ports.py
    from abc import ABC, abstractmethod
    from typing import Optional
    import uuid
    from.models import User
    
    class UserRepositoryPort(ABC):
        @abstractmethod
        def get_by_id(self, user_id: uuid.UUID) -> Optional[User]:
            raise NotImplementedError
    
        @abstractmethod
        def save(self, user: User) -> User:
            raise NotImplementedError
    

    application/use_cases.py

    유스케이스 클래스를 정의합니다. 생성자를 통해 필요한 포트(의존성)를 주입받습니다. execute 메서드는 비즈니스 흐름을 조율합니다.

    # application/use_cases.py
    from domain.models import User
    from domain.ports import UserRepositoryPort
    from pydantic import EmailStr
    
    class RegisterUserUseCase:
        def __init__(self, user_repository: UserRepositoryPort):
            self._user_repository = user_repository
    
        def execute(self, email: EmailStr, username: str) -> User:
            new_user = User(email=email, username=username)
            return self._user_repository.save(new_user)
    

    주변부 구현: 인프라 어댑터

    infrastructure/adapters/persistence.py

    아웃바운드 포트를 상속받아 실제 기술(SQLAlchemy)을 사용하여 구현합니다. 도메인 모델과 영속성 모델 간의 변환을 책임집니다.

    # infrastructure/adapters/persistence.py
    from typing import Optional
    import uuid
    from sqlalchemy.orm import Session
    from domain.models import User
    from domain.ports import UserRepositoryPort
    from.orm_models import UserModel  # SQLAlchemy 모델
    
    class SQLAlchemyUserRepositoryAdapter(UserRepositoryPort):
        def __init__(self, db_session: Session):
            self._db_session = db_session
    
        def get_by_id(self, user_id: uuid.UUID) -> Optional[User]:
            user_orm = self._db_session.query(UserModel).filter(UserModel.id == user_id).first()
            if user_orm:
                return User.model_validate(user_orm)
            return None
    
        def save(self, user: User) -> User:
            user_orm = UserModel(**user.model_dump())
            self._db_session.add(user_orm)
            self._db_session.commit()
            self._db_session.refresh(user_orm)
            return User.model_validate(user_orm)
    

    infrastructure/web/api.py

    FastAPI를 사용하여 드라이빙 어댑터를 구현합니다. 이 엔드포인트는 HTTP 요청을 받아 데이터를 파싱한 후, 해당 유스케이스를 호출하여 비즈니스 로직을 실행시킵니다.

    # infrastructure/web/api.py
    from fastapi import APIRouter, Depends
    from pydantic import BaseModel, EmailStr
    from application.use_cases import RegisterUserUseCase
    from.dependencies import get_register_user_use_case # 의존성 주입 함수
    
    router = APIRouter()
    
    class UserCreateRequest(BaseModel):
        email: EmailStr
        username: str
    
    class UserResponse(BaseModel):
        id: uuid.UUID
        email: EmailStr
        username: str
    
    @router.post("/users", response_model=UserResponse, status_code=201)
    def create_user(
        request: UserCreateRequest,
        use_case: RegisterUserUseCase = Depends(get_register_user_use_case)
    ):
        new_user = use_case.execute(email=request.email, username=request.username)
        return new_user
    

    모든 것을 연결하기: 의존성 주입의 결정적 역할

    main.py

    이 파일은 '컴포지션 루트(Composition Root)' 역할을 합니다. 즉, 애플리케이션의 모든 구체적인 구현체들이 인스턴스화되고, 필요한 곳에 '주입(inject)'되는 곳입니다. 이 과정을 통해 핵심 애플리케이션은 자신이 어떤 구체적인 기술을 사용하고 있는지 전혀 알 필요가 없게 됩니다.

    # main.py
    from fastapi import FastAPI
    from infrastructure.web.api import router as user_router
    from infrastructure.adapters.database import SessionLocal # SQLAlchemy 세션
    
    app = FastAPI()
    
    # dependencies.py 에 있을 법한 내용
    def get_db():
        db = SessionLocal()
        try:
            yield db
        finally:
            db.close()
    
    def get_user_repository(db: Session = Depends(get_db)):
        return SQLAlchemyUserRepositoryAdapter(db)
    
    def get_register_user_use_case(repo: UserRepositoryPort = Depends(get_user_repository)):
        return RegisterUserUseCase(repo)
    
    # FastAPI의 의존성 주입 시스템을 활용하여 연결
    app.dependency_overrides[get_register_user_use_case] = get_register_user_use_case
    
    app.include_router(user_router)
    

    이처럼 의존성 역전 원칙(DIP)이라는 추상적인 개념은 의존성 주입(DI)이라는 구체적인 패턴을 통해 실현됩니다. 애플리케이션의 가장 바깥 계층인 main.py에서 모든 의존성을 조립하고 연결하는 이 마지막 단계가 없다면, 헥사고날 아키텍처는 그저 실행 불가능한 설계도에 불과합니다. 의존성 주입은 이 아키텍처에 생명을 불어넣는 실질적인 메커니즘입니다.


    결론: 아키텍처적 사고방식의 채택

    핵심 이점 요약

    본 보고서를 통해 살펴본 바와 같이, 헥사고날 아키텍처는 현대 소프트웨어 개발이 직면한 여러 도전을 해결하기 위한 강력한 해법을 제시합니다. 그 핵심 이점은 다음과 같이 요약할 수 있습니다.

    • 향상된 테스트 용이성: 비즈니스 로직을 외부 기술로부터 완벽하게 분리함으로써, 빠르고 안정적인 단위 테스트가 가능해져 코드 품질과 신뢰성을 높입니다.
    • 장기적인 유지보수성: 관심사의 명확한 분리는 코드의 이해를 돕고, 변경의 영향을 국소화하여 시스템이 성장함에 따라 발생하는 복잡성을 제어합니다.
    • 기술적 유연성: 데이터베이스, 프레임워크, 외부 서비스 등 기술 스택의 변화에 비즈니스 로직의 수정 없이 어댑터 교체만으로 대응할 수 있어, 미래의 변화에 대한 적응력을 극대화합니다.

    최종 고찰

    헥사고날 아키텍처는 맹목적으로 따라야 할 교리나 모든 문제에 대한 만병통치약이 아닙니다. 이는 아키텍트의 도구함에 있는 강력한 도구 중 하나이며, 그 가치를 제대로 발휘하기 위해서는 신중한 고려와 트레이드오프에 대한 명확한 이해가 필요합니다. 이 아키텍처의 도입은 소프트웨어 프로젝트의 장기적인 건전성과 적응성에 대한 전략적 투자입니다.

    따라서 개발자는 사용하는 프레임워크가 제시하는 기본 경로를 넘어서 생각해야 합니다. 당면한 문제의 본질, 프로젝트의 예상 수명, 비즈니스 도메인의 복잡성과 변동성을 깊이 이해하고, 이에 가장 적합한 아키텍처를 의식적으로 선택하는 '아키텍처적 사고방식'을 갖추는 것이 무엇보다 중요합니다. 헥사고날 아키텍처는 이러한 사고방식을 실천으로 옮길 수 있는 훌륭한 청사진을 제공합니다.

    댓글