Communitygithub.com

Hexagonal Architecture

Ports & Adapters 시스템을 설계, 구현, 리팩토링하여 명확한 도메인 경계, 의존성 역전, 테스트 가능한 유스케이스 오케스트레이션을 제공합니다. TypeScript, Java, Kotlin, Go 서비스에서 도입하거나 리팩토링 시 사용하세요.

Hexagonal Architecture란 무엇인가요?

이 스킬은 Hexagonal Architecture (Ports and Adapters)를 실제 프로젝트에 도입하거나 기존 코드베이스를 리팩토링할 때 필요한 완전한 가이드를 제공합니다. 핵심은 도메인 로직이 프레임워크, 전송 계층, 데이터베이스와 같은 외부 요소로부터 완전히 독립되도록 하는 것입니다. 스킬은 유스케이스 경계 정의, 아웃바운드 포트 식별, 유스케이스 구현, 어댑터 작성, 컴포지션 루트에서 의존성 주입을 수행하는 6단계 프로세스를 제시합니다. TypeScript 예제뿐 아니라 Java, Kotlin, Go 언어별 모듈 구조와 패턴을 제공하며, 마이그레이션 전략(Strangler 접근법)과 각 경계별 테스트 방법(단위, 통합, E2E)을 상세히 다룹니다. ORM 모델 임포트, 서비스 로케이터 패턴 등 일반적인 안티패턴을 피하는 방법도 포함되어 있습니다. Cursor, Claude Code, Codex, Windsurf 등 다양한 AI 에이전트에서 어시스턴트 컨텍스트로 사용하기에 적합합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/hexagonal-architecture

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Hexagonal Architecture (육각형 아키텍처)

Hexagonal architecture (Ports and Adapters)는 비즈니스 로직을 프레임워크, 전송 계층, 영속성 세부 사항으로부터 독립시킵니다. 핵심 애플리케이션은 추상적인 포트(ports)에 의존하고, 어댑터(adapters)는 가장자리에서 이러한 포트를 구현합니다.

사용 시점

  • 장기적인 유지보수성과 테스트 가능성이 중요한 새 기능을 구축할 때
  • 도메인 로직이 입출력 문제와 혼재된 레이어드 아키텍처나 프레임워크 중심 코드를 리팩토링할 때
  • 동일한 유스케이스에 대해 여러 인터페이스(HTTP, CLI, 큐 워커, 크론 잡)를 지원해야 할 때
  • 비즈니스 규칙을 다시 작성하지 않고 인프라(데이터베이스, 외부 API, 메시지 버스)를 교체해야 할 때

요청에 경계, 도메인 중심 설계, 강하게 결합된 서비스 리팩토링, 또는 특정 라이브러리로부터 애플리케이션 로직 분리가 포함될 때 이 스킬을 사용하세요.

핵심 개념

  • 도메인 모델: 비즈니스 규칙과 엔티티/값 객체. 프레임워크 임포트가 없습니다.
  • 유스케이스 (애플리케이션 계층): 도메인 동작과 워크플로우 단계를 조율합니다.
  • 인바운드 포트: 애플리케이션이 수행할 수 있는 작업을 설명하는 계약(커맨드/쿼리/유스케이스 인터페이스).
  • 아웃바운드 포트: 애플리케이션이 필요로 하는 의존성에 대한 계약(리포지토리, 게이트웨이, 이벤트 발행자, 시계, UUID 등).
  • 어댑터: 포트의 인프라 및 전달 구현체(HTTP 컨트롤러, DB 리포지토리, 큐 컨슈머, SDK 래퍼).
  • 컴포지션 루트: 구체적인 어댑터가 유스케이스에 바인딩되는 단일 와이어링 위치.

아웃바운드 포트 인터페이스는 일반적으로 애플리케이션 계층에 위치하지만(추상화가 진정으로 도메인 수준인 경우에만 도메인에 위치), 인프라 어댑터가 이를 구현합니다.

의존성 방향은 항상 안쪽을 향합니다:

  • 어댑터 -> 애플리케이션/도메인
  • 애플리케이션 -> 포트 인터페이스(인바운드/아웃바운드 계약)
  • 도메인 -> 도메인 전용 추상화(프레임워크나 인프라 의존성 없음)
  • 도메인 -> 외부 의존성 없음

작동 방식

1단계: 유스케이스 경계 모델링

명확한 입력 및 출력 DTO를 가진 단일 유스케이스를 정의합니다. 전송 계층 세부 사항(Express req, GraphQL context, 잡 페이로드 래퍼)은 이 경계 밖에 유지합니다.

2단계: 아웃바운드 포트 먼저 정의

모든 부수 효과를 포트로 식별합니다:

  • 영속성 (UserRepositoryPort)
  • 외부 호출 (BillingGatewayPort)
  • 횡단 관심사 (LoggerPort, ClockPort)

포트는 기술이 아닌 기능을 모델링해야 합니다.

3단계: 순수한 오케스트레이션으로 유스케이스 구현

유스케이스 클래스/함수는 생성자/인수를 통해 포트를 전달받습니다. 애플리케이션 수준의 불변식을 검증하고, 도메인 규칙을 조정하며, 일반 데이터 구조를 반환합니다.

4단계: 가장자리에 어댑터 구축

  • 인바운드 어댑터: 프로토콜 입력을 유스케이스 입력으로 변환합니다.
  • 아웃바운드 어댑터: 애플리케이션 계약을 구체적인 API/ORM/쿼리 빌더에 매핑합니다.
  • 매핑은 어댑터 내에 유지되고, 유스케이스 내부에는 없습니다.

5단계: 컴포지션 루트에서 모든 것을 와이어링

어댑터를 인스턴스화한 다음 유스케이스에 주입합니다. 숨겨진 서비스 로케이터 동작을 방지하려면 이 와이어링을 중앙 집중화하세요.

6단계: 경계별로 테스트

  • 유스케이스 단위 테스트는 가짜 포트를 사용합니다.
  • 어댑터 통합 테스트는 실제 인프라 의존성으로 수행합니다.
  • 인바운드 어댑터를 통한 사용자 중심 흐름의 E2E 테스트.

아키텍처 다이어그램

flowchart LR
  Client["클라이언트 (HTTP/CLI/Worker)"] --> InboundAdapter["인바운드 어댑터"]
  InboundAdapter -->|"호출"| UseCase["유스케이스 (애플리케이션 계층)"]
  UseCase -->|"사용"| OutboundPort["아웃바운드 포트 (인터페이스)"]
  OutboundAdapter["아웃바운드 어댑터"] -->|"구현"| OutboundPort
  OutboundAdapter --> ExternalSystem["DB/API/Queue"]
  UseCase --> DomainModel["도메인 모델"]

제안되는 모듈 구조

명시적인 경계를 가진 기능 우선 구성 사용:

src/
  features/
    orders/
      domain/
        Order.ts
        OrderPolicy.ts
      application/
        ports/
          inbound/
            CreateOrder.ts
          outbound/
            OrderRepositoryPort.ts
            PaymentGatewayPort.ts
        use-cases/
          CreateOrderUseCase.ts
      adapters/
        inbound/
          http/
            createOrderRoute.ts
        outbound/
          postgres/
            PostgresOrderRepository.ts
          stripe/
            StripePaymentGateway.ts
      composition/
        ordersContainer.ts

TypeScript 예제

포트 정의

export interface OrderRepositoryPort {
  save(order: Order): Promise<void>;
  findById(orderId: string): Promise<Order | null>;
}

export interface PaymentGatewayPort {
  authorize(input: { orderId: string; amountCents: number }): Promise<{ authorizationId: string }>;
}

유스케이스

type CreateOrderInput = {
  orderId: string;
  amountCents: number;
};

type CreateOrderOutput = {
  orderId: string;
  authorizationId: string;
};

export class CreateOrderUseCase {
  constructor(
    private readonly orderRepository: OrderRepositoryPort,
    private readonly paymentGateway: PaymentGatewayPort
  ) {}

  async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
    const order = Order.create({ id: input.orderId, amountCents: input.amountCents });

    const auth = await this.paymentGateway.authorize({
      orderId: order.id,
      amountCents: order.amountCents,
    });

    // markAuthorized는 새로운 Order 인스턴스를 반환합니다. 제자리에서 변경하지 않습니다.
    const authorizedOrder = order.markAuthorized(auth.authorizationId);
    await this.orderRepository.save(authorizedOrder);

    return {
      orderId: order.id,
      authorizationId: auth.authorizationId,
    };
  }
}

아웃바운드 어댑터

export class PostgresOrderRepository implements OrderRepositoryPort {
  constructor(private readonly db: SqlClient) {}

  async save(order: Order): Promise<void> {
    await this.db.query(
      "insert into orders (id, amount_cents, status, authorization_id) values ($1, $2, $3, $4)",
      [order.id, order.amountCents, order.status, order.authorizationId]
    );
  }

  async findById(orderId: string): Promise<Order | null> {
    const row = await this.db.oneOrNone("select * from orders where id = $1", [orderId]);
    return row ? Order.rehydrate(row) : null;
  }
}

컴포지션 루트

export const buildCreateOrderUseCase = (deps: { db: SqlClient; stripe: StripeClient }) => {
  const orderRepository = new PostgresOrderRepository(deps.db);
  const paymentGateway = new StripePaymentGateway(deps.stripe);

  return new CreateOrderUseCase(orderRepository, paymentGateway);
};

다중 언어 매핑

에코시스템 전반에 걸쳐 동일한 경계 규칙을 사용합니다. 구문과 와이어링 스타일만 변경됩니다.

  • TypeScript/JavaScript
    • 포트: application/ports/* 인터페이스/타입.
    • 유스케이스: 생성자/인수 주입을 사용하는 클래스/함수.
    • 어댑터: adapters/inbound/*, adapters/outbound/*.
    • 컴포지션: 명시적인 팩토리/컨테이너 모듈 (숨겨진 전역 변수 없음).
  • Java
    • 패키지: domain, application.port.in, application.port.out, application.usecase, adapter.in, adapter.out.
    • 포트: application.port.*의 인터페이스.
    • 유스케이스: 일반 클래스 (Spring @Service는 선택 사항이며 필수가 아님).
    • 컴포지션: Spring 설정 또는 수동 와이어링 클래스; 와이어링을 domain/usecase 클래스 밖에 유지.
  • Kotlin
    • 모듈/패키지는 Java 분할을 따름 (domain, application.port, application.usecase, adapter).
    • 포트: Kotlin 인터페이스.
    • 유스케이스: 생성자 주입을 사용하는 클래스 (Koin/Dagger/Spring/수동).
    • 컴포지션: 모듈 정의 또는 전용 컴포지션 함수; 서비스 로케이터 패턴 피하기.
  • Go
    • 패키지: internal/<feature>/domain, application, ports, adapters/inbound, adapters/outbound.
    • 포트: 소비 애플리케이션 패키지가 소유한 작은 인터페이스.
    • 유스케이스: 인터페이스 필드와 명시적인 New... 생성자를 가진 구조체.
    • 컴포지션: cmd/<app>/main.go (또는 전용 와이어링 패키지)에서 와이어링, 생성자를 명시적으로 유지.

피해야 할 안티패턴

  • 도메인 엔티티가 ORM 모델, 웹 프레임워크 타입, SDK 클라이언트를 임포트하는 것.
  • 유스케이스가 req, res, 큐 메타데이터에서 직접 읽는 것.
  • 도메인/애플리케이션 매핑 없이 유스케이스에서 데이터베이스 행을 직접 반환하는 것.
  • 유스케이스 포트를 통하지 않고 어댑터가 서로 직접 호출하게 하는 것.
  • 숨겨진 전역 싱글톤으로 여러 파일에 의존성 와이어링을 분산시키는 것.

마이그레이션 플레이북

  1. 변경이 잦은 하나의 수직 슬라이스(단일 엔드포인트/잡)를 선택합니다.
  2. 명시적인 입력/출력 타입으로 유스케이스 경계를 추출합니다.
  3. 기존 인프라 호출 주변에 아웃바운드 포트를 도입합니다.
  4. 컨트롤러/서비스의 오케스트레이션 로직을 유스케이스로 이동합니다.
  5. 기존 어댑터를 유지하되, 새 유스케이스에 위임하도록 만듭니다.
  6. 새 경계 주변에 테스트를 추가합니다 (단위 + 어댑터 통합).
  7. 슬라이스별로 반복합니다. 전체 재작성은 피합니다.

기존 시스템 리팩토링

  • Strangler 접근법: 현재 엔드포인트를 유지하고, 한 번에 하나의 유스케이스를 새 포트/어댑터를 통해 라우팅합니다.
  • 빅뱅 재작성 금지: 기능 슬라이스별로 마이그레이션하고 특성화 테스트로 동작을 보존합니다.
  • 퍼사드 우선: 내부를 교체하기 전에 레거시 서비스를 아웃바운드 포트 뒤에 래핑합니다.
  • 컴포지션 동결: 새 의존성이 도메인/유스케이스 계층으로 누출되지 않도록 초기에 와이어링을 중앙 집중화합니다.
  • 슬라이스 선택 규칙: 변경이 잦고 영향 범위가 좁은 흐름을 먼저 우선시합니다.
  • 롤백 경로: 프로덕션 동작이 확인될 때까지 마이그레이션된 슬라이스별로 되돌릴 수 있는 토글 또는 경로 스위치를 유지합니다.

테스트 가이드 (동일한 헥사고날 경계)

  • 도메인 테스트: 순수한 비즈니스 규칙으로 엔티티/값 객체를 테스트합니다 (목 없음, 프레임워크 설정 없음).
  • 유스케이스 단위 테스트: 아웃바운드 포트에 대한 페이크/스텁으로 오케스트레이션을 테스트합니다. 비즈니스 결과와 포트 상호 작용을 단언합니다.
  • 아웃바운드 어댑터 계약 테스트: 포트 수준에서 공유 계약 스위트를 정의하고 각 어댑터 구현에 대해 실행합니다.
  • 인바운드 어댑터 테스트: 프로토콜 매핑(HTTP/CLI/큐 페이로드를 유스케이스 입력으로, 출력/에러 매핑을 프로토콜로 다시)을 검증합니다.
  • 어댑터 통합 테스트: 직렬화, 스키마/쿼리 동작, 재시도, 타임아웃에 대해 실제 인프라(DB/API/큐)에서 실행합니다.
  • 엔드 투 엔드 테스트: 인바운드 어댑터 -> 유스케이스 -> 아웃바운드 어댑터를 통한 중요한 사용자 여정을 다룹니다.
  • 리팩토링 안전성: 추출 전에 특성화 테스트를 추가합니다. 새 경계 동작이 안정적이고 동등해질 때까지 유지합니다.

모범 사례 체크리스트

  • 도메인 및 유스케이스 계층은 내부 타입과 포트만 임포트합니다.
  • 모든 외부 의존성은 아웃바운드 포트로 표현됩니다.
  • 검증은 경계에서 수행됩니다 (인바운드 어댑터 + 유스케이스 불변식).
  • 불변 변환을 사용합니다 (공유 상태를 변경하는 대신 새 값/엔티티를 반환).
  • 오류는 경계 간에 변환됩니다 (인프라 오류 -> 애플리케이션/도메인 오류).
  • 컴포지션 루트는 명시적이고 감사하기 쉽습니다.
  • 유스케이스는 포트에 대한 간단한 인메모리 페이크로 테스트 가능합니다.
  • 리팩토링은 동작을 보존하는 테스트를 사용하여 하나의 수직 슬라이스에서 시작합니다.
  • 언어/프레임워크별 세부 사항은 어댑터에 유지되고, 도메인 규칙에는 절대 포함되지 않습니다.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬