Hexagonal Architecture


Hexagonal Architecture(헥사고날 아키텍처, Ports and Adapters 아키텍처) 심층 분석

1. 태그 정리

2. 분류 구조 분석 및 개선 제안

현재 분류:

분석 및 개선안
Hexagonal Architecture(헥사고날 아키텍처, Ports and Adapters) 는 Clean Architecture, Onion Architecture 등과 핵심 철학을 공유하지만, 본질적으로는 독립적인 아키텍처 스타일입니다. Clean Architecture 내부 하위 분류로 보다는 Structural 의 바로 아래에 위치시키는 것이 더 명확합니다.
제안:


3. 200 자 요약 설명

헥사고날 아키텍처는 비즈니스 로직과 외부 시스템 (데이터베이스, UI 등) 을 포트 (Ports) 와 어댑터 (Adapters) 구조로 분리해 느슨하게 결합된 시스템을 구현하는 아키텍처 패턴입니다. 이를 통해 핵심 로직은 외부 기술 변화에 영향을 받지 않고, 높은 유연성과 테스트 용이성이 확보됩니다 135.


4. 전체 개요 (250 자)

Hexagonal Architecture(헥사고날 아키텍처, Ports and Adapters 패턴) 는 1990 년대 Alistair Cockburn 이 제안한 소프트웨어 아키텍처 스타일로, 애플리케이션의 비즈니스 로직 (코어) 과 외부 요소 (Databases, UI, 외부 API 등) 를 확실하게 구분하고, 포트 (Port) 와 어댑터 (Adapter) 를 통해 통신함으로써 높은 모듈성, 확장성, 유지보수성을 달성합니다. 도메인 주도 설계 (DDD), 자동화 테스트, 기술 교체 등을 유연하게 적용할 수 있어 현대 개발에서 널리 활용되어 왔습니다 24.

5. 핵심 개념 및 실무 연관성

헥사고날 아키텍처 핵심 개념

실무 연관성

6. 주제별 심층 조사 분석

등장 및 발전 배경

목적 및 필요성

구분목적 및 필요성설명
목적도메인 로직 보호기술 변화/외부 환경과 무관한 핵심 로직 보존
필요성변화 대응성UI, DB, 메시징 등 외부 시스템 교체에 유연하게 대응

주요 기능 및 역할 (관계 구조)

관계: 포트와 어댑터는 항상 코어 (도메인) 에만 의존

특징

핵심 원칙

주요 원리 및 작동 원리 (다이어그램)

flowchart TD
    subgraph Core[코어(도메인/비즈니스 로직)]
      E[Entity/Domain] --> U[Use Case or Service]
    end
    subgraph Ports[포트]
      PI[Inbound Port (Input)] -.-> U
      PO[Outbound Port (Output)] -.-> U
    end
    subgraph Adapters[어댑터]
      AI[Adapter-In (예: REST API, UI)] --> PI
      AO[Adapter-Out (예: DB, MessageQueue)] --> PO
    end
    AI -.-> Core
    AO -.-> Core

설명:

필수/선택 구성요소 정리

구분구성요소역할비고
필수도메인 (엔티티)핵심 비즈니스 로직DB, UI, 외부 무관
필수포트 (Port)경계 인터페이스인바운드/아웃바운드 구분
필수어댑터 (Adapter)변환자실제 구현체 연결
선택서비스계층복잡한 도메인 로직 관리복잡도/규모마다 필요에 따라

구현 기법 및 방법

장점

구분항목설명
장점유지보수성포트/어댑터만 수정하여 기술 교체 가능, 핵심로직 불변 4
테스트 용이성코어만 독립테스트 가능, TDD·BDD 적용 수월 35
확장·유연성포트와 어댑터만 추가하면 새로운 인터페이스 수용 3
개발 병렬화팀별로 코어와 어댑터 개발 분담 가능 5

단점과 문제점 그리고 해결방안

단점

구분항목설명해결책
단점초기 복잡성다양한 인터페이스/어댑터 작성점진적 도입, 템플릿 활용
러닝커브설계 패턴 학습필수실전 예제, 문서화
작은 시스템 과적합단순한 시스템에 불필요 복잡성프로젝트 규모 고려 적용

문제점

구분항목원인영향탐지 및 진단예방 방법해결 방법 및 기법
문제점어댑터 관리 증가외부 시스템 다양화유지보수 비용 증가의존성 분석관심사 분리 철저어댑터 통합, 도구화
디버깅 복잡성여러 계층·추상화문제 추적 어려움상세 로깅계층별 로깅로그, 트레이싱 도구 사용

도전 과제 (최신 트렌드 반영)

분류 기준에 따른 종류 및 유형

분류 기준유형설명
적용 범위전체 시스템 적용모든 계층에 철저 적용
부분 적용핵심 로직에만 적용, 일부 레거시 호환
포트·어댑터 수단일/복수 포트단일 서비스/복합 시스템 지원
도메인 강조 여부DDD 통합형도메인 주도 설계 기반 구조
비즈니스 단순형단순 서비스에 적합

실무 사용 예시

적용 영역사용 구조/기술목적효과
백엔드 APIREST/GraphQL/Message유연한 확장, 다양한 클라이언트 호환유지보수 용이/테스트 자동화
데이터 변환DB 선택 (Oracle/MySQL 등)DB 교체/병렬 운용코어 수정 없이 데이터 저장소 교체
외부 시스템 연결Adapter 통한 3rd Party 연동외부 API/메시징 등 연결서비스중단 없이 확장/교체

활용 사례

시나리오

이커머스 주문 시스템에서 REST API, 메시지 큐, 데이터베이스를 독립적으로 교체 가능한 구조로 설계함.

시스템 구성
시스템 구성 다이어그램
graph TD
  subgraph Core
    O[주문 도메인 서비스]
    P[결제 도메인 서비스]
    D[배송 도메인 서비스]
  end
  REST[REST API Adapter]-->O
  MQ[Message Queue Adapter]-->O
  CLI[CLI Adapter]-->O
  O-->|Outbound Port|DB[DB Adapter]
  O-->|Outbound Port|G[외부 결제 Adapter]
  D-->|Outbound Port|SH[Shipping API Adapter]
Workflow
역할
유무에 따른 차이점
구현 예시 (Python)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 포트 정의
class OrderPort:
    def place_order(self, order): pass

# 도메인 서비스/유스케이스
class OrderService(OrderPort):
    def place_order(self, order):
        # 주문 핵심 로직 처리
        pass

# 인바운드 어댑터 예시 (REST API)
class OrderRestAdapter:
    def __init__(self, port: OrderPort):
        self.port = port

    def handle_request(self, request):
        order = parse_order(request)
        return self.port.place_order(order)

# 아웃바운드 어댑터 예시 (DB 연동)
class OrderDBAdapter:
    def save(self, order):
        # DB 저장 로직
        pass

실무에서 효과적으로 적용하기 위한 고려사항 및 권장사항

항목설명권장사항
계층 (포트/어댑터) 명확화추상화 인터페이스와 실제 구현 구분명확한 네이밍, 코드 분리
테스트 전략코어 단위 테스트 중심모킹 (Mock), 스터빙 활용
문서화 및 코드리뷰설계/개발 일관성 유지Mermaid 다이어그램 활용
유지보수성어댑터/포트 확장성, 코드 일관성모듈별 책임 명확화

최적화하기 위한 고려사항 및 권장사항

항목설명권장사항
어댑터 성능 최적화I/O 나 외부 시스템 호출 최적화캐싱, 비동기 처리 도입
DI 활용객체 주입을 통한 결합도 감소프레임워크 DI 컨테이너 활용
로깅/트레이싱계층별 모니터링표준화된 로그 전략, 오픈소스 도구 활용

주제와 관련하여 주목할 내용 정리

카테고리주제항목설명
이론포트와 어댑터 (Ports/Adapters)경계 추상화도메인과 외부 결합 최소화, 규격 표준화
실무테스트 전략독립 테스트어댑터 모킹 (Mock) 중심 유닛테스트
성능Adapter 최적화비동기 처리I/O 병목 최소화, 스케일링 대응
확장DDD 조합도메인 중심복잡 비즈니스 로직 분할·보호

반드시 학습해야할 내용

카테고리주제항목설명
패턴디커플링 (Decoupling)의존성 분리도메인/외부 시스템의 완전한 분리 원칙 실천
코딩DI/IoC의존성 주입/제어의 역전프레임워크별 객체 생성, 주입 메커니즘 이해
아키텍처계층화 설계포트/어댑터, 도메인 계층각 계층 역할 명확화 및 구조화 연습
운영자동화 테스트단위/통합 테스트유지보수성 증진, CI/CD 연동

용어 정리

카테고리용어 (한/영)설명
아키텍처포트 (Port)내부 (도메인) 와 외부 시스템 간 통신을 위한 인터페이스
아키텍처어댑터 (Adapter)포트와 외부 시스템을 연결하는 실제 구현체
아키텍처의존성 역전 (Dependency Inversion)외부 코드가 내부 (코어) 에 의존하게 설계
테스트모킹 (Mock)/스터빙 (Stub)가짜 구현체를 통한 코어 테스트

참고 및 출처



Hexagonal Architecture(헥사고날 아키텍처, Ports and Adapters 아키텍처) 계속 심화 정리

헥사고날 아키텍처와 유사 아키텍처 비교

구분헥사고날 아키텍처Layered 아키텍처클린 아키텍처 (Clean Architecture)
구조 개념코어, 포트, 어댑터로 모든 외부와 내부 분리(프레젠테이션 → 도메인 → 데이터) 등 계층 다중엔터티, 유스케이스, 인터페이스 어댑터 등 중심 구조
외부 시스템 연결 방식어댑터가 포트를 통해 독립적으로 연결계층 간 직접 호출/의존꼭 내부로만 의존성이 향하도록 설계
테스트 용이성코어만 독립 테스트 매우 용이DB, UI 등 외부의존성 코드 분리 어려움계층별로 단위 테스트 체계화
기술/환경 교체 유연성매우 높음낮음매우 높음

실제 적용/확장 패턴 및 현대 트렌드

확장 적용 TIP 과 세부 실무 전략

  1. 코어 로직은 Domain Layer(도메인 계층) 만, 외부 인터페이스는 반드시 어댑터 생성

    • 신규 API, 외부 공급자 (결제, 알림 등) 도입에도 핵심 비즈니스 코드에 영향 없음
  2. Port(포트) 를 인터페이스로, Adapter(어댑터) 를 구현 클래스로 설계

    • Python 예시:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      # 포트 정의 (인터페이스)
      class PaymentPort:
          def execute_payment(self, data):
              pass
      
      # 어댑터 구현 (외부 결제 모듈 연동)
      class ExternalPaymentAdapter(PaymentPort):
          def execute_payment(self, data):
              # 외부 결제 API 호출 및 결과 반환
              pass
      
  3. 인바운드/아웃바운드 어댑터 의존성 분리

    • 인바운드: REST, CLI, 메시지 소비자 등
    • 아웃바운드: DB, 3rd party API, 큐 생산자 등
  4. CI(Continuous Integration, 지속적 통합) 파이프라인에서 포트 - 어댑터 일관성 검증

    • 계층별 Mock 테스트 (CI 파이프라인에 통합)
    • 변경 감지 시 어댑터 단위로 영향 분석

추가적으로 주목할 최신 트렌드 및 도전 과제

헥사고날 아키텍처를 사용해야 하는 대표적 상황

활용 시 반드시 학습해야 할 내용 (추가)

카테고리주제항목설명
코딩Port-Driven TestMock 기반 테스트외부 어댑터 없이 코어만 유닛테스트 실행
계층화Outbound Integration외부 연동 어댑터다양한 외부 시스템 표준화된 방식과의 정상/오류 처리
관측성Structured Logging계층별 로그/트레이스장애 원인 신속 탐색, 운영 자동화에 필수

용어 정리 (보완)

카테고리용어 (한/영)설명
아키텍처인바운드 포트 (Inbound Port)외부 입력 (사용자, 타 시스템) 신호가 코어로 전달되는 진입 인터페이스
아키텍처아웃바운드 포트 (Outbound Port)코어가 외부 시스템 (DB, API 등) 과 통신할 때 사용되는 출력 인터페이스
아키텍처인바운드 어댑터 (Inbound Adapter)인바운드 포트에 맞춰 외부 입력을 변환하여 전달하는 구현체
아키텍처아웃바운드 어댑터 (Outbound Adapter)아웃바운드 포트를 통해 외부 시스템 (데이터베이스, 외부 API 등) 에 연결하는 클래스
코딩의존성 주입 (Dependency Injection, DI)외부 객체를 코드 내부가 아닌 어플리케이션 외부 (프레임워크 등) 에서 주입하는 방식

참고 및 출처



Hexagonal Architecture(헥사고날 아키텍처, Ports and Adapters 아키텍처)—마무리 및 종합 정리

확장 적용 시 실제 사례 중심 종합

1. 마이크로서비스 (MSA) 적용에서의 실제 예시

2. 이벤트 기반 비동기 구조와 연계

3. 레거시 시스템 모던화

4. 클라우드 및 배포 환경 최적화

헥사고날 아키텍처와 기타 구조 패턴 적용 전략 요약

적용 방식특징상황별 추천 포인트
헥사고날 아키텍처포트/어댑터로 독립성 유지, 외부기술 교체 용이기술 변화 잦음, 자동화 테스트 중시, API 확장 잦은 시스템
클린 아키텍처 (Clean Architecture)계층·도메인 중심 설계, 의존성은 내부 방향으로만대규모 복합 도메인, 유지보수성 중시
Layered 구조전통적 3 계층 (프레젠테이션/비즈니스/데이터), 직접 접근 구조단순한 시스템, 소규모 레거시 개선 등에 적합

현업 개발 관점 주의 및 리더십 전략

현장 적용성 한줄 요약

헥사고날 아키텍처 (Ports and Adapters) 는 핵심 도메인 독립, 변화에 강한 플러그형 소프트웨어 설계 구조로서, 지속적 변화와 확장을 요구하는 시스템에 매우 실용적입니다.

용어 정리 (종합)

카테고리용어 (한글/영어)설명
아키텍처헥사고날 아키텍처 (Hexagonal Architecture)포트/어댑터로 도메인과 외부 시스템 완전 분리
아키텍처포트 (Port)코어 로직과 외부시스템 연결하는 추상 인터페이스
아키텍처어댑터 (Adapter)포트에 맞춰 외부 입출력을 구체 구현하는 클래스/모듈
아키텍처인바운드/아웃바운드 어댑터 (Inbound/Outbound Adapter)입력 (사용자/API)/출력 (DB/외부 API) 별 실제 어댑터 구현체
테스트목 (Mock)/스터빙 (Stub)외부 시스템 없이 단위 테스트를 위해 사용되는 가짜 객체
아키텍처의존성 주입 (Dependency Injection, DI)외부 객체를 내부가 아닌 외부 환경에서 주입해주는 설계 기법

참고 및 출처


Hexagonal Architecture (헥사고날 아키텍처) 심화 분석

1. 태그 정리

2. 분류 구조 검증

현재 분류: “Software Engineering > Design and Architecture > Architecture Styles and Patterns > Architecture Styles > Structural > Clean Architecture”

제안하는 더 적절한 분류:

1
Software Engineering > Design and Architecture > Architecture Styles and Patterns > Domain-Centric Architectures > Hexagonal Architecture

근거: Hexagonal Architecture 는 Clean Architecture 의 하위 개념이 아니라, Domain-Driven Design 과 밀접한 관련이 있는 독립적인 아키텍처 패턴으로, 도메인 중심 아키텍처로 분류하는 것이 더 적절합니다.

3. 주제 요약 설명 (200 자 내외)

Hexagonal Architecture (헥사고날 아키텍처) 는 Alistair Cockburn 이 2005 년에 제안한 “Ports and Adapters” 패턴으로, 비즈니스 로직을 외부 의존성으로부터 격리하여 테스트 가능하고 유지보수가 용이한 소프트웨어를 설계하는 아키텍처 패턴입니다. 포트와 어댑터를 통해 핵심 도메인과 외부 시스템 간의 결합도를 낮춥니다.

4. 전체 개요 (250 자 내외)

Hexagonal Architecture 는 소프트웨어의 핵심 비즈니스 로직을 중앙에 배치하고, 외부 시스템과의 상호작용을 포트 (Port) 와 어댑터 (Adapter) 를 통해 관리하는 아키텍처 패턴입니다. 이를 통해 데이터베이스, UI, 외부 API 등의 기술적 세부사항 변경 시에도 비즈니스 로직의 수정 없이 대응할 수 있어 시스템의 유연성과 테스트 용이성을 크게 향상시킵니다.

5. 핵심 개념

5.1 핵심 개념

5.2 실무 구현을 위한 연관성

6. 세부 조사 내용

등장 및 발전 배경

Hexagonal Architecture 는 2005 년 Alistair Cockburn 에 의해 제안되었습니다. 전통적인 계층형 아키텍처의 한계를 극복하기 위해 개발되었으며, 특히 다음과 같은 문제들을 해결하고자 했습니다:

2005 년 처음 “Hexagonal Architecture” 로 명명되었다가 “Ports and Adapters” 로 개명되었으며, 2024 년 Cockburn 은 Juan Manuel Garrido de Paz 와 함께 이에 대한 종합적인 책을 출간했습니다.

목적 및 필요성

달성하고자 하는 목적:

필요성:

주요 기능 및 역할

기능:

역할:

특징

핵심 원칙

  1. 의존성 방향 제어: 모든 의존성은 중심부를 향해야 함
  2. 인터페이스 우선: 구현체가 아닌 인터페이스에 의존
  3. 단일 책임: 각 어댑터는 하나의 외부 시스템만 담당
  4. 관심사 분리: 비즈니스 로직과 기술적 관심사 완전 분리
  5. 설정 외부화: 어댑터 연결은 설정을 통해 런타임에 결정

주요 원리 및 작동 원리

graph TB
    subgraph "External Systems"
        UI[User Interface]
        CLI[Command Line]
        API[REST API]
        DB[(Database)]
        EXT[External Services]
        FILE[File System]
    end
    
    subgraph "Adapters Layer"
        PA1[Primary Adapter<br/>Web Controller]
        PA2[Primary Adapter<br/>CLI Handler]
        SA1[Secondary Adapter<br/>DB Repository]
        SA2[Secondary Adapter<br/>File Storage]
        SA3[Secondary Adapter<br/>HTTP Client]
    end
    
    subgraph "Ports Layer"
        PP1[Primary Port<br/>Use Case Interface]
        SP1[Secondary Port<br/>Repository Interface]
        SP2[Secondary Port<br/>Storage Interface]
        SP3[Secondary Port<br/>External API Interface]
    end
    
    subgraph "Domain Core"
        UC[Use Cases<br/>Application Services]
        ENT[Entities]
        VO[Value Objects]
        BR[Business Rules]
    end
    
    UI --> PA1
    CLI --> PA2
    API --> PA1
    
    PA1 --> PP1
    PA2 --> PP1
    PP1 --> UC
    
    UC --> SP1
    UC --> SP2
    UC --> SP3
    
    SP1 --> SA1
    SP2 --> SA2
    SP3 --> SA3
    
    SA1 --> DB
    SA2 --> FILE
    SA3 --> EXT
    
    UC --> ENT
    UC --> VO
    UC --> BR

작동 원리:

  1. 요청 입력: Primary Adapter 가 외부 요청을 받아 Primary Port 를 통해 전달
  2. 비즈니스 처리: Use Case 가 비즈니스 로직을 실행하며 필요시 Secondary Port 호출
  3. 외부 연동: Secondary Adapter 가 실제 외부 시스템과 통신하여 결과 반환
  4. 응답 반환: 처리 결과가 역순으로 Primary Adapter 를 통해 외부로 전달

구조 및 아키텍처

Hexagonal Architecture 는 다음과 같은 핵심 구성요소로 이루어집니다:

필수 구성요소

1. Domain Core (도메인 코어)

2. Primary Ports (주 포트)

3. Secondary Ports (부 포트)

4. Primary Adapters (주 어댑터)

5. Secondary Adapters (부 어댑터)

선택 구성요소

1. Application Services (애플리케이션 서비스)

2. Domain Events (도메인 이벤트)

구현 기법 및 방법

1. 의존성 주입 (Dependency Injection)

정의: 런타임에 의존성을 외부에서 주입하는 기법

구성:

목적:

실제 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Python with dependency injection
class UserService:
    def __init__(self, user_repository: UserRepository, email_service: EmailService):
        self._user_repository = user_repository
        self._email_service = email_service
    
    def create_user(self, user_data: dict) -> User:
        # Business logic implementation
        user = User.create(user_data)
        saved_user = self._user_repository.save(user)
        self._email_service.send_welcome_email(saved_user.email)
        return saved_user

# Configuration
def configure_dependencies():
    user_repository = PostgreSQLUserRepository()
    email_service = SMTPEmailService()
    return UserService(user_repository, email_service)
2. 포트 인터페이스 설계 (Port Interface Design)

정의: 도메인과 외부 세계 간의 계약을 정의하는 기법

구성:

목적:

실제 예시:

 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
// TypeScript interface definition
interface UserRepository {
    findById(id: string): Promise<User | null>;
    save(user: User): Promise<User>;
    findByEmail(email: string): Promise<User | null>;
}

interface EmailService {
    sendWelcomeEmail(email: string): Promise<void>;
    sendPasswordResetEmail(email: string, token: string): Promise<void>;
}

// Use Case implementation
class CreateUserUseCase {
    constructor(
        private userRepository: UserRepository,
        private emailService: EmailService
    ) {}
    
    async execute(userData: CreateUserRequest): Promise<User> {
        const existingUser = await this.userRepository.findByEmail(userData.email);
        if (existingUser) {
            throw new UserAlreadyExistsError();
        }
        
        const user = new User(userData);
        const savedUser = await this.userRepository.save(user);
        await this.emailService.sendWelcomeEmail(user.email);
        
        return savedUser;
    }
}
3. 어댑터 패턴 구현 (Adapter Pattern Implementation)

정의: 서로 다른 인터페이스를 연결하는 구조적 디자인 패턴

구성:

목적:

실제 예시:

 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
# Repository adapter for PostgreSQL
class PostgreSQLUserRepository(UserRepository):
    def __init__(self, db_connection):
        self._db = db_connection
    
    def find_by_id(self, user_id: str) -> Optional[User]:
        query = "SELECT * FROM users WHERE id = %s"
        result = self._db.execute(query, (user_id,))
        if result:
            return User.from_dict(result[0])
        return None
    
    def save(self, user: User) -> User:
        query = """
            INSERT INTO users (id, email, name, created_at) 
            VALUES (%s, %s, %s, %s) 
            ON CONFLICT (id) DO UPDATE SET 
                email = EXCLUDED.email,
                name = EXCLUDED.name
        """
        self._db.execute(query, (user.id, user.email, user.name, user.created_at))
        return user

# Email service adapter for SMTP
class SMTPEmailService(EmailService):
    def __init__(self, smtp_config):
        self._config = smtp_config
    
    async def send_welcome_email(self, email: str) -> None:
        message = self._create_welcome_message(email)
        await self._send_email(email, message)
    
    async def _send_email(self, to: str, message: str) -> None:
        # SMTP implementation details
        pass

장점

구분항목설명
장점높은 테스트 가능성의존성 격리로 인해 단위 테스트와 통합 테스트가 용이하며, Mock 객체를 통한 독립적 테스트 가능
장점기술 독립성비즈니스 로직이 특정 프레임워크나 데이터베이스에 종속되지 않아 기술 교체 시 영향 최소화
장점유연한 확장성새로운 어댑터 추가만으로 다양한 외부 시스템과 통합 가능
장점명확한 관심사 분리도메인 로직과 기술적 세부사항이 명확히 분리되어 코드 이해와 유지보수 용이
장점병렬 개발 지원인터페이스 정의 후 팀별로 독립적인 개발 진행 가능
장점재사용성 향상핵심 비즈니스 로직을 다양한 컨텍스트에서 재사용 가능

단점과 문제점 그리고 해결방안

단점
구분항목설명해결책
단점초기 복잡성 증가포트와 어댑터 설계로 인한 초기 개발 복잡도 상승점진적 적용과 팀 교육을 통한 학습 곡선 완화
단점코드량 증가인터페이스와 구현체 분리로 인한 보일러플레이트 코드 증가코드 생성 도구와 템플릿 활용
단점성능 오버헤드추상화 계층으로 인한 미미한 성능 저하크리티컬한 부분에 대한 성능 프로파일링과 최적화
단점과도한 추상화 위험불필요한 추상화로 인한 시스템 복잡성 증가YAGNI 원칙 적용과 적절한 추상화 수준 유지
문제점
구분항목원인영향탐지 및 진단예방 방법해결 방법 및 기법
문제점인터페이스 불일치포트와 어댑터 간 계약 불일치런타임 오류 발생계약 테스트, 타입 검사강타입 언어 사용, API 문서화계약 테스트 도입, Consumer-Driven Contract
문제점순환 의존성잘못된 의존성 방향 설정시스템 결합도 증가의존성 분석 도구의존성 방향 규칙 정의아키텍처 테스트, 리팩토링
문제점어댑터 누수도메인에 기술적 세부사항 침투아키텍처 일관성 훼손정적 분석, 코드 리뷰아키텍처 가이드라인 수립리팩토링, 교육 강화

도전 과제

1. 아키텍처 경계 유지

원인: 개발자의 편의나 일정 압박으로 인한 경계 침범
영향: 아키텍처 일관성 훼손과 장기적 유지보수 비용 증가
해결 방법:

2. 마이크로서비스 전환

원인: 모놀리스에서 마이크로서비스로의 분해 과정에서 발생하는 복잡성
영향: 서비스 간 통신 복잡도 증가와 데이터 일관성 문제
해결 방법:

3. 성능 최적화

원인: 추상화 계층으로 인한 성능 오버헤드
영향: 응답시간 증가와 처리량 감소
해결 방법:

분류 기준에 따른 종류 및 유형

분류 기준종류/유형설명
적용 범위단일 바운디드 컨텍스트하나의 도메인 영역에만 적용
적용 범위멀티 바운디드 컨텍스트여러 도메인 영역에 걸쳐 적용
시스템 규모모놀리식 헥사고날단일 배포 단위로 구성
시스템 규모마이크로서비스 헥사고날각 서비스별로 헥사고날 구조 적용
기술 스택순수 헥사고날프레임워크 의존성 최소화
기술 스택프레임워크 기반Spring, Quarkus 등 프레임워크 활용
복잡도단순 헥사고날기본적인 포트와 어댑터만 사용
복잡도고급 헥사고날이벤트 소싱, CQRS 등 고급 패턴 결합

실무 사용 예시

사용 목적함께 사용되는 기술효과
웹 애플리케이션 개발Spring Boot, JPA, REST API비즈니스 로직과 웹 계층 분리로 테스트 용이성 증대
마이크로서비스 구축Docker, Kubernetes, Event Bus서비스 간 결합도 감소와 독립적 배포 가능
레거시 시스템 현대화API Gateway, Message Queue점진적 마이그레이션과 리스크 최소화
도메인 주도 설계DDD, Event Sourcing, CQRS복잡한 비즈니스 로직의 체계적 관리
클라우드 네이티브 앱AWS Lambda, Cloud Functions서버리스 환경에서의 비즈니스 로직 재사용

활용 사례

시나리오: 전자상거래 주문 관리 시스템

시스템 구성:

시스템 구성 다이어그램:

graph TB
    subgraph "External Systems"
        WEB[Web UI]
        MOBILE[Mobile App]
        ADMIN[Admin Panel]
        PG[Payment Gateway]
        EMAIL[Email Service]
        SMS[SMS Service]
        INVENTORY[(Inventory DB)]
        ORDER_DB[(Order DB)]
    end
    
    subgraph "Primary Adapters"
        REST[REST Controller]
        GRAPHQL[GraphQL Resolver]
        CLI[CLI Handler]
    end
    
    subgraph "Primary Ports"
        CREATE_ORDER[Create Order Port]
        CANCEL_ORDER[Cancel Order Port]
        VIEW_ORDER[View Order Port]
    end
    
    subgraph "Order Domain Core"
        ORDER_SERVICE[Order Service]
        ORDER_ENTITY[Order Entity]
        ORDER_RULES[Business Rules]
    end
    
    subgraph "Secondary Ports"
        PAYMENT_PORT[Payment Port]
        INVENTORY_PORT[Inventory Port]
        NOTIFICATION_PORT[Notification Port]
        PERSISTENCE_PORT[Order Repository Port]
    end
    
    subgraph "Secondary Adapters"
        PAYMENT_ADAPTER[Payment Adapter]
        INVENTORY_ADAPTER[Inventory Adapter]
        EMAIL_ADAPTER[Email Adapter]
        SMS_ADAPTER[SMS Adapter]
        DB_ADAPTER[Database Adapter]
    end
    
    WEB --> REST
    MOBILE --> REST
    ADMIN --> GRAPHQL
    
    REST --> CREATE_ORDER
    REST --> CANCEL_ORDER
    GRAPHQL --> VIEW_ORDER
    CLI --> CREATE_ORDER
    
    CREATE_ORDER --> ORDER_SERVICE
    CANCEL_ORDER --> ORDER_SERVICE
    VIEW_ORDER --> ORDER_SERVICE
    
    ORDER_SERVICE --> ORDER_ENTITY
    ORDER_SERVICE --> ORDER_RULES
    
    ORDER_SERVICE --> PAYMENT_PORT
    ORDER_SERVICE --> INVENTORY_PORT
    ORDER_SERVICE --> NOTIFICATION_PORT
    ORDER_SERVICE --> PERSISTENCE_PORT
    
    PAYMENT_PORT --> PAYMENT_ADAPTER
    INVENTORY_PORT --> INVENTORY_ADAPTER
    NOTIFICATION_PORT --> EMAIL_ADAPTER
    NOTIFICATION_PORT --> SMS_ADAPTER
    PERSISTENCE_PORT --> DB_ADAPTER
    
    PAYMENT_ADAPTER --> PG
    INVENTORY_ADAPTER --> INVENTORY
    EMAIL_ADAPTER --> EMAIL
    SMS_ADAPTER --> SMS
    DB_ADAPTER --> ORDER_DB

Workflow:

  1. 고객이 웹/모바일 앱에서 주문 생성 요청
  2. REST Controller 가 요청을 받아 Create Order Port 호출
  3. Order Service 가 비즈니스 규칙 검증 및 주문 엔티티 생성
  4. 재고 확인을 위해 Inventory Port 를 통해 재고 시스템 조회
  5. 결제 처리를 위해 Payment Port 를 통해 결제 게이트웨이 호출
  6. 주문 정보를 Order Repository Port 를 통해 데이터베이스에 저장
  7. 주문 확인 알림을 Notification Port 를 통해 이메일/SMS 발송

역할:

유무에 따른 차이점:

구현 예시:

  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
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# Primary Port (Use Case Interface)
from abc import ABC, abstractmethod
from typing import Optional

class CreateOrderUseCase(ABC):
    @abstractmethod
    async def execute(self, order_data: CreateOrderRequest) -> OrderResponse:
        pass

# Domain Entity
class Order:
    def __init__(self, customer_id: str, items: List[OrderItem]):
        self.id = str(uuid.uuid4())
        self.customer_id = customer_id
        self.items = items
        self.status = OrderStatus.PENDING
        self.created_at = datetime.utcnow()
        self._validate()
    
    def _validate(self):
        if not self.items:
            raise EmptyOrderError("Order must contain at least one item")
        if self.total_amount <= 0:
            raise InvalidOrderAmountError("Order amount must be positive")
    
    @property
    def total_amount(self) -> Decimal:
        return sum(item.price * item.quantity for item in self.items)
    
    def confirm_payment(self, payment_id: str):
        if self.status != OrderStatus.PENDING:
            raise InvalidOrderStateError("Can only confirm payment for pending orders")
        self.status = OrderStatus.CONFIRMED
        self.payment_id = payment_id

# Secondary Ports (Repository Interfaces)
class OrderRepository(ABC):
    @abstractmethod
    async def save(self, order: Order) -> Order:
        pass
    
    @abstractmethod
    async def find_by_id(self, order_id: str) -> Optional[Order]:
        pass

class PaymentService(ABC):
    @abstractmethod
    async def process_payment(self, amount: Decimal, payment_method: str) -> PaymentResult:
        pass

class InventoryService(ABC):
    @abstractmethod
    async def check_availability(self, items: List[OrderItem]) -> bool:
        pass
    
    @abstractmethod
    async def reserve_items(self, items: List[OrderItem]) -> ReservationResult:
        pass

# Application Service (Domain Core)
class OrderService(CreateOrderUseCase):
    def __init__(
        self,
        order_repository: OrderRepository,
        payment_service: PaymentService,
        inventory_service: InventoryService,
        notification_service: NotificationService
    ):
        self._order_repository = order_repository
        self._payment_service = payment_service
        self._inventory_service = inventory_service
        self._notification_service = notification_service
    
    async def execute(self, order_data: CreateOrderRequest) -> OrderResponse:
        # Business logic implementation
        order = Order(order_data.customer_id, order_data.items)
        
        # Check inventory availability
        if not await self._inventory_service.check_availability(order.items):
            raise InsufficientInventoryError("Some items are not available")
        
        # Reserve items
        reservation = await self._inventory_service.reserve_items(order.items)
        
        try:
            # Process payment
            payment_result = await self._payment_service.process_payment(
                order.total_amount, 
                order_data.payment_method
            )
            
            # Confirm order
            order.confirm_payment(payment_result.payment_id)
            
            # Save order
            saved_order = await self._order_repository.save(order)
            
            # Send notification
            await self._notification_service.send_order_confirmation(
                order_data.customer_email, 
                saved_order
            )
            
            return OrderResponse.from_order(saved_order)
            
        except PaymentFailedException:
            # Release reserved items
            await self._inventory_service.release_reservation(reservation.id)
            raise

# Primary Adapter (REST Controller)
from fastapi import APIRouter, HTTPException, Depends

router = APIRouter()

@router.post("/orders", response_model=OrderResponse)
async def create_order(
    order_request: CreateOrderRequest,
    order_service: CreateOrderUseCase = Depends(get_order_service)
):
    try:
        return await order_service.execute(order_request)
    except (EmptyOrderError, InvalidOrderAmountError) as e:
        raise HTTPException(status_code=400, detail=str(e))
    except InsufficientInventoryError as e:
        raise HTTPException(status_code=409, detail=str(e))
    except PaymentFailedException as e:
        raise HTTPException(status_code=402, detail=str(e))

# Secondary Adapter (Database Repository)
class PostgreSQLOrderRepository(OrderRepository):
    def __init__(self, db_session):
        self._db = db_session
    
    async def save(self, order: Order) -> Order:
        # Database-specific implementation
        order_data = {
            'id': order.id,
            'customer_id': order.customer_id,
            'status': order.status.value,
            'total_amount': order.total_amount,
            'created_at': order.created_at
        }
        
        query = """
            INSERT INTO orders (id, customer_id, status, total_amount, created_at)
            VALUES (:id, :customer_id, :status, :total_amount, :created_at)
        """
        
        await self._db.execute(query, order_data)
        await self._db.commit()
        return order
    
    async def find_by_id(self, order_id: str) -> Optional[Order]:
        query = "SELECT * FROM orders WHERE id = :order_id"
        result = await self._db.fetch_one(query, {'order_id': order_id})
        
        if result:
            return Order.from_dict(result)
        return None

# Configuration (Dependency Injection)
def configure_order_service() -> CreateOrderUseCase:
    # Database connection
    db_session = get_database_session()
    
    # Repository implementations
    order_repository = PostgreSQLOrderRepository(db_session)
    
    # External service adapters
    payment_service = StripePaymentService(stripe_config)
    inventory_service = HTTPInventoryService(inventory_api_config)
    notification_service = SMTPNotificationService(email_config)
    
    # Application service
    return OrderService(
        order_repository,
        payment_service,
        inventory_service,
        notification_service
    )

실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점

구분고려사항주의할 점권장사항
설계 단계도메인 경계 명확화과도한 세분화 방지DDD 의 Bounded Context 활용
개발 단계인터페이스 우선 설계구현 세부사항 노출 금지계약 기반 개발 적용
테스트 단계계층별 테스트 전략 수립과도한 Mock 사용 주의Test Pyramid 원칙 준수
배포 단계설정 외부화하드코딩된 의존성 방지환경별 설정 분리
유지보수아키텍처 문서화문서와 코드 불일치코드로서의 문서화
팀 협업개발팀 교육학습 곡선 무시점진적 도입과 멘토링

최적화하기 위한 고려사항 및 주의할 점

구분고려사항주의할 점권장사항
성능추상화 오버헤드 최소화불필요한 계층 추가 방지성능 크리티컬 부분 직접 최적화
메모리객체 생성 비용 관리과도한 객체 래핑 주의객체 풀링과 캐싱 활용
네트워크외부 호출 최적화동기 호출의 성능 영향비동기 처리와 배치 처리
데이터베이스쿼리 최적화N+1 문제 발생 주의배치 로딩과 쿼리 튜닝
확장성수평적 확장 고려상태 공유 문제무상태 설계와 이벤트 기반 아키텍처
모니터링관찰 가능성 확보디버깅 복잡성 증가분산 추적과 로깅 전략

9. 주제와 관련하여 주목할 내용

카테고리주제항목설명
아키텍처 패턴Clean Architecture계층형 구조Uncle Bob 의 Clean Architecture 와의 관계 및 차이점
아키텍처 패턴Onion Architecture동심원 구조Jeffrey Palermo 의 양파 아키텍처와의 유사점
설계 원칙SOLID 원칙의존성 역전DIP 를 통한 의존성 방향 제어
설계 원칙Domain-Driven Design전술적 설계엔티티, 값 객체, 애그리게이트와의 연계
테스트 전략Test Pyramid계층별 테스트단위/통합/E2E 테스트 분배 전략
테스트 전략Contract TestingAPI 계약포트 인터페이스의 계약 검증
개발 방법론TDD테스트 주도 개발포트 인터페이스 우선 설계
개발 방법론BDD행동 주도 개발Use Case 중심의 개발 접근법
기술 트렌드Microservices서비스 분해마이크로서비스 아키텍처로의 진화
기술 트렌드Event Sourcing이벤트 기반도메인 이벤트와 이벤트 소싱
기술 트렌드CQRS명령 - 쿼리 분리읽기/쓰기 모델 분리
구현 기술Dependency InjectionIoC ContainerSpring, CDI 등 DI 프레임워크
구현 기술API Gateway서비스 메시마이크로서비스 간 통신 관리
구현 기술Message Queue비동기 통신RabbitMQ, Apache Kafka 등

10. 반드시 학습해야할 내용

카테고리주제항목설명
기초 개념소프트웨어 아키텍처아키텍처 원칙관심사 분리, 결합도/응집도, 추상화
기초 개념객체지향 설계SOLID 원칙특히 의존성 역전 원칙과 인터페이스 분리 원칙
설계 패턴GoF 디자인 패턴Adapter Pattern포트와 어댑터 구현의 핵심 패턴
설계 패턴GoF 디자인 패턴Strategy Pattern다양한 구현체 교체를 위한 패턴
설계 패턴Enterprise PatternsRepository Pattern데이터 접근 계층 추상화
설계 패턴Enterprise PatternsService Layer Pattern비즈니스 로직 계층 구성
도메인 설계Domain-Driven Design전략적 설계Bounded Context, Context Map
도메인 설계Domain-Driven Design전술적 설계Entity, Value Object, Aggregate
테스트 기법Test-Driven Development단위 테스트Mock, Stub 을 활용한 격리된 테스트
테스트 기법Integration Testing계약 테스트포트 인터페이스 계약 검증
구현 기술Dependency InjectionIoC 원리의존성 주입과 제어 역전
구현 기술Clean Code코드 품질가독성, 유지보수성, 테스트 가능성

용어 정리

카테고리용어설명
아키텍처 개념Port (포트)도메인과 외부 세계 간의 인터페이스로, 의도를 나타내는 계약
아키텍처 개념Adapter (어댑터)포트의 구현체로, 특정 기술과 도메인을 연결하는 구성요소
아키텍처 개념Primary Port외부에서 시스템으로의 입력을 정의하는 인터페이스 (Driving Port)
아키텍처 개념Secondary Port시스템에서 외부로의 출력을 정의하는 인터페이스 (Driven Port)
아키텍처 개념Domain Core비즈니스 규칙과 로직이 구현된 애플리케이션의 핵심부
아키텍처 개념Use Case시스템이 제공하는 특정 기능을 나타내는 비즈니스 시나리오
설계 원칙Dependency Inversion고수준 모듈이 저수준 모듈에 의존하지 않도록 하는 원칙
설계 원칙Separation of Concerns서로 다른 관심사를 별도의 모듈로 분리하는 설계 원칙
설계 원칙Interface Segregation클라이언트가 사용하지 않는 인터페이스에 의존하지 않도록 하는 원칙
구현 패턴Repository Pattern데이터 접근 로직을 캡슐화하여 도메인 객체와 분리하는 패턴
구현 패턴Service Layer비즈니스 로직을 구성하고 트랜잭션을 관리하는 계층
구현 패턴DTO (Data Transfer Object)계층 간 데이터 전송을 위한 객체
테스트 개념Mock Object테스트 시 실제 객체를 대체하는 가짜 객체
테스트 개념Stub미리 정해진 응답을 반환하는 테스트 더블
테스트 개념Contract Test서비스 간 API 계약을 검증하는 테스트
DDD 개념Bounded Context특정 도메인 모델이 적용되는 명시적 경계
DDD 개념Aggregate데이터 변경의 단위로 취급되는 연관된 객체들의 클러스터
DDD 개념Domain Event도메인 전문가가 관심을 갖는 비즈니스 이벤트

참고 및 출처


알겠습니다! “Hexagonal Architecture (Ports and Adapters Architecture)” 에 대한 단계별 9 번 작업을 수행한 내용을 아래와 같이 제공합니다.


1️⃣ 태그 (3‑4 개, 영어, 빈칸→‑로 표시)


2️⃣ 분류 구조 검토 및 제안

현재 구조:
Software Engineering > Design and Architecture > Architecture Styles and Patterns > Architecture Styles > Structural > Clean Architecture
→ 적절하며, Hexagonal Architecture 는 Clean Architecture 와 동급의 패턴입니다. 더욱 정확히 표현하자면:

1
2
3
4
5
6
7
Software Engineering
└─ Design and Architecture
   └─ Architecture Styles and Patterns
      └─ Architecture Styles
         ├─ Hexagonal Architecture (Ports & Adapters)
         ├─ Clean Architecture
         └─ Onion Architecture

즉, Clean Architecture 옆에 Hexagonal Architecture 를 배치하는 것이 계층적 정확성을 높입니다.


3️⃣ 요약 (≈ 200 자)

Hexagonal Architecture 는 알리스터 콕번 (Alistair Cockburn) 이 제안한 패턴으로, 애플리케이션의 핵심 로직 (Domain) 이 외부 시스템 (UI, DB, 메시징 등) 과 분리되도록 Ports(인터페이스) 와 Adapters(구현) 를 사용해 구조화합니다. 이 방식은 느슨한 결합, 높은 응집, 테스트 용이성, 기술 독립성을 제공하며 Clean Architecture 및 DDD 와 자연스럽게 결합됩니다.(위키백과, AWS Documentation, softengbook.org, DEV Community)


4️⃣ 전체 개요 (≈ 250 자)

Hexagonal Architecture(Ports and Adapters Architecture) 는 애플리케이션 핵심인 도메인 로직을 외부 의존성 (입력, 출력, 저장소 등) 으로부터 분리하는 아키텍처 패턴입니다. 내부 핵심은 Domain 클래스와 Use Case(또는 Interactor) 로 구성되며, 외부 시스템과는 Ports(추상 인터페이스) 를 통해 통신합니다. 外部와 통신하는 구체 구현은 Adapters 가 담당하여 핵심 로직을 보호합니다. 이를 통해 기술 스택 변경, 테스트 격리, 병렬 개발이 용이해지며 Clean Architecture, Onion Architecture 와 유사한 철학을 공유합니다.(위키백과, softengbook.org, bitloops.com)


5️⃣ 핵심 개념

5.1 실무 구현 연관성


6️⃣ 추가 조사된 주요 항목 정리


7️⃣ 실무 사용 예시 / 비교

비교: Hexagonal Vs Clean Vs Layered

항목Hexagonal ArchitectureClean Architecture전통적 Layered Architecture
구조 방식중심 도메인 + Ports/Adaptersconcentric rings, adapter 외부에 위치계층별 Presentation→Business→Data
의존성 흐름Adapters → Ports → Domain외부 → 내부 중심 도메인으로 방향성 강함상위 계층이 하위 계층에 의존
기술 독립성높음: adapter 교체 가능매우 높음상대적으로 낮음
테스트 용이성Core 단위 테스트 가능Core + Use Cases 테스트 유리UI / DB mocking 필요 복잡

8️⃣ 실무 활용 사례 (요약)

(자세한 사례 중심 활용 사례는 요청 시 머지 가능)


9️⃣ 도전 과제 / 고려사항

단점 (표)

구분항목설명해결책
단점Adapter 수 증가 및 복잡도 증가Ports 수 & adapter 구현 증가Port 과다 설계 방지, 단순화 전략
Latency 및 Overheadadapter 추상화로 인한 호출 지연profiling 기반 병목 파악 및 최적화 (AWS Documentation)
작은 프로젝트에는 과도 설계단순 프로젝트에 불필요한 구조핵심 기능 중심 최소한 구조 적용

문제점 (표)

구분항목원인영향탐지·진단예방 방법해결 방법
문제점Ports 정의 과도포트 granularity 과다 설계관리 난이도 증가코드 리뷰, 설계 점검최소 단위 port 적극 고민refactor 또는 통합
문제점Adapter 구현 중복동일 포트에 다수 adapter 존재유지보수 복잡화adapter 모듈 분석adapter 템플릿 활용공통 로직 추출, 테스트 생성

🔍 계속 진행 가능 항목

필요하신 방향을 알려주시면 해당 항목 추가로 정리해드립니다.

좋습니다! 이제 “Hexagonal Architecture (Ports & Adapters)” 에 대한 심화 활용 사례, 워크플로우, 코드 예시, 그리고 도메인‑포트‑어댑터 구조 설계를 포함한 내용을 이어서 정리해드리겠습니다. 이번에는 Python 기반 사례 중심으로 구성했습니다.


✅ 활용 사례: Python 기반 사용자 관리 시스템

시나리오:
REST API 를 통해 사용자 CRUD 기능을 제공하며, 도메인 로직은 데이터베이스나 웹 프레임워크에 독립적으로 설계.

시스템 구성:

시스템 구성 다이어그램:

graph LR
  Controller[Inbound Adapter\n(e.g. Flask)] -->|calls| ServicePort(Inbound Port)
  ServicePort --> Domain[Domain Logic / Use Cases]
  Domain --> RepoPort(Outbound Port)
  RepoPort --> DBAdapter[Outbound Adapter\n(SQLAlchemy)]

🛠 Workflow 및 역할

  1. 유저 요청 (HTTP POST /users) → Controller (Inbound Adapter)
  2. Controller 는 DTO → domain 파라미터로 변환 후 UserServicePort 호출
  3. UserUseCase (domain core) 내부에서 비즈니스 로직 실행 (중복 여부 확인 등)
  4. 필요한 경우 UserRepositoryPort 통해 저장 또는 조회
  5. 도메인이 반환한 정보를 Controller 는 응답 DTO 로 변환 후 클라이언트 응답

🧱 구현 예시 (Python 스타일, Flask + SQLAlchemy)

 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
52
53
54
55
56
57
58
59
60
61
62
63
64
# domain/entities/user.py
class User:
    def __init__(self, email: str, name: str):
        self.email = email
        self.name = name

# domain/ports/user_repository_port.py
from abc import ABC, abstractmethod

class UserRepositoryPort(ABC):
    @abstractmethod
    def save(self, user: User) -> None: ...
    @abstractmethod
    def find_by_email(self, email: str) -> User | None: ...

# domain/services/user_service.py
class UserService:
    def __init__(self, repo: UserRepositoryPort):
        self.repo = repo

    def create_user(self, email: str, name: str):
        if self.repo.find_by_email(email):
            raise ValueError("Email already exists")
        user = User(email, name)
        self.repo.save(user)
        return user

# adapters/db/user_repository_sqlalchemy.py
from domain.entities.user import User
from domain.ports.user_repository_port import UserRepositoryPort
from adapters.db.models import UserModel

class SqlAlchemyUserRepository(UserRepositoryPort):
    def __init__(self, session):
        self.session = session
    def save(self, user: User) -> None:
        orm = UserModel(email=user.email, name=user.name)
        self.session.add(orm)
        self.session.commit()

    def find_by_email(self, email: str):
        orm = self.session.query(UserModel).filter_by(email=email).first()
        return User(orm.email, orm.name) if orm else None

# entrypoints/http/flask_controller.py
from flask import Flask, request, jsonify
from domain.services.user_service import UserService
from adapters.db.user_repository_sqlalchemy import SqlAlchemyUserRepository
from adapters.db.session import db_session

app = Flask(__name__)
user_service = UserService(SqlAlchemyUserRepository(db_session))

@app.route('/users', methods=['POST'])
def create_user():
    data = request.json
    try:
        user = user_service.create_user(data['email'], data['name'])
        return jsonify({'email': user.email, 'name': user.name}), 201
    except ValueError as e:
        return jsonify({'error': str(e)}), 400

if __name__ == '__main__':
    app.run()

🔍 주목할 내용 요약 (표)

카테고리주제항목설명
설계 원칙Driver vs Driven Port 구분Inbound/Outbound Ports명확한 역할 구분
테스트 전략Core 테스트 독립성 유지Port mocking도메인 테스트 시 외부 의존 배제
적용 범위모듈 단위 Hexagon 설계 가능Modular Hexagon기능별 모듈 단위 독립 아키텍처
스케일 확장마이크로서비스 전환 용이성Adapter 교체 중심 분리기술 독립성 유지하며 분리 가능

📚 반드시 학습해야 할 내용 (보완)

카테고리주제항목설명
구조 설계Modular Hexagon Pattern기능별 독립 도메인 모듈확장성 및 독립성 확보 전략
아키텍처 검증Architecture Fitness TestsCI 환경에서 의존성 규칙 검사Ports‑Adapters 경계 유지
Port 설계Port Granularity포트 과다 또는 과소 설계단순성과 명확성 고려 설계

🧩 기타 사항


필요하시면 이 구조에 기반한 테스트 코드, CI 구성 예시, adapter 교체 전략, 또는 언어나 플랫폼을 넘은 설계 예시도 제공해드릴 수 있습니다. 추가 요청 방향을 알려주세요!

다음은 **Hexagonal Architecture (Ports & Adapters)** 의 도전 과제 보완, 테스트 및 아키텍처 검증 전략, 최적화 고려사항, 그리고 CI 기반 설계 검증에 대한 심화 내용입니다.


🧪 아키텍처 검증 및 테스트 전략


⚙️ 성능 최적화 고려사항

항목위험 요소대책
추상화 호출포트 - 어댑터 오버헤드로 CPU/메모리 지연 가능profiling 도구 활용, critical path inline 최적화
포트 granularity포트 인터페이스가 너무 세분화되면 구현복잡도 상승포트 설계 시 단순함과 재사용성 균형 조정
adapter 수 증가다수 adapter 관리 부담공통 코드 추출, 템플릿 또는 베이스 클래스 활용

🎯 CI 기반 Hexagonal Architecture 검증 예시 (Python Workflow)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install
        run: pip install -r requirements.txt
      - name: Run Unit Tests
        run: pytest tests/unit
      - name: Run Architecture Fitness Tests
        run: pytest tests/test_architecture_fitness.py
      - name: Run Integration Tests
        run: pytest tests/integration

🔍 도전 과제 보완: 사용자 경험 고려


핵심 요약

필요하시면 위 내용을 기반으로 fitness test 코드 예시, CI pipeline 전체 구성, 다른 언어 프로젝트 구조, 도메인별 adapter 분리 전략 등 더 심화된 자료도 제공 가능합니다. 요청 주시면 이어서 정리해드리겠습니다!

알겠습니다! 다음은 **Hexagonal Architecture (Ports & Adapters)** 의 구성요소별 심화 구조 설계와 architecture fitness test 코드 예시, 단위 테스트 & 통합 테스트 사례, 다른 언어 플랫폼 구성 구조 요약입니다.


1️⃣ 모듈 단위 Hexagon 설계: 구조 설계 다이어그램

각 기능 (예: 사용자 (user), 주문 (order), 상품 (product)) 마다 독립된 hexagon 모듈로 설계할 수 있습니다.

graph TD
  subgraph UserModule
    UI1[UserController]
    UPo(UserServicePort)
    UseCasesUC(UserService)
    UPo --> UseCasesUC
    UseCasesUC --> URepPort(UserRepositoryPort)
    URepPort --> UDBAdapter(SQLAdapter)
  end

  subgraph OrderModule
    UI2[OrderController]
    OrPo(OrderServicePort)
    OrUC(OrderService)
    OrUC --> ORepPort(OrderRepositoryPort)
    ORepPort --> ODBAdapter(SQLAdapter)
  end

  subgraph CommonAdapters
    SQLAdapter((Shared DB Adapter))
    EmailAdapter((Email Sender Adapter))
  end

설명:


2️⃣ Architecture Fitness Test 예시 (Python, pytest)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# tests/test_architecture_fitness.py
import os
import pytest

def test_adapters_do_not_import_domain():
    import adapters
    domain_modules = []
    for root, _, files in os.walk("adapters"):
        for f in files:
            if f.endswith(".py"):
                path = os.path.join(root, f)
                content = open(path).read()
                assert "import domain" not in content, f"{path} imports domain directly"

def test_ports_are_only_imported_by_adapters_or_domain():
    # Ensure adapters import ports, domain imports ports, but ports don't import adapters
    from domain import ports
    assert not hasattr(ports, 'adapters')

3️⃣ 테스트 구조 및 예시

✅ Unit Test (domain 핵심 로직만 검증)

 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
# tests/unit/test_user_service.py
import pytest
from domain.services.user_service import UserService
from domain.entities.user import User

class FakeRepo:
    def __init__(self):
        self.data = {}
    def save(self, user):
        self.data[user.email] = user
    def find_by_email(self, email):
        return self.data.get(email)

def test_create_user_success():
    repo = FakeRepo()
    svc = UserService(repo)
    user = svc.create_user("a@example.com", "Alice")
    assert user.email == "a@example.com"

def test_create_user_duplicate():
    repo = FakeRepo()
    repo.save(User("a@example.com", "Alice"))
    svc = UserService(repo)
    with pytest.raises(ValueError):
        svc.create_user("a@example.com", "Alice")

🧪 Integration Test (Adapter 와 외부 시스템 연동)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# tests/integration/test_user_repository.py
import pytest
from adapters.db.user_repository_sqlalchemy import SqlAlchemyUserRepository
from domain.entities.user import User
from adapters.db.session import db_session

def test_save_and_find():
    repo = SqlAlchemyUserRepository(db_session)
    user = User("b@example.com", "Bob")
    repo.save(user)
    found = repo.find_by_email("b@example.com")
    assert found and found.email == "b@example.com"

4️⃣ 다른 언어 및 플랫폼 구성 요약

언어/플랫폼구조 방식특징
Node.js + Expressdomain/core, ports, controllers, repositoriesadapter 는 Express 라우터, ORM 등으로 구현
Go (Golang)pkg/domain, pkg/ports, pkg/adapters 구조정적 타입, 인터페이스 설계에 적합, DI 없이 간단 구조
Kotlin + Spring Bootport 인터페이스, adapter 구현, configuration 클래스Spring DI 및 bean 방식으로 adapter 연결 구조 유지
Python + AWS Lambdahandler 와 domain 분리, adapter 는 이식 가능한 모듈 형식serverless 환경에서 변경 없이 domain 재사용 가능

🧭 요약: 지금까지 정리한 아키텍처 검증 요건

더 구체적인 Go 구현 예시, Node.js 프로젝트 구조, Kotlin Spring Boot DI 설정, AWS Lambda 서버리스 Hexagon 설계 등이 필요하시면 말씀해 주세요! 강조하고 싶은 특정 부분 요청하시면 해당 방향으로 정리해드리겠습니다.

다음은 요청하신 Hexagonal Architecture 에 대한 미작성된 항목 중 다음 세 가지를 보완하여 작성한 표입니다.


✅ 분류 기준에 따른 종류 및 유형

분류 기준유형설명
포트 방향Inbound Port / Outbound Port애플리케이션 진입 및 외부 리소스 호출을 위한 구분
아키텍처 구성 방식모듈형 Hexagon / 모놀리식 Hexagon각 기능을 모듈로 분리할지 여부에 따라 구조 결정
기술 스택 대응HTTP / gRPC / Event AdapterInbound/Outbound 어댑터의 기술적 채택 방식에 따라 구분

✅ 실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점

항목설명권장사항
포트 과잉 정의각 use case 마다 포트를 분리하면 유지보수 어려움기능 단위 묶음 기준으로 port 설계
모듈화 전략단일 Hexagon 구조는 기능 확장에 어려움기능 기반 모듈로 Hexagon 분리
의존성 주입DI 설정이 복잡해질 수 있음프레임워크 기능 또는 수동 주입으로 간단화
테스트 구성테스트 범위 혼동 가능Domain 은 Unit Test, Adapter 는 Integration Test 분리

✅ 최적화하기 위한 고려사항 및 주의할 점

항목설명권장사항
Adapter 호출 최적화지나친 추상화는 성능 저하 원인핵심 경로는 인라인 최적화 및 직접 호출 병행
I/O 집중 영역DB, API 등 Outbound Adapter 병목 발생 가능비동기 처리, 캐싱, 리트라이 패턴 적용
테스트 병목전체 테스트 수행 시간이 길어짐도메인 테스트는 병렬, 어댑터 테스트는 주기적 수행
코드 난이도 증가추상 레이어가 많아 가독성 저하문서화, 코드 템플릿, 표준 명명 규칙 운영

좋습니다. 이어서 다음 항목들을 작성하겠습니다.


📌 기타 사항


📚 용어 정리

카테고리용어설명
아키텍처Hexagonal ArchitecturePorts 와 Adapters 를 통해 핵심 로직과 외부 요소를 분리하는 아키텍처 스타일
아키텍처Port도메인 계층이 외부와 소통하기 위해 정의하는 추상화된 인터페이스
아키텍처Adapter포트를 구현하여 외부 기술 요소 (UI, DB 등) 와 연결하는 계층
아키텍처Inbound Port외부 요청이 도메인 로직으로 진입하기 위한 인터페이스
아키텍처Outbound Port도메인이 외부 시스템에 의존을 표현할 때 사용하는 인터페이스
설계 원칙Dependency Rule외부는 내부를 알 수 있지만, 내부는 외부를 알 수 없음 (의존 방향성 규칙)
개발 기법TDD (Test-Driven Development)테스트를 먼저 작성하고 그에 맞는 기능을 구현하는 개발 방식
테스트Architecture Fitness Test아키텍처 구조적 규칙을 코드 수준에서 검증하는 테스트 기법

🔗 참고 및 출처


✅ 마무리 요약

Hexagonal Architecture 는 소프트웨어의 핵심 로직을 외부 시스템으로부터 분리함으로써 유지보수성과 테스트 용이성을 극대화합니다. 기술 독립성과 도메인 중심 설계를 가능하게 하며, 마이크로서비스 또는 서버리스 이전에 기반이 되는 유연한 구조입니다. Ports(인터페이스) 와 Adapters(구현체) 를 통해 의존성을 명확하게 분리하고, 아키텍처 규칙을 검증할 수 있는 테스트와 자동화 전략까지 지원할 수 있는 강력한 패턴입니다.


이제 전체 항목이 모두 작성 완료되었습니다. 추가로 특정 플랫폼의 예제, 언어별 프로젝트 구조 예시, 또는 비교 기반 문서가 필요하시다면 요청해 주세요.


1. 태그


2. 분류 구조 분석

계층 구조:
Computer Science and Engineering > Software Engineering > Design and Architecture > Architecture Styles and Patterns > Architecture Patterns

분석 및 근거:
Hexagonal Architecture 는 소프트웨어 엔지니어링의 설계 및 아키텍처 분야에서 널리 사용되는 아키텍처 패턴 (Architecture Pattern) 으로, 의존성 역전과 인터페이스 기반 설계를 통해 결합도를 낮추고 유지보수성과 확장성을 높이는 것이 목적입니다. 이는 “Architecture Styles and Patterns” 하위의 “Architecture Patterns” 에 적합하게 분류됩니다. 실제로 Hexagonal Architecture 는 레이어드 (Layered) 구조와 도메인 중심 설계 (Domain-Driven Design) 의 영향을 받아 설계되었으며, 소프트웨어 엔지니어링의 핵심 설계 원칙에 부합합니다 13.


3. 요약 (200 자 내외)

Hexagonal Architecture 는 비즈니스 로직을 중심에 두고, 포트와 어댑터를 통해 외부 기술 의존성을 분리해 결합도를 낮추고 유지보수성과 테스트 용이성을 높이는 아키텍처 패턴이다 14.


4. 개요 (250 자 내외)

Hexagonal Architecture(포트와 어댑터 아키텍처) 는 애플리케이션의 핵심 비즈니스 로직을 외부 프레임워크, 데이터베이스, UI 등과 분리해, 변경에 유연하고 테스트가 용이하며, 장기적으로 유지보수와 확장이 쉬운 구조를 제공하는 설계 방법론이다 14.


5. 핵심 개념

실무 구현 요소


6. 조사 내용 (주요 항목별 정리)

배경

Hexagonal Architecture 는 계층형 아키텍처의 단점 (계층 간 의존성, 비즈니스 로직과 UI/DB 코드 혼재) 을 보완하기 위해 Alistair Cockburn 이 2005 년에 제안한 아키텍처 패턴입니다 13.

목적 및 필요성

주요 기능 및 역할

특징

핵심 원칙

주요 원리 및 작동 원리

 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
[Diagram: Hexagonal Architecture Layers]
       +-------------------------------+
       |        Adapter (Inbound)      |
       | (e.g. REST API, UI, Message)  |
       +---------------+---------------+
                       |
       +---------------v---------------+
       |           Port (Inbound)      |
       | (Interface for Core Service)  |
       +---------------+---------------+
                       |
       +---------------v---------------+
       |           Core (Domain)       |
       | (Business Logic, Entities)    |
       +---------------+---------------+
                       |
       +---------------v---------------+
       |           Port (Outbound)     |
       | (Interface for Infra Service) |
       +---------------+---------------+
                       |
       +---------------v---------------+
       |        Adapter (Outbound)     |
       | (e.g. DB, External Service)   |
       +-------------------------------+

의존성은 항상 안쪽 (코어) 으로 향함. 외부 (어댑터) 가 내부 (코어) 를 호출하거나 사용할 수 있지만, 내부 (코어) 는 외부 (어댑터) 를 알지 못함 56.

구조 및 아키텍처

각 계층의 역할

구현 기법

장점

구분항목설명특성 원인
장점유지보수성변경이 용이하고, 장기적으로 관리가 쉬움도메인과 기술 분리, 포트 - 어댑터 구조
장점확장성새로운 기능 추가 및 기술 교체가 쉬움인터페이스 활용, 의존성 역전
장점테스트 용이성도메인과 유스케이스가 독립적으로 테스트 가능외부 의존성 분리
장점프레임워크 독립성프레임워크 교체가 쉬움도메인과 기술 분리

단점과 문제점 그리고 해결방안

구분항목설명해결책
단점초기 설계 복잡성계층 구조 설계와 인터페이스 정의가 필요경험 축적, 도메인 중심 설계 강조
단점학습 곡선개발자 교육 및 이해 필요교육, 문서화, 예시 코드 제공
구분항목원인영향탐지 및 진단예방 방법해결 방법 및 기법
문제점계층 간 혼합설계 미흡, 인터페이스 미정의테스트 및 유지보수 어려움코드 리뷰, 정적 분석인터페이스 명확화, 계층 분리리팩토링, 계층 재설계
문제점포트 경계 모호도메인 정의 미흡, 포트 설계 부족비즈니스 로직과 인프라 혼재코드 리뷰, 아키텍처 리뷰도메인 모델링 강화, 포트 설계 명확화도메인 중심 설계, 포트 리팩토링

도전 과제

구분항목원인영향탐지 및 진단예방 방법해결 방법 및 기법
도전 과제대규모 시스템 적용계층 간 통신 및 관리 복잡성능 저하, 유지보수 어려움모니터링, 프로파일링모듈화, 마이크로서비스 전환마이크로서비스 아키텍처 적용, 도메인 분리
도전 과제팀 온보딩 및 코드 관리아키텍처 복잡성, 팀원 이해 부족개발 비용 증가, 일관성 저하코드 리뷰, 문서화교육, 예시 코드 제공, 멘토링문서화, 코드 리뷰, 멘토링

분류 기준에 따른 종류 및 유형

분류 기준종류/유형설명
계층 구조포트 - 어댑터 구조외부와의 연결을 포트와 어댑터로 분리
설계 패턴Hexagonal Architecture의존성 역전, 인터페이스 활용, 도메인 중심 설계
적용 범위모노리틱, 마이크로서비스둘 다 적용 가능, 마이크로서비스에 적합

실무 사용 예시

사용 목적함께 사용하는 기술효과
유지보수성 향상Spring, Django, Node.js도메인과 기술 분리로 변경 용이
테스트 용이성JUnit, pytest도메인과 유스케이스 독립 테스트
프레임워크 교체다양한 ORM, DB인터페이스 활용으로 교체 용이

활용 사례

카카오페이 홈 서버:
Hexagonal Architecture 를 적용해 도메인과 기술을 분리하여, 다양한 연동 API 에 유연하게 대응. 시스템 구성은 도메인 (비즈니스 규칙), 포트 (외부와의 인터페이스), 어댑터 (외부 시스템 통합) 로 계층화됨.
Workflow:
외부 API → 인바운드 어댑터 → 인바운드 포트 → 코어 (도메인) → 아웃바운드 포트 → 아웃바운드 어댑터 → 외부 시스템
역할:
도메인: 비즈니스 규칙, 포트: 외부와의 인터페이스, 어댑터: 외부 시스템 통합 11.

구현 예시 (Python)

 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
# domain/user.py
class User:
    def __init__(self, id, name):
        self.id = id
        self.name = name

# port/user_port.py
class UserPort:
    def get_user(self, user_id): pass

# adapter/user_adapter.py
class UserAdapter(UserPort):
    def get_user(self, user_id):
        # 실제로는 DB나 외부 API에서 데이터를 가져옴
        return User(user_id, "Alice")

# service/user_service.py
class UserService:
    def __init__(self, user_port: UserPort):
        self.user_port = user_port
    def get_user(self, user_id):
        return self.user_port.get_user(user_id)

# main.py
adapter = UserAdapter()
service = UserService(adapter)
user = service.get_user(1)
print(user.name)  # Alice

실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점

항목설명권장사항
도메인 중심 설계도메인을 명확히 정의하고, 비즈니스 규칙을 도메인에 포함도메인 모델링 강화
인터페이스 활용계층 간 인터페이스로 결합도 낮춤인터페이스 명확화
테스트 코드 작성도메인과 유스케이스는 독립적으로 테스트TDD, 단위 테스트 강화
계층 분리계층 간 혼합 방지코드 리뷰, 정적 분석

최적화하기 위한 고려사항 및 주의할 점

항목설명권장사항
모듈화각 계층별로 모듈화하여 관리마이크로서비스 적용 고려
성능 최적화계층 간 통신 최소화, 캐싱 활용프로파일링, 캐싱 전략 적용
유지보수성문서화, 코드 리뷰문서화, 코드 리뷰 강화

기타 사항


7. 추가 조사 내용


8. 주목할 내용

카테고리주제항목설명
설계 패턴Hexagonal Architecture도메인 중심비즈니스 로직을 중심에 두고 외부 기술과 분리
설계 패턴Hexagonal Architecture포트 - 어댑터 구조외부와의 연결을 인터페이스와 구현체로 분리
실무 적용Hexagonal Architecture마이크로서비스도메인 분리, 독립적 배포에 유리
실무 적용Hexagonal Architecture테스트 전략도메인과 유스케이스 독립 테스트

9. 반드시 학습해야 할 내용

카테고리주제항목설명
설계 원칙Hexagonal Architecture의존성 역전내부가 외부에 의존하지 않음
설계 원칙Hexagonal Architecture인터페이스 활용계층 간 결합도 낮춤
실무 적용Hexagonal Architecture도메인 모델링비즈니스 규칙 명확화
실무 적용Hexagonal Architecture테스트 코드 작성도메인과 유스케이스 독립 테스트

10. 용어 정리

카테고리용어설명
설계 패턴Hexagonal Architecture도메인 중심, 포트 - 어댑터 구조, 의존성 역전을 갖춘 소프트웨어 설계 패턴
설계 원칙의존성 역전 (Dependency Inversion)고수준 모듈이 저수준 모듈에 의존하지 않도록 인터페이스 활용
설계 원칙인터페이스 분리 (Interface Segregation)인터페이스를 최소 단위로 분리하여 결합도 낮춤
실무 적용도메인 (Domain)비즈니스 엔티티와 규칙을 포함하는 계층
실무 적용포트 (Port)외부와의 상호작용을 위한 인터페이스
실무 적용어댑터 (Adapter)외부 시스템과의 통합을 담당하는 구현체

11. 참고 및 출처


1. 태그

1
Hexagonal-Architecture, Ports-and-Adapters, Clean-Software-Design, Domain-Isolation

2. 분류 계층 적절성 분석


3. 200 자 요약

Hexagonal Architecture(Ports & Adapters) 는 비즈니스 로직을 핵심 (Core) 에 집중시키고, 외부 입출력 모듈 (UI, DB, 메시징 등) 을 포트와 어댑터로 연결해 의존성을 내부로만 흐르게 하는 아키텍처입니다. 테스트 용이성과 유연성을 확보하고, 외부 기술 변화에도 핵심 코드 비침투성을 보장합니다.


4. 250 자 개요

Hexagonal Architecture 는 Alistair Cockburn(2005) 가 제안한 Ports & Adapters 패턴으로, 핵심 비즈니스 로직 (Core) 을 중심에 두고 외부 시스템 (UI, DB, API, 메시지 등) 을 포트 (인터페이스) 와 어댑터 (구현) 로 분리하는 구조입니다. 내부 코드는 외부 기술에 전혀 의존하지 않으며, 외부 변화에도 유연하게 대응 가능한 설계가 가능합니다. 또한, 단위 테스트가 쉽고 마이크로서비스 설계 시 모듈화가 용이합니다. 테스트 및 기술 변경 비용을 크게 줄이는 동시에, 비즈니스 중심 개발을 촉진합니다. (en.wikipedia.org)


5. 핵심 개념 및 실무 구현 요소

핵심 개념

5.1 실무 구현 요소


다음 단계로:

Hexagonal architecture: introduction and structure - WATA Factory


5. 구조 및 아키텍처 (Structure & Architecture)

Hexagonal Architecture 는 Ports & Adapters라 불리며, 내부 비즈니스 로직 (Core/Domain) 을 ** 포트 (Ports)** 라는 인터페이스로 외부와 연결하고, 실제 구현은 어댑터 (Adapters) 형태로 외부에 둡니다 (en.wikipedia.org).

계층 구조 및 구성 요소

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
             +----------------------------+
             |      Primary Adapters      |
   UI/API →  |   (REST controllers, CLI) |
             +------------+---------------+
                          |
                 +--------v--------+
                 |   Inbound Port   |
                 +--------+--------+
          +---------------+------------------+
          |                                      |
          |       Domain / Application Core     |
          | (Entities, Use Cases, Domain Logic) |
          |                                      |
          +---------------+------------------+
                 +--------+--------+
                 |   Outbound Port  |
                 +--------+--------+
                          |
             +------------v---------------+
             |     Secondary Adapters     |
             | (DB, MQ, External API...) |
             +----------------------------+

필수 구성요소 Vs 선택 구성요소


6. 주요 원리 & 작동 원리

핵심 원리

작동 흐름 (다이어그램)

flowchart LR
  A[UI/API/CLI/Test] -->|calls| IP[Input Port Interface]
  IP -->|executes| UA[Use Case in Core]
  UA -->|calls| OP[Output Port Interface]
  OP -->|delegates to| SA[Secondary Adapter]
  SA -->|interacts with| Svc[DB / MQ / External API]
  UA -->|returns result| IP
  IP -->|responds to| A

7. 구성 요소 기능 요약

구성 요소역할
Domain/Core순수 비즈니스 로직, 상태 유지
Input Port외부 요청 인터페이스 계약
Use Cases유스케이스 단위 실행 로직
Output Port외부 호출 인터페이스 계약
Primary Adapter사용자 요청을 Port 호출로 변환
Secondary AdapterPort 호출을 외부 기술로 실행
DI 구성 모듈Port 와 Adapter 를 연결하는 설정 코드

아래는 8. 구현 기법, 9. 장단점 분석, 10. 문제점 상세 분석, 11. 실무 사용 예시, 12. 활용 사례, 13. 구현 예시 섹션입니다.


8. 구현 기법 (Implementation Techniques)


9. 장단점 분석

✅ 장점

구분항목설명
장점독립성핵심 로직은 외부 기술에 무관함
테스트 용이성핵심 Port 만 모킹하면 유닛 테스트 가능
유연성새로운 Adapter 추가 시 Core 무변경 가능
모듈화마이크로서비스 설계 및 배포 단위 조직 가능
유지보수성의존성 흐름 내부 집중, 외부 변화 영향 최소화

❌ 단점

구분항목설명해결책
단점초기 복잡도Port/Adapter 계층 도입으로 구조 복잡단순 프로젝트엔 Vertical Slice 방식 or Onion 구조 병행
설정·DI 오버헤드DI 설정 및 모듈 구성 필요DI 컨테이너 자동 구성 기능 or 컴파일타임 DI 사용
Boilerplate 증가Port/Adapter 양식 반복코드 생성 스크립트, 공통 유틸 라이브러리 활용

10. 문제점 상세 분석

구분항목원인영향탐지예방해결
문제점Port 남발핵심이 아닌 부분까지 추상화 대상 설정Adapter 수 급증, 이해도 저하코드 리뷰 시 계층 과도 확인필요 부분만 Port 도입, 가이드 정책 설정Port 정리 리팩토링, 코드 컨벤션 강화
문제점Adapter 복잡도외부 API 변화로 복잡한 매핑 처리Core 호출 실패, 버그 증가테스트 실패, 예외 로그 증가Adapter Unit 테스트 자동화Mapper 추상 추출, 오류 대응 유틸 추가

11. 실무 사용 예시

기술 조합목적효과
Java Spring + Hexagonal레거시 Monolith → 모듈화 리팩토링도메인 분리, Adapter 변경 용이
NestJS + TypeORM + RabbitMQ이벤트 기반 주문 처리Handler/test 분리, 비동기 기능 개선
Python FastAPI + SQLAlchemyAPI 서버에서 비즈니스 모듈 분리Port 테스트 가능, DB 교체 쉬움

12. 활용 사례: 이벤트 기반 주문 시스템

🏗️ 시스템 구성

graph LR
  REST[REST API] --> IP[OrderService Port]
  IP --> UC[CreateOrderInteractor]
  UC --> OP1[PaymentGateway Port]
  OP1 --> Adp1[StripeAdapter]
  UC --> OP2[OrderRepo Port]
  OP2 --> Adp2[PostgresAdapter]
  UC --> OP3[EventPort]
  OP3 --> Adp3[RabbitMQAdapter]

📋 워크플로우

  1. REST 요청 → Inbound Port 호출

  2. Interactor:

    • 결제 처리 via StripeAdapter
    • 주문 저장 via PostgresAdapter
    • 이벤트 발행 via RabbitMQAdapter
  3. 각 Adapter 는 해당 외부 서비스를 호출

  4. Core 로직은 Port 만 참고하여 외부 독립적


13. 구현 예시 (Python + FastAPI)

 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
52
53
54
55
56
57
58
59
60
61
# core/port.py
from abc import ABC, abstractmethod
from pydantic import BaseModel

class PaymentPort(ABC):
    @abstractmethod
    def charge(self, amount: float) -> str: ...

class OrderPort(ABC):
    @abstractmethod
    def save(self, order): ...

# core/usecase.py
class CreateOrderUseCase:
    def __init__(self, pay: PaymentPort, repo: OrderPort):
        self.pay = pay
        self.repo = repo

    def execute(self, order_data):
        tx = self.pay.charge(order_data.amount)
        order = {"id": order_data.id, "status": "PAID", "tx": tx}
        self.repo.save(order)
        return order

# adapters/payment_stripe.py
class StripePaymentAdapter(PaymentPort):
    def charge(self, amount):
        # 실제 Stripe 라이브러리 호출
        return "stripe_tx_123"

# adapters/order_repo.py
class InMemoryOrderRepo(OrderPort):
    def __init__(self):
        self.db = {}

    def save(self, order):
        self.db[order["id"]] = order

# app.py FastAPI 설정
from fastapi import FastAPI
from core.usecase import CreateOrderUseCase
from core.port import PaymentPort, OrderPort
from adapters.payment_stripe import StripePaymentAdapter
from adapters.order_repo import InMemoryOrderRepo
from pydantic import BaseModel
import uuid

app = FastAPI()
pay_adapter = StripePaymentAdapter()
order_repo = InMemoryOrderRepo()
usecase = CreateOrderUseCase(pay_adapter, order_repo)

class OrderRequest(BaseModel):
    amount: float

@app.post("/orders")
def create_order(req: OrderRequest):
    order_data = req
    order_data.id = str(uuid.uuid4())
    order = usecase.execute(order_data)
    return order

다음은 요청하신 도전 과제, 실무 적용 고려사항 및 주의할 점, 최적화 고려사항 및 주의할 점, 용어 정리, 참고 및 출처 항목입니다.


🎯 14. 도전 과제 (Challenges)

  1. 빈약한 도메인 모델 (Anemic Domain Model)

    • 원인: 도메인 로직이 Core 가 아닌 Adapter 에 흩어짐
    • 영향: 객체지향 설계 원칙 위반, 유지보수성 저하 (medium.com, geeksforgeeks.org)
    • 탐지/진단: 도메인 객체가 단순 데이터 보관자일 경우
    • 예방: Tell‑don’t‑ask 패턴, 도메인 내 로직 집중
    • 해결: 도메인 모델로 비즈니스 로직 이동 후 테스트 자동화
  2. ORM 및 데이터 피그 (Primitive Obsession)

    • 원인: 도메인 모델과 DB 간 불일치, ORM 직접 노출
    • 영향: 테스트 어려움, 데이터 중복 변환 발생 (medium.com, medium.com)
    • 탐지: DB 전용 속성 증가
    • 예방: Value Object 사용, domain‑first 디자인
    • 해결: Adapter 에서 DTO 매핑, 도메인 순수성 유지
  3. Adapter 간 상호 호출 (오용)

  4. 프로젝트 스캐터 (분산화 복잡도)

    • 원인: 과도한 패키지 및 계층 분리
    • 영향: 초기 진입 장벽, 코드 탐색 어려움
    • 탐지: 디렉터리 및 모듈 수 증가
    • 예방: 패키징 정책 수립, 문서화, 코드 템플릿 활용
    • 해결: 리팩토링, 템플릿 기반 초기세팅

📋 15. 실무 적용 고려사항 및 주의할 점

항목설명권장사항
프로젝트 규모단순한 CRUD 에는 과도한 구조Vertical Slice 또는 Onion 구조 사용
개발 역량패턴 숙지 부족 시 오남용 위험코드 컨벤션, 설계 리뷰, 교육 병행
포트 정립과도한 Port 정의핵심 흐름만 Port 로 정의, 모듈화
DI 도구 선택무거운 DI 도구 사용 시 개념 복잡컴파일타임 DI(AOT) 또는 경량 프레임워크
테스트 범위유닛 테스트만으로는 부족End-to-end, Contract Testing 병행
문서화구조 복잡으로 팀 혼돈 발생다이어그램 + 코드 매핑 문서화 필수

⚙️ 16. 최적화 고려사항 및 주의할 점

항목설명권장사항
성능 오버헤드DI 호출, 포트/어댑터 레이어 소모AOT DI, 캐싱, 매핑 최소화
빌드 병목모듈 분리 많으면 CI 빌드 지연병렬/증분 빌드 지원, 모노레포 구조 고려
코드량 증가Port/Adapter 코드 반복코드 생성 도구, 템플릿 활용
테스트 유지관리Adapter 불일치로 유지 단점 발생Contract Tests + Mock 자동화
패키징 복잡도모듈 수 증가로 탐색 어려움기능 기반 패키징, 의존 관계 문서화

📌 용어 정리

카테고리용어설명
패턴Anemic Domain Model도메인 객체가 로직 없이 데이터만 보관
패턴Tell‑don’t‑ask도메인 메시징 중심 설계, 로직 캡슐화 강화
개념Value Object불변 객제로 도메인 속성 표현
설계원칙Contract Testing사용자의 기대 인터페이스를 테스트로 보증
도구AOT DI컴파일 시점 의존성 주입 구조 생성

📚 참고 및 출처


해당 내용까지 Hexagonal Architecture 의 이론과 실무 가이드라인을 모두 정리했습니다. 추가 코드 최적화, 프레임워크별 적용 전략 등을 원하시면 언제든 요청해 주세요!

안녕하세요! 헥사고날 아키텍처 (Hexagonal Architecture) 에 대한 포괄적인 조사를 시작하겠습니다. 먼저 관련 정보를 웹에서 검색하여 최신 자료를 수집한 후 체계적으로 정리해드리겠습니다.

헥사고날 아키텍처 (Hexagonal Architecture) 포괄적 조사

1. 태그 및 분류

태그: Hexagonal-Architecture, Ports-and-Adapters, Software-Design-Pattern, Domain-Driven-Design

주제 분류 적절성 검토:
제시된 분류 “Computer Science and Engineering” > “Software Engineering” > “Design and Architecture” > “Architecture Styles and Patterns” > “Architecture Patterns” 는 매우 적절합니다. 헥사고날 아키텍처는 소프트웨어 설계 패턴 중 아키텍처 패턴에 해당하며, 시스템의 전체적인 구조를 정의하는 설계 원칙이므로 이 분류가 정확합니다.

요약 문장: 헥사고날 아키텍처는 비즈니스 로직을 외부 의존성으로부터 격리하여 포트 (Ports) 와 어댑터 (Adapters) 를 통해 통신하게 함으로써 테스트 용이성, 유연성, 유지보수성을 향상시키는 소프트웨어 설계 패턴입니다.

개요: 2005 년 Alistair Cockburn 이 창안한 헥사고날 아키텍처는 포트와 어댑터 패턴이라고도 불리며, 애플리케이션의 핵심 비즈니스 로직을 중앙에 배치하고 외부 시스템과의 상호작용을 표준화된 인터페이스를 통해 처리합니다. 이를 통해 기술적 의존성 없이 독립적으로 테스트 가능하고 유연한 시스템을 구축할 수 있습니다.


2. 첫 번째 부분: 기본 개념 및 배경

핵심 개념

헥사고날 아키텍처의 핵심 개념들과 실무 구현 요소:

이론적 핵심 개념:

실무 구현 요소:

배경

헥사고날 아키텍처는 2005 년 Alistair Cockburn 에 의해 창안되었습니다. 전통적인 계층형 아키텍처 (Layered Architecture) 의 한계를 극복하기 위해 개발되었으며, 특히 다음과 같은 문제점들을 해결하고자 했습니다:

목적 및 필요성

주요 목적:

  1. 비즈니스 로직을 외부 기술적 관심사로부터 격리
  2. 애플리케이션이 사용자, 프로그램, 자동화된 테스트, 배치 스크립트에 의해 동등하게 구동될 수 있도록 함
  3. 런타임 디바이스와 데이터베이스로부터 독립적으로 개발 및 테스트 가능하도록 함

필요성:

주요 기능 및 역할

헥사고날 아키텍처의 주요 기능:

  1. 비즈니스 로직 보호: 도메인 계층을 외부 변화로부터 보호
  2. 인터페이스 표준화: 포트를 통한 일관된 통신 방식 제공
  3. 기술적 독립성: 특정 프레임워크나 라이브러리에 종속되지 않음
  4. 확장성: 새로운 어댑터 추가로 기능 확장 용이
  5. 테스트 격리: 목 (Mock) 객체를 통한 독립적 테스트
특징

핵심 특징:


3. 두 번째 부분: 원칙과 구조

핵심 원칙
  1. 의존성 역전 원칙 (Dependency Inversion Principle): 고수준 모듈이 저수준 모듈에 의존하지 않음
  2. 단일 책임 원칙 (Single Responsibility Principle): 각 구성 요소는 하나의 명확한 책임을 가짐
  3. 인터페이스 분리 원칙 (Interface Segregation Principle): 클라이언트가 사용하지 않는 인터페이스에 의존하지 않음
  4. 개방 - 폐쇄 원칙 (Open-Closed Principle): 확장에는 열려있고 수정에는 닫혀있음
주요 원리

작동 원리 다이어그램:

graph TD
    subgraph "External World"
        UI[User Interface]
        CLI[Command Line]
        Test[Test Scripts]
        DB[(Database)]
        API[External API]
        Queue[Message Queue]
    end
    
    subgraph "Hexagonal Architecture"
        subgraph "Adapters Layer"
            PA1[Primary Adapter<br/>Web Controller]
            PA2[Primary Adapter<br/>CLI Handler]
            SA1[Secondary Adapter<br/>DB Repository]
            SA2[Secondary Adapter<br/>API Client]
        end
        
        subgraph "Ports Layer"
            PP1[Primary Port<br/>UseCase Interface]
            PP2[Primary Port<br/>Command Interface]
            SP1[Secondary Port<br/>Repository Interface]
            SP2[Secondary Port<br/>External Service Interface]
        end
        
        subgraph "Core Domain"
            BL[Business Logic<br/>Domain Services<br/>Entities<br/>Value Objects]
        end
    end
    
    UI --> PA1
    CLI --> PA2
    Test --> PA1
    
    PA1 --> PP1
    PA2 --> PP2
    PP1 --> BL
    PP2 --> BL
    
    BL --> SP1
    BL --> SP2
    SP1 --> SA1
    SP2 --> SA2
    
    SA1 --> DB
    SA2 --> API
    SA1 --> Queue
구조 및 아키텍처

구조 다이어그램:

graph LR
    subgraph "Outside World"
        subgraph "Left Side (Drivers)"
            W[Web UI]
            M[Mobile App]
            T[Tests]
            C[CLI]
        end
        
        subgraph "Right Side (Driven)"
            D[(Database)]
            E[External API]
            F[File System]
            Q[Message Queue]
        end
    end
    
    subgraph "Hexagon"
        subgraph "Primary Adapters"
            WA[Web Adapter]
            MA[Mobile Adapter]
            TA[Test Adapter]
            CA[CLI Adapter]
        end
        
        subgraph "Ports"
            PP[Primary Ports<br/>Inbound]
            SP[Secondary Ports<br/>Outbound]
        end
        
        subgraph "Application Core"
            AC[Application Services<br/>Use Cases<br/>Domain Logic<br/>Business Rules]
        end
        
        subgraph "Secondary Adapters"
            DA[Database Adapter]
            EA[API Adapter]
            FA[File Adapter]
            QA[Queue Adapter]
        end
    end
    
    W --> WA
    M --> MA
    T --> TA
    C --> CA
    
    WA --> PP
    MA --> PP
    TA --> PP
    CA --> PP
    
    PP --> AC
    AC --> SP
    
    SP --> DA
    SP --> EA
    SP --> FA
    SP --> QA
    
    DA --> D
    EA --> E
    FA --> F
    QA --> Q

필수 구성요소:

  1. 애플리케이션 코어 (Application Core)

    • 기능: 비즈니스 로직과 도메인 규칙 포함
    • 역할: 애플리케이션의 핵심 기능 수행
    • 특징: 외부 기술에 의존하지 않는 순수한 비즈니스 로직
  2. 포트 (Ports)

    • Primary Ports (Inbound Ports)

      • 기능: 외부에서 애플리케이션으로의 진입점 정의
      • 역할: 사용 사례 인터페이스 제공
      • 특징: 애플리케이션이 제공하는 서비스 명세
    • Secondary Ports (Outbound Ports)

      • 기능: 애플리케이션에서 외부 시스템으로의 진출점 정의
      • 역할: 외부 서비스 의존성 추상화
      • 특징: 애플리케이션이 필요로 하는 외부 서비스 계약
  3. 어댑터 (Adapters)

    • Primary Adapters (Driver Adapters)

      • 기능: 외부 시스템의 요청을 애플리케이션 포트로 변환
      • 역할: 사용자 인터페이스, API 엔드포인트 등 제공
      • 특징: 포트에 의존하며 외부 기술과 결합
    • Secondary Adapters (Driven Adapters)

      • 기능: 애플리케이션의 요청을 외부 시스템 호출로 변환
      • 역할: 데이터베이스 접근, 외부 API 호출 등 수행
      • 특징: 포트를 구현하며 구체적인 기술 사용

선택 구성요소:

  1. 컨피규레이터 (Configurator)
    • 기능: 의존성 주입을 통한 포트와 어댑터 연결
    • 역할: 애플리케이션 시작 시 구성 요소들을 조립
    • 특징: 보통 프레임워크나 DI 컨테이너가 담당
구현 기법

1. 의존성 주입 (Dependency Injection)

2. 인터페이스 분리 (Interface Segregation)

3. 어댑터 패턴 (Adapter Pattern)


4. 세 번째 부분: 장단점 및 활용

장점
구분항목설명
장점테스트 용이성포트 인터페이스를 통한 목 객체 사용으로 독립적인 단위 테스트 가능
장점기술적 유연성어댑터 교체만으로 다른 기술 스택 사용 가능
장점비즈니스 로직 보호도메인 계층의 격리로 비즈니스 규칙 안정성 확보
장점병렬 개발 지원포트 정의 후 팀별 독립적 개발 가능
장점유지보수성 향상명확한 책임 분리로 코드 이해도 및 수정 용이성 증대
장점확장성새로운 어댑터 추가로 기능 확장 시 기존 코드 영향 최소화
단점과 문제점 그리고 해결방안

단점

구분항목설명해결책
단점초기 복잡성 증가포트와 어댑터 계층 추가로 인한 구조 복잡성단계적 도입, 명확한 문서화, 팀 교육
단점학습 곡선새로운 패턴 적용을 위한 팀의 학습 시간 필요실습 위주 교육, 멘토링, 점진적 적용
단점과도한 추상화단순한 애플리케이션에 대한 불필요한 복잡성프로젝트 규모 및 복잡성 평가 후 적용 여부 결정
단점성능 오버헤드간접 호출 계층으로 인한 약간의 성능 저하성능 크리티컬 부분 식별 및 최적화

문제점

구분항목원인영향탐지 및 진단예방 방법해결 방법 및 기법
문제점매핑 복잡성도메인 모델과 외부 모델 간 변환개발 시간 증가, 버그 발생 위험코드 리뷰, 자동화 테스트명확한 매핑 규칙 정의매핑 라이브러리 사용, 자동 생성 도구
문제점디버깅 어려움간접 호출로 인한 호출 스택 추적 복잡성문제 해결 시간 증가로깅 강화, 트레이싱 도구적절한 로깅 전략 수립분산 트레이싱, 상세 로깅
문제점포트 설계 오류부적절한 인터페이스 설계잦은 인터페이스 변경, 결합도 증가인터페이스 안정성 분석도메인 이해 선행, 프로토타이핑리팩토링, 인터페이스 재설계
도전 과제

기술적 도전 과제:

  1. 마이크로서비스 환경에서의 적용

    • 원인: 분산 시스템의 복잡성과 네트워크 통신
    • 영향: 트랜잭션 관리, 데이터 일관성 문제
    • 해결 방법: 이벤트 소싱, CQRS 패턴, 분산 트레이싱
  2. 레거시 시스템 통합

    • 원인: 기존 시스템의 구조적 제약
    • 영향: 어댑터 복잡성 증가, 성능 저하
    • 해결 방법: 단계적 마이그레이션, Anti-Corruption Layer 패턴
  3. 실시간 성능 요구사항

    • 원인: 간접 호출로 인한 지연시간
    • 영향: 응답 시간 증가, 처리량 감소
    • 해결 방법: 성능 프로파일링, 캐싱 전략, 비동기 처리
분류 기준에 따른 종류 및 유형
분류 기준종류/유형설명
포트 방향성Primary Ports (Inbound)외부에서 애플리케이션으로의 진입점
포트 방향성Secondary Ports (Outbound)애플리케이션에서 외부로의 진출점
어댑터 유형Driver Adapters애플리케이션을 구동하는 외부 시스템용
어댑터 유형Driven Adapters애플리케이션이 구동하는 외부 시스템용
기술별 분류Web AdaptersHTTP, REST API, GraphQL 등
기술별 분류Database AdaptersRDBMS, NoSQL, 파일 시스템 등
기술별 분류Messaging Adapters메시지 큐, 이벤트 스트림 등
실무 사용 예시
구분사용 목적함께 사용되는 기술효과
E-commerce결제 시스템 독립성다양한 결제 게이트웨이 API결제 업체 변경 시 비즈니스 로직 무변경
금융 서비스규제 대응레거시 시스템, 신규 API규제 변경 시 빠른 대응, 시스템 안정성
콘텐츠 관리다중 채널 지원웹, 모바일, API채널별 독립적 개발, 일관된 비즈니스 로직
IoT 플랫폼디바이스 다양성다양한 통신 프로토콜새로운 디바이스 추가 시 유연성
활용 사례

사례: E-commerce 주문 관리 시스템

시스템 구성:

시스템 구성 다이어그램:

graph TB
    subgraph "External Systems"
        Customer[고객 웹사이트]
        Admin[관리자 대시보드]
        PaymentGW[결제 게이트웨이]
        ShippingAPI[배송 API]
        InventoryDB[(재고 DB)]
    end
    
    subgraph "Primary Adapters"
        WebAPI[Web API Controller]
        AdminWeb[Admin Web Controller]
    end
    
    subgraph "Application Core"
        OrderService[주문 서비스]
        InventoryService[재고 서비스]
        PricingService[가격 서비스]
    end
    
    subgraph "Secondary Adapters"
        PaymentAdapter[결제 어댑터]
        ShippingAdapter[배송 어댑터]
        DBAdapter[데이터베이스 어댑터]
    end
    
    Customer --> WebAPI
    Admin --> AdminWeb
    
    WebAPI --> OrderService
    AdminWeb --> OrderService
    OrderService --> InventoryService
    OrderService --> PricingService
    
    OrderService --> PaymentAdapter
    OrderService --> ShippingAdapter
    OrderService --> DBAdapter
    
    PaymentAdapter --> PaymentGW
    ShippingAdapter --> ShippingAPI
    DBAdapter --> InventoryDB

Workflow:

  1. 고객이 웹사이트에서 주문 요청
  2. Web API Controller 가 요청을 받아 주문 서비스 호출
  3. 주문 서비스가 재고 확인, 가격 계산 수행
  4. 주문 서비스가 결제 어댑터를 통해 결제 처리
  5. 결제 완료 후 배송 어댑터를 통해 배송 요청
  6. 데이터베이스 어댑터를 통해 주문 정보 저장
  7. 고객에게 주문 완료 응답 반환

기존 시스템과의 차이점:

구현 예시

Python 을 사용한 주문 관리 시스템 구현:

  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
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# Domain Layer - 비즈니스 로직
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime

@dataclass
class Order:
    """주문 엔티티"""
    order_id: str
    customer_id: str
    items: List[dict]
    total_amount: float
    status: str = "PENDING"
    created_at: datetime = None
    
    def __post_init__(self):
        if self.created_at is None:
            self.created_at = datetime.now()
    
    def calculate_total(self) -> float:
        """총 금액 계산"""
        return sum(item['price'] * item['quantity'] for item in self.items)
    
    def confirm_payment(self):
        """결제 확인"""
        self.status = "PAID"
    
    def ship_order(self):
        """배송 처리"""
        if self.status == "PAID":
            self.status = "SHIPPED"
        else:
            raise ValueError("결제되지 않은 주문은 배송할 수 없습니다")

# Secondary Ports (Outbound Ports)
class OrderRepository(ABC):
    """주문 저장소 포트"""
    
    @abstractmethod
    def save(self, order: Order) -> Order:
        pass
    
    @abstractmethod
    def find_by_id(self, order_id: str) -> Optional[Order]:
        pass

class PaymentService(ABC):
    """결제 서비스 포트"""
    
    @abstractmethod
    def process_payment(self, order_id: str, amount: float) -> bool:
        pass

class ShippingService(ABC):
    """배송 서비스 포트"""
    
    @abstractmethod
    def request_shipping(self, order: Order) -> str:
        pass

# Primary Ports (Inbound Ports)
class OrderUseCase(ABC):
    """주문 사용 사례 포트"""
    
    @abstractmethod
    def create_order(self, customer_id: str, items: List[dict]) -> Order:
        pass
    
    @abstractmethod
    def process_order(self, order_id: str) -> Order:
        pass

# Application Service - 도메인 로직 조율
class OrderService(OrderUseCase):
    """주문 서비스 - 사용 사례 구현"""
    
    def __init__(self, 
                 order_repository: OrderRepository,
                 payment_service: PaymentService,
                 shipping_service: ShippingService):
        self.order_repository = order_repository
        self.payment_service = payment_service
        self.shipping_service = shipping_service
    
    def create_order(self, customer_id: str, items: List[dict]) -> Order:
        """주문 생성"""
        import uuid
        
        order = Order(
            order_id=str(uuid.uuid4()),
            customer_id=customer_id,
            items=items,
            total_amount=0
        )
        order.total_amount = order.calculate_total()
        
        # 주문 저장
        saved_order = self.order_repository.save(order)
        return saved_order
    
    def process_order(self, order_id: str) -> Order:
        """주문 처리"""
        # 주문 조회
        order = self.order_repository.find_by_id(order_id)
        if not order:
            raise ValueError(f"주문을 찾을 수 없습니다: {order_id}")
        
        # 결제 처리
        payment_success = self.payment_service.process_payment(
            order.order_id, order.total_amount
        )
        
        if payment_success:
            order.confirm_payment()
            
            # 배송 요청
            shipping_id = self.shipping_service.request_shipping(order)
            order.ship_order()
            
            # 주문 상태 업데이트 저장
            self.order_repository.save(order)
        
        return order

# Secondary Adapters (Driven Adapters)
class MySQLOrderRepository(OrderRepository):
    """MySQL 주문 저장소 어댑터"""
    
    def __init__(self, connection_string: str):
        self.connection_string = connection_string
        # 실제 구현에서는 데이터베이스 연결 초기화
    
    def save(self, order: Order) -> Order:
        """주문 저장"""
        # 실제 구현에서는 SQL INSERT/UPDATE 수행
        print(f"MySQL에 주문 저장: {order.order_id}")
        return order
    
    def find_by_id(self, order_id: str) -> Optional[Order]:
        """주문 조회"""
        # 실제 구현에서는 SQL SELECT 수행
        print(f"MySQL에서 주문 조회: {order_id}")
        # 임시 데이터 반환
        return Order(
            order_id=order_id,
            customer_id="customer123",
            items=[{"name": "상품A", "price": 10000, "quantity": 2}],
            total_amount=20000
        )

class StripePaymentAdapter(PaymentService):
    """Stripe 결제 어댑터"""
    
    def __init__(self, api_key: str):
        self.api_key = api_key
    
    def process_payment(self, order_id: str, amount: float) -> bool:
        """Stripe를 통한 결제 처리"""
        print(f"Stripe 결제 처리: 주문 {order_id}, 금액 {amount}")
        # 실제 구현에서는 Stripe API 호출
        return True  # 성공 가정

class CourierShippingAdapter(ShippingService):
    """택배 배송 어댑터"""
    
    def __init__(self, api_endpoint: str):
        self.api_endpoint = api_endpoint
    
    def request_shipping(self, order: Order) -> str:
        """배송 요청"""
        print(f"택배 배송 요청: 주문 {order.order_id}")
        # 실제 구현에서는 택배사 API 호출
        return "SHIP123456"  # 배송 추적 번호

# Primary Adapters (Driver Adapters)
from flask import Flask, request, jsonify

class OrderWebController:
    """웹 API 컨트롤러 어댑터"""
    
    def __init__(self, order_service: OrderUseCase):
        self.order_service = order_service
        self.app = Flask(__name__)
        self._setup_routes()
    
    def _setup_routes(self):
        """라우트 설정"""
        
        @self.app.route('/orders', methods=['POST'])
        def create_order():
            """주문 생성 엔드포인트"""
            try:
                data = request.json
                order = self.order_service.create_order(
                    customer_id=data['customer_id'],
                    items=data['items']
                )
                return jsonify({
                    'order_id': order.order_id,
                    'status': order.status,
                    'total_amount': order.total_amount
                }), 201
            except Exception as e:
                return jsonify({'error': str(e)}), 400
        
        @self.app.route('/orders/<order_id>/process', methods=['POST'])
        def process_order(order_id):
            """주문 처리 엔드포인트"""
            try:
                order = self.order_service.process_order(order_id)
                return jsonify({
                    'order_id': order.order_id,
                    'status': order.status,
                    'total_amount': order.total_amount
                }), 200
            except Exception as e:
                return jsonify({'error': str(e)}), 400

# Configuration - 의존성 주입 설정
class ApplicationConfig:
    """애플리케이션 구성"""
    
    @staticmethod
    def create_order_service() -> OrderService:
        """주문 서비스 생성 및 의존성 주입"""
        # Secondary Adapters 생성
        order_repository = MySQLOrderRepository("mysql://localhost/orders")
        payment_service = StripePaymentAdapter("sk_test_123456")
        shipping_service = CourierShippingAdapter("https://api.courier.com")
        
        # 주문 서비스 생성
        return OrderService(
            order_repository=order_repository,
            payment_service=payment_service,
            shipping_service=shipping_service
        )
    
    @staticmethod
    def create_web_app() -> Flask:
        """웹 애플리케이션 생성"""
        order_service = ApplicationConfig.create_order_service()
        controller = OrderWebController(order_service)
        return controller.app

# 애플리케이션 실행
if __name__ == "__main__":
    app = ApplicationConfig.create_web_app()
    app.run(debug=True)

# 테스트 예시
import unittest
from unittest.mock import Mock

class TestOrderService(unittest.TestCase):
    """주문 서비스 테스트"""
    
    def setUp(self):
        """테스트 설정"""
        self.mock_repository = Mock(spec=OrderRepository)
        self.mock_payment = Mock(spec=PaymentService)
        self.mock_shipping = Mock(spec=ShippingService)
        
        self.order_service = OrderService(
            self.mock_repository,
            self.mock_payment,
            self.mock_shipping
        )
    
    def test_create_order(self):
        """주문 생성 테스트"""
        # Given
        items = [{"name": "상품A", "price": 10000, "quantity": 2}]
        self.mock_repository.save.return_value = Mock()
        
        # When
        order = self.order_service.create_order("customer123", items)
        
        # Then
        self.assertEqual(order.total_amount, 20000)
        self.mock_repository.save.assert_called_once()
    
    def test_process_order_success(self):
        """주문 처리 성공 테스트"""
        # Given
        order = Order("order123", "customer123", [], 20000)
        self.mock_repository.find_by_id.return_value = order
        self.mock_payment.process_payment.return_value = True
        self.mock_shipping.request_shipping.return_value = "SHIP123"
        
        # When
        result = self.order_service.process_order("order123")
        
        # Then
        self.assertEqual(result.status, "SHIPPED")
        self.mock_payment.process_payment.assert_called_once()
        self.mock_shipping.request_shipping.assert_called_once()

4. 네 번째 부분: 최적화 및 고려사항

실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점
구분고려사항주의할 점권장사항
프로젝트 규모복잡한 비즈니스 로직이 있는 중대형 프로젝트단순한 CRUD 애플리케이션에는 과도한 복잡성프로젝트 복잡도 평가 후 적용 결정
팀 역량팀의 아키텍처 패턴 이해도와 경험학습 곡선으로 인한 초기 생산성 저하점진적 도입과 지속적인 교육
포트 설계안정적이고 명확한 인터페이스 정의잦은 인터페이스 변경으로 인한 불안정성도메인 모델링 선행, 프로토타이핑
테스트 전략포트별 독립적인 테스트 케이스 작성통합 테스트 복잡성 증가테스트 피라미드 전략 수립
성능 최적화간접 호출로 인한 성능 오버헤드과도한 추상화로 인한 성능 저하성능 크리티컬 경로 식별 및 최적화
최적화하기 위한 고려사항 및 주의할 점
구분최적화 방법주의사항권장사항
포트 설계응집도 높은 인터페이스 설계너무 세분화된 포트로 인한 복잡성단일 책임 원칙 기반 포트 설계
어댑터 관리어댑터별 독립적인 라이프사이클 관리어댑터 간 의존성 발생컨테이너 기반 의존성 관리
매핑 최적화도메인 -DTO 간 효율적인 매핑과도한 매핑 로직으로 인한 성능 저하매핑 라이브러리 활용, 자동화
캐싱 전략포트 레벨에서의 캐싱 적용캐시 무효화 복잡성 증가적절한 캐시 범위 설정
모니터링포트별 성능 및 에러 모니터링분산된 로깅으로 인한 추적 어려움통합 모니터링 시스템 구축
주제와 관련하여 주목할 내용
카테고리주제항목설명
관련 패턴Clean Architecture동심원 구조헥사고날 아키텍처를 확장한 계층형 구조
관련 패턴Onion Architecture계층 분리의존성 역전을 통한 도메인 중심 설계
관련 패턴Domain-Driven Design도메인 모델링비즈니스 도메인 중심의 소프트웨어 설계
구현 기술Dependency Injection의존성 주입느슨한 결합을 위한 핵심 기술
구현 기술Interface Segregation인터페이스 분리클라이언트별 특화된 인터페이스 제공
테스트 전략Test Doubles목 객체외부 의존성 격리를 위한 테스트 기법
마이크로서비스Anti-Corruption Layer부패 방지 계층레거시 시스템과의 통합 패턴
마이크로서비스Event-Driven Architecture이벤트 기반 통신비동기 메시징을 통한 서비스 간 통신
주제와 관련하여 반드시 학습해야할 내용
카테고리주제항목설명
설계 원칙SOLID 원칙객체지향 설계의존성 역전, 단일 책임 등 핵심 원칙
설계 패턴Adapter Pattern구조 패턴호환되지 않는 인터페이스 연결
설계 패턴Strategy Pattern행위 패턴알고리즘 캡슐화 및 교체 가능성
아키텍처Layered Architecture계층형 구조전통적 아키텍처와의 차이점 이해
구현 기술Dependency InjectionIoC 컨테이너Spring, Guice 등 DI 프레임워크
테스트 기법Unit Testing단위 테스트Mock, Stub 을 활용한 독립적 테스트
도메인 모델링Domain-Driven Design전술적 설계Entity, Value Object, Aggregate
마이크로서비스Service Decomposition서비스 분할비즈니스 역량 기반 서비스 경계 설정
기타 사항

최신 트렌드와 발전 방향:

  1. 클라우드 네이티브 환경

    • 컨테이너화를 통한 어댑터 독립적 배포
    • 서비스 메시와의 통합
    • FaaS (Function as a Service) 환경에서의 적용
  2. 마이크로서비스 아키텍처와의 융합

    • 서비스별 헥사고날 아키텍처 적용
    • API 게이트웨이를 통한 포트 표준화
    • 이벤트 소싱과의 결합
  3. AI/ML 시스템과의 통합

    • 머신러닝 모델을 어댑터로 활용
    • 데이터 파이프라인과의 연동
    • 실시간 추론 시스템 구축

도구 및 프레임워크 지원:

성공적인 도입을 위한 체크리스트:

  1. 명확한 도메인 모델 정의
  2. 안정적인 포트 인터페이스 설계
  3. 효율적인 어댑터 구현
  4. 포괄적인 테스트 전략
  5. 지속적인 리팩토링 및 개선

용어 정리

카테고리용어설명
핵심 개념Port (포트)애플리케이션과 외부 세계 간의 통신 인터페이스
핵심 개념Adapter (어댑터)포트의 구체적인 구현체로 특정 기술과 연결
핵심 개념Hexagon (헥사곤)비즈니스 로직이 포함된 애플리케이션 핵심 부분
포트 유형Primary Port외부에서 애플리케이션으로의 진입점 인터페이스
포트 유형Secondary Port애플리케이션에서 외부 시스템으로의 진출점 인터페이스
어댑터 유형Driver Adapter애플리케이션을 구동하는 외부 시스템용 어댑터
어댑터 유형Driven Adapter애플리케이션이 구동하는 외부 시스템용 어댑터
설계 원칙Dependency Inversion고수준 모듈이 저수준 모듈에 의존하지 않는 원칙
설계 원칙Separation of Concerns관심사의 분리를 통한 모듈화 원칙
구현 기법Dependency Injection객체의 의존성을 외부에서 주입하는 기법
테스트 기법Test Double테스트 목적으로 실제 객체를 대체하는 객체
테스트 기법Mock Object호출을 검증하기 위한 테스트 더블

참고 및 출처


헥사고날 아키텍처 (Hexagonal Architecture) 는 소프트웨어 설계 패턴 중 하나로, 애플리케이션의 핵심 비즈니스 로직을 외부 요소로부터 분리하여 유연하고 유지보수가 용이한 시스템을 구축하는 것을 목표로 한다.
이 아키텍처는 Alistair Cockburn 에 의해 제안되었으며, ’ 포트와 어댑터 아키텍처 (Ports and Adapters Architecture)’ 라고도 불린다.

핵심 원칙
  1. 의존성 방향:
    모든 의존성은 도메인 계층을 향해 안쪽으로 흐른다.
    도메인 계층은 외부를 전혀 알지 못하며, 포트를 통해서만 통신한다.
  2. 관심사의 분리:
    각 계층은 명확한 책임을 가지며, 다른 계층의 구현 세부사항을 알지 못한다.
    이는 시스템의 유지보수성과 테스트 용이성을 향상시킨다.
  3. 인터페이스 기반 설계:
    포트는 인터페이스로 정의되어, 구체적인 구현체 (어댑터) 를 쉽게 교체할 수 있게 한다.
주요 구성 요소

https://www.linkedin.com/pulse/whats-hexagonal-architecture-luis-soares-m-sc-/

헥사고날 아키텍처는 크게 세 가지 주요 구성 요소로 이루어져 있다:

  1. 핵심 비즈니스 로직 (도메인): 애플리케이션의 중심에 위치하며, 순수한 비즈니스 로직을 포함한다.

  2. 포트 (Ports): 애플리케이션의 내부와 외부를 연결하는 인터페이스 역할을 한다. 두 가지 유형이 있다:

    • 인바운드 포트: 외부에서 애플리케이션을 사용하기 위한 API
    • 아웃바운드 포트: 애플리케이션이 외부 시스템을 사용하기 위한 API
  3. 어댑터 (Adapters): 포트를 구현하여 실제로 외부 시스템과 통신하는 역할을 한다. 두 가지 유형이 있다:

    • Primary (Driving) Adapters: 애플리케이션을 구동하는 역할 (예: UI)
    • Secondary (Driven) Adapters: 애플리케이션에 의해 구동되는 역할 (예: 데이터베이스)
작동 방식
  1. 외부 요청이 Primary Adapter 를 통해 들어온다.
  2. Primary Adapter 는 인바운드 포트를 통해 핵심 비즈니스 로직과 통신한다.
  3. 비즈니스 로직은 필요한 경우 아웃바운드 포트를 통해 Secondary Adapter 와 통신한다.
  4. Secondary Adapter 는 외부 시스템 (예: 데이터베이스) 과 상호작용한다.
장점
  1. 유연성: 외부 시스템이나 인프라와의 의존성이 낮아 구성요소를 쉽게 교체하거나 업데이트할 수 있다.
  2. 테스트 용이성: 비즈니스 로직을 독립적으로 테스트할 수 있어 테스트가 더 안정적이고 쉬워진다.
  3. 유지보수성: 책임이 명확히 분리되어 있어 코드의 이해와 수정이 용이하다.
  4. 확장성: 새로운 어댑터를 추가하거나 기존 어댑터를 수정하여 시스템을 쉽게 확장할 수 있다.
단점
  1. 구현 복잡성: 포트와 어댑터를 구성하고 관리하는 데 복잡성이 증가할 수 있다.
  2. 초기 개발 시간 증가: 아키텍처를 설계하고 구현하는 데 초기에 더 많은 시간이 소요될 수 있다.
구현 예시
  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
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional
from uuid import UUID, uuid4

# Domain(핵심 비즈니스 로직)
@dataclass
class Product:
    id: UUID
    name: str
    price: float
    stock: int

    def decrease_stock(self, quantity: int) -> None:
        if self.stock < quantity:
            raise ValueError("Insufficient stock")
        self.stock -= quantity

# Ports(포트) - 내부에서 외부로 향하는 인터페이스
class ProductRepository(ABC):
    @abstractmethod
    def save(self, product: Product) -> None:
        pass

    @abstractmethod
    def find_by_id(self, product_id: UUID) -> Optional[Product]:
        pass

    @abstractmethod
    def find_all(self) -> List[Product]:
        pass

class ProductNotificationService(ABC):
    @abstractmethod
    def notify_low_stock(self, product: Product) -> None:
        pass

# Application Service(응용 서비스)
class ProductService:
    def __init__(
        self, 
        product_repository: ProductRepository,
        notification_service: ProductNotificationService
    ):
        self.product_repository = product_repository
        self.notification_service = notification_service

    def create_product(self, name: str, price: float, stock: int) -> UUID:
        product = Product(id=uuid4(), name=name, price=price, stock=stock)
        self.product_repository.save(product)
        return product.id

    def update_stock(self, product_id: UUID, quantity: int) -> None:
        product = self.product_repository.find_by_id(product_id)
        if not product:
            raise ValueError("Product not found")

        product.decrease_stock(quantity)
        
        # 재고가 10개 미만이면 알림 발송
        if product.stock < 10:
            self.notification_service.notify_low_stock(product)
            
        self.product_repository.save(product)

# Adapters(어댑터) - 외부 시스템과의 연동
class PostgresProductRepository(ProductRepository):
    def __init__(self, db_connection):
        self.db = db_connection

    def save(self, product: Product) -> None:
        # PostgreSQL 데이터베이스에 제품 저장
        query = """
            INSERT INTO products (id, name, price, stock)
            VALUES (%s, %s, %s, %s)
            ON CONFLICT (id) DO UPDATE
            SET name = excluded.name,
                price = excluded.price,
                stock = excluded.stock
        """
        self.db.execute(query, (
            str(product.id),
            product.name,
            product.price,
            product.stock
        ))

    def find_by_id(self, product_id: UUID) -> Optional[Product]:
        # PostgreSQL 데이터베이스에서 제품 조회
        query = "SELECT id, name, price, stock FROM products WHERE id = %s"
        result = self.db.execute(query, (str(product_id),)).fetchone()
        
        if result:
            return Product(
                id=UUID(result[0]),
                name=result[1],
                price=result[2],
                stock=result[3]
            )
        return None

    def find_all(self) -> List[Product]:
        # 모든 제품 조회
        query = "SELECT id, name, price, stock FROM products"
        results = self.db.execute(query).fetchall()
        
        return [
            Product(
                id=UUID(row[0]),
                name=row[1],
                price=row[2],
                stock=row[3]
            )
            for row in results
        ]

class EmailNotificationService(ProductNotificationService):
    def __init__(self, email_client):
        self.email_client = email_client

    def notify_low_stock(self, product: Product) -> None:
        message = f"Low stock alert: {product.name} has only {product.stock} items remaining"
        self.email_client.send_email(
            to="warehouse@example.com",
            subject="Low Stock Alert",
            body=message
        )

# REST API Adapter
from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.post("/products")
async def create_product(name: str, price: float, stock: int):
    try:
        product_id = product_service.create_product(name, price, stock)
        return {"product_id": product_id}
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))

@app.put("/products/{product_id}/stock")
async def update_product_stock(product_id: UUID, quantity: int):
    try:
        product_service.update_stock(product_id, quantity)
        return {"message": "Stock updated successfully"}
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))

용어 정리

용어설명

참고 및 출처