hexagonal-architecture
Ports & Adapters 시스템을 설계·구현·리팩터링한다. 명확한 도메인 경계, 의존성 역전, 테스트 가능한 유스케이스 오케스트레이션을 TypeScript·Java·Kotlin·Go 서비스 전반에 적용한다.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Ports & Adapters 시스템을 설계·구현·리팩터링한다. 명확한 도메인 경계, 의존성 역전, 테스트 가능한 유스케이스 오케스트레이션을 TypeScript·Java·Kotlin·Go 서비스 전반에 적용한다.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
새 Spring Boot 서비스를 api-gateway + auth-api 에코시스템에 연결한다. auth-api 클라이언트 등록 → Gateway 라우팅 추가 → 서비스에 econo-passport 연동 → 동작 확인까지 한 번에 처리. 다음 상황에서 반드시 이 스킬을 사용한다: - "새 서비스 Gateway에 연결해줘", "새 서비스 auth 연동" - "서비스 등록해줘", "Gateway 뒤에 붙여줘" - "/register-service" 직접 호출 - 새 Spring Boot 서비스가 추가되고 인증이 필요할 때 ARGUMENTS: 서비스명, 서비스 경로(선택), 업스트림 URL(선택) 예: "EEOS-BE /Users/mando/study/eeos/EEOS-BE/eeos"
이 스킬은 사용자가 "PR 생성해줘", "PR 만들어줘", "pull request 생성", "pr 올려줘", "/git-pr" 등을 요청할 때 호출된다. 현재 브랜치의 커밋을 원격으로 push하고, `.github/PULL_REQUEST_TEMPLATE.md`를 채워 GitHub Pull Request를 생성한다.
신규 기능을 개발할 때 정책 문서 → 코드 → 테스트 순서로 작성한다. ADR은 기술적 결정에만 사용하고, 기능 정책은 docs/features/ 에 작성한다. 다음 상황에서 반드시 이 스킬을 사용한다: - "기능 추가해줘", "기획부터 해봐", "설계해봐" - 새로운 API 엔드포인트 또는 도메인 규칙이 생길 때 - "/new-feature" 직접 호출 ARGUMENTS: 기능 이름 또는 요구사항 (없으면 대화에서 추출)
Architecture Decision Record(ADR)를 작성한다. 기술적 결정사항, 설계 선택, 트레이드오프를 문서화하여 나중에 "왜 이렇게 했지?"를 알 수 있게 한다. 다음 상황에서 반드시 이 스킬을 사용한다: - "ADR 써줘", "결정사항 문서화해줘", "이 결정 기록해줘" - 기술 방향 선택 후 ("A 대신 B 쓰기로 했어") - 설계 논의가 끝났을 때 - 나중에 이 결정이 왜 내려졌는지 설명이 필요할 것 같을 때 - "/adr" 직접 호출 ARGUMENTS: 결정 내용 또는 결정 번호 (없으면 대화에서 추출)
AI 에이전트의 액션 스페이스, 도구 정의, 관측(Observation) 포맷을 설계·최적화해 작업 완수율을 높일 때 사용한다.
로컬 개발, 컨테이너 보안, 네트워킹, 볼륨 전략, 멀티 서비스 오케스트레이션을 위한 Docker 및 Docker Compose 패턴.
| name | hexagonal-architecture |
| description | Ports & Adapters 시스템을 설계·구현·리팩터링한다. 명확한 도메인 경계, 의존성 역전, 테스트 가능한 유스케이스 오케스트레이션을 TypeScript·Java·Kotlin·Go 서비스 전반에 적용한다. |
| origin | ECC |
헥사고날 아키텍처(Ports and Adapters)는 비즈니스 로직을 프레임워크·전송 계층·영속화 세부사항으로부터 독립적으로 유지한다. 코어 애플리케이션은 추상 포트에 의존하고, 어댑터가 가장자리에서 그 포트를 구현한다.
요청이 경계, 도메인 중심 설계, 강하게 결합된 서비스의 리팩터링, 또는 애플리케이션 로직과 특정 라이브러리의 분리와 관련될 때 이 스킬을 사용한다.
아웃바운드 포트 인터페이스는 보통 애플리케이션 계층에 위치한다(추상화가 진정 도메인 수준일 때만 도메인에 둔다). 인프라 어댑터가 이를 구현한다.
의존성 방향은 항상 안쪽이다:
명확한 입력·출력 DTO를 가진 단일 유스케이스를 정의한다. 전송 계층 세부사항(Express req, GraphQL context, 잡 페이로드 래퍼)은 이 경계 바깥에 둔다.
모든 부수 효과를 포트로 식별한다:
UserRepositoryPort)BillingGatewayPort)LoggerPort, ClockPort)포트는 기술이 아니라 능력(capability)을 모델링해야 한다.
유스케이스 클래스/함수는 생성자/인자로 포트를 주입받는다. 애플리케이션 수준 불변식을 검증하고, 도메인 규칙을 조율하며, 평범한 데이터 구조를 반환한다.
어댑터를 인스턴스화한 뒤 유스케이스에 주입한다. 숨은 서비스 로케이터 동작을 피하기 위해 와이어링은 한 곳에 모은다.
flowchart LR
Client["Client (HTTP/CLI/Worker)"] --> InboundAdapter["Inbound Adapter"]
InboundAdapter -->|"calls"| UseCase["UseCase (Application Layer)"]
UseCase -->|"uses"| OutboundPort["OutboundPort (Interface)"]
OutboundAdapter["Outbound Adapter"] -->|"implements"| OutboundPort
OutboundAdapter --> ExternalSystem["DB/API/Queue"]
UseCase --> DomainModel["DomainModel"]
명시적 경계를 가진 기능 우선(feature-first) 구조를 사용한다:
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
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);
};
생태계 전반에 동일한 경계 규칙을 적용한다. 문법과 와이어링 스타일만 달라진다.
application/ports/* 인터페이스/타입.adapters/inbound/*, adapters/outbound/*.domain, application.port.in, application.port.out, application.usecase, adapter.in, adapter.out.application.port.*의 인터페이스.@Service는 선택, 필수 아님).domain, application.port, application.usecase, adapter)을 따른다.internal/<feature>/domain, application, ports, adapters/inbound, adapters/outbound.New... 생성자.cmd/<app>/main.go(또는 전용 와이어링 패키지)에서 와이어링하며, 생성자는 명시적으로 유지.req, res, 큐 메타데이터를 직접 읽음.