Communitygithub.com

육각형 아키텍처

도메인 경계, 의존성 역전, 테스트 가능한 유스케이스 오케스트레이션을 통해 TypeScript, Java, Kotlin, Go 서비스에서 포트와 어댑터 시스템을 설계, 구현, 리팩토링합니다. 포트와 어댑터를 도입하거나 리팩토링할 때, 또는 도메인 로직이 I/O와 뒤섞였을 때 사용하세요.

육각형 아키텍처란 무엇인가요?

이 스킬은 육각형 아키텍처(포트와 어댑터)를 설계, 구현, 리팩토링하는 방법을 안내합니다. 비즈니스 로직을 프레임워크, 전송, 영속성 세부 사항으로부터 분리하여 유지보수성과 테스트 용이성을 극대화하는 것이 핵심입니다. TypeScript, Java, Kotlin, Go 등 다양한 언어를 지원하며, 각 언어에 맞는 패키지 구조와 구현 방식을 제시합니다. 핵심 개념으로 도메인 모델, 유스케이스, 인바운드/아웃바운드 포트, 어댑터, 컴포지션 루트를 명확히 정의하고, 의존성 방향은 항상 내부(도메인/애플리케이션)를 향하도록 규정합니다. 마이그레이션 플레이북과 리팩토링 전략(Strangler 패턴, 슬라이스 단위 마이그레이션)을 제공하며, 테스트는 각 경계(단위, 통합, E2E)에 맞게 수행하도록 권장합니다. 안티패턴(도메인 엔티티의 ORM/웹 프레임워크 임포트, 유스케이스의 직접적인 요청/응답 처리 등)을 명시하여 실수를 방지하고, 최고의 실천법 체크리스트를 통해 올바른 아키텍처 적용을 돕습니다.

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

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

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

문서

육각형 아키텍처은(는) 무엇을 하나요?

육각형 아키텍처(포트와 어댑터)는 비즈니스 로직을 프레임워크, 전송, 영속성 세부 사항으로부터 독립적으로 유지합니다. 핵심 앱은 추상적인 포트에 의존하고, 어댑터는 경계에서 이러한 포트를 구현합니다.

사용 시기

  • 장기적인 유지보수성과 테스트 용이성이 중요한 새 기능을 구축할 때.
  • 도메인 로직이 I/O 문제와 혼합된 계층형 또는 프레임워크 중심 코드를 리팩토링할 때.
  • 동일한 유스케이스에 대해 여러 인터페이스(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/워커)"] --> InboundAdapter["인바운드 어댑터"]
  InboundAdapter -->|"호출"| UseCase["유스케이스 (애플리케이션 계층)"]
  UseCase -->|"사용"| OutboundPort["아웃바운드 포트 (인터페이스)"]
  OutboundAdapter["아웃바운드 어댑터"] -->|"구현"| OutboundPort
  OutboundAdapter --> ExternalSystem["DB/API/큐"]
  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 설정 또는 수동 와이어링 클래스; 와이어링을 도메인/유스케이스 클래스 외부에 유지.
  • 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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬