[프롬프트] 클린 아키텍처 설계 프롬프트 - 최적 프롬프트 레시피
💡 한 줄 요약: "클린 아키텍처로 설계해줘"라는 단순 질의 시 발생하는 ORM 중심의 빈혈 도메인 모델(Anemic Domain Model), 계층 간 결합도 상승, 비즈니스 로직의 인프라 의존 문제를 원천 차단하고, Principal Enterprise Software Architect 및 Clean Architecture Tech Lead 관점에서 Entities, Use Cases, Interface Adapters, Frameworks & Drivers 4계층과 의존성 역전 원칙(DIP), In/Outbound Port, 헥사고날(Hexagonal) 구조를 완벽히 도출하는 최적 프롬프트 레시피입니다.
1. 페인포인트 & 작업 목표
2. 프롬프트 레시피 비교
❌ 레시피 A: 기본형 (단순 질의 초안)
역할 정의, 계층 제약, 의존성 역전 원칙, 순수 도메인 규칙 없이 단순 아키텍처 설계를 요청하는 초안입니다.
[영문 프롬프트 (복사용)]
Design clean architecture for: [service][한글 프롬프트 (1:1 매칭 복사용)]
다음 서비스에 대해 클린 아키텍처를 설계해줘: [서비스명]⚡ 레시피 B: Principal Enterprise Software Architect 실무 종결형 클린 아키텍처 설계 프롬프트
순수 도메인 엔티티 보호, Use Case 단일 책임 원칙, In/Outbound Port 기반 의존성 역전(DIP), 패키지 디렉토리 구조 및 실제 인터페이스/구현체 코드 명세를 완벽히 도출하는 레시피입니다.
[영문 프롬프트 (복사용)]
You are a Principal Enterprise Software Architect and Clean Architecture Tech Lead.
Design a production-grade, strictly decoupled Clean Architecture specification and code implementation for the target business domain below.
### Target Domain Context:
- Domain Service: [e.g., E-Commerce Order & Payment Authorization Domain]
- Core Business Capabilities: [e.g., Order creation, discount calculation, inventory reservation, PG payment authorization, order cancellation]
- Target Tech Stack: [e.g., TypeScript / Node.js (or Java / Spring Boot)]
### Architectural Directives & Constraints:
1. 4-TIER ONION/CLEAN LAYER STRUCTURE:
- Enterprise Business Rules (Pure Entities): Zero external framework/ORM dependencies. Pure domain logic and invariant validation.
- Application Business Rules (Use Cases): Single-purpose Interactor/UseCase classes. Execute workflows via ports.
- Interface Adapters: Inbound Adapters (REST Controllers, CLI) and Outbound Adapters (Prisma/JPA Repositories, External Payment Client).
- Frameworks & Drivers: Express/Fastify/NestJS or Spring configuration, database engines.
2. STRICT DEPENDENCY INVERSION (DIP) & PORTS:
- Define explicit Inbound Ports (Driving Ports: Use Case interfaces called by controllers).
- Define explicit Outbound Ports (Driven Ports: Repository & Payment Gateway interfaces implemented in the adapter layer).
- Outer layers depend on inner layers; inner layers NEVER import outer layers.
3. DOMAIN PURITY & DATA MAPPING:
- Forbid ORM decorators inside domain entities. Require explicit Mapper classes between Domain Entity and DB Schema Model.
4. DELIVERABLE SPECIFICATION:
- Provide modular package directory tree.
- Provide compilable code implementations for: (1) Pure Domain Entity with invariants, (2) Inbound & Outbound Port interfaces, (3) Use Case Interactor with transactional flow, (4) Outbound Persistence Adapter.[한글 프롬프트 (1:1 매칭 복사용)]
당신은 수석 엔터프라이즈 소프트웨어 아키텍트(Principal Enterprise Software Architect)이자 클린 아키텍처 테크 리드(Clean Architecture Tech Lead)입니다.
아래 설명된 대상 비즈니스 도메인에 대해 엄격히 결합이 분리된 프로덕션급 클린 아키텍처 명세 및 코드 구현을 설계하십시오.
### 대상 도메인 컨텍스트:
- 도메인 서비스: [예: 전자상거래 주문 및 결제 승인 도메인]
- 핵심 비즈니스 기능: [예: 주문 생성, 할인 정책 계산, 재고 가예약, PG 결제 승인, 주문 취소]
- 대상 기술 스택: [예: TypeScript / Node.js (또는 Java / Spring Boot)]
### 아키텍처 설계 필수 지침 및 제약사항:
1. 4계층 어니언/클린 아키텍처 구조:
- 엔터프라이즈 비즈니스 규칙(Pure Entities): 외부 프레임워크 및 ORM 의존성 0%. 순수 도메인 로직 및 불변식(Invariant) 검증 캡슐화.
- 애플리케이션 비즈니스 규칙(Use Cases): 단일 목적 인터랙터(UseCase) 클래스 설계. 포트를 통한 오케스트레이션.
- 인터페이스 어댑터(Interface Adapters): 인바운드 어댑터(REST 컨트롤러) 및 아웃바운드 어댑터(Prisma/JPA 리포지토리, 외부 PG 결제 연동).
- 프레임워크 및 드라이버(Frameworks & Drivers): 웹 프레임워크 라우터 및 DB 엔진 연동.
2. 엄격한 의존성 역전 원칙(DIP) 및 포트 사양:
- 명시적인 인바운드 포트(Driving Ports: 컨트롤러가 호출하는 Use Case 인터페이스) 정의.
- 명시적인 아웃바운드 포트(Driven Ports: 어댑터 계층이 구현할 영속성 및 외부 결제 인터페이스) 정의.
- 바깥쪽 계층이 안쪽 계층에 의존하며, 안쪽 계층은 바깥쪽 계층을 절대 참조(import)하지 않음.
3. 도메인 순수성 및 데이터 매퍼 강제:
- 도메인 엔티티 내 ORM 데코레이터 선언 절대 금지. 순수 도메인 엔티티와 DB 영속성 스키마 간 데이터 변환 매퍼(Mapper) 구현.
4. 산출물 명세:
- 모듈식 패키지 디렉토리 구조도 제공.
- 다음 항목의 컴파일 가능한 실제 구현 코드 제공: (1) 불변식을 가진 순수 도메인 엔티티, (2) 인바운드/아웃바운드 포트 인터페이스, (3) 트랜잭션 흐름을 제어하는 Use Case 인터랙터, (4) 아웃바운드 영속성 어댑터.@Entity, @Column)를 엔티티에서 완전히 격리하고 Mapper를 강제하여 프레임워크 침투를 차단합니다.🚀 레시피 C: 레거시 모놀리스 Fat Service ➔ 헥사고날 포트 & 어댑터 쾌속 리팩토링 특화
DB, 결제 SDK, 이메일 전송, 비즈니스 로직이 1,500줄 이상 한데 엉킨 레거시 모놀리스 Service를 포트 & 어댑터 패턴으로 안전하게 분해하는 리팩토링 레시피입니다.
[영문 프롬프트 (복사용)]
You are a Staff Refactoring Architect specializing in Legacy Modernization and Hexagonal Architecture.
Refactor the provided legacy fat monolithic service into a decoupled Hexagonal (Ports and Adapters) architecture.
### Target Legacy Context:
- Legacy Service Code: [Paste or summarize 1,500-line legacy Spring/NestJS Service with mixed DB, HTTP, and domain logic]
- Pain Points: Mixed transaction scopes, untestable external SDK calls, DB entity pollution.
### Modernization Directives:
1. SEPARATION OF CONCERNS & DECONSTRUCTION:
- Extract pure domain invariants into a domain entity/aggregate root.
- Split bloated methods into isolated Command Use Cases and Query Use Cases (CQRS hint).
2. PORT CONTRACT EXTRACTION:
- Extract all DB interactions into Outbound Command/Query Ports.
- Extract third-party payment/messaging calls into Outbound Gateway Ports.
3. STEP-BY-STEP STRANGLER FIG MIGRATION:
- Provide a zero-downtime, non-breaking refactoring step plan (Strangler Fig Pattern).
- Write refactored Inbound Controller, UseCase Interactor, Port Interfaces, and Adapter implementations.[한글 프롬프트 (1:1 매칭 복사용)]
당신은 레거시 시스템 현대화 및 헥사고날 아키텍처를 전문으로 하는 스태프 리팩토링 아키텍트(Staff Refactoring Architect)입니다.
제공된 레거시 Fat Monolithic 서비스를 결합이 분리된 헥사고날(포트 & 어댑터) 아키텍처로 안전하게 리팩토링하십시오.
### 대상 레거시 컨텍스트:
- 레거시 서비스 코드: [DB, 외부 HTTP, 비즈니스 로직이 혼재된 1,500줄 규모의 레거시 Spring/NestJS Service 코드 또는 요약]
- 직면한 문제점: 혼재된 트랜잭션 범위, 테스트 불가능한 외부 SDK 결합, DB 엔티티 오염.
### 현대화 리팩토링 지침:
1. 관심사 분리 및 해체:
- 서비스 내부에 흩어진 순수 도메인 비즈니스 불변식을 도메인 엔티티(또는 애그리게이트 루트)로 추출하십시오.
- 비대해진 메서드들을 독립적인 Command Use Case와 Query Use Case로 분리하십시오.
2. 포트 계약(Contract) 추출:
- 모든 DB 상호작용을 아웃바운드 포트(OrderCommandPort, OrderQueryPort)로 추출하십시오.
- 외부 PG 결제 및 알림 연동을 아웃바운드 게이트웨이 포트(PaymentGatewayPort)로 격리하십시오.
3. 스트랭글러 피그(Strangler Fig) 단계적 마이그레이션:
- 기존 서비스 운영에 무중단으로 점진 적용할 수 있는 단계적 리팩토링 전략을 제시하십시오.
- 리팩토링된 인바운드 컨트롤러, UseCase 인터랙터, 포트 인터페이스 및 어댑터 구현체 코드를 작성하십시오.3. 실행 결과 비교: 레시피 A vs B vs C 실전 대조
❌ 레시피 A 실행 결과: [단순 질의의 한계] ORM에 오염된 빈혈 도메인 3티어 구조
레시피 A를 실행했을 때 도출되는 전형적인 안티패턴 코드입니다.
// 안티패턴: ORM 데코레이터 침투, 빈혈 도메인 모델, Controller-Service 강결합
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
amount: number;
@Column()
status: string; // 단순 데이터 홀더, 상태 변경 유효성 검증 로직 전무
}
@Injectable()
export class OrderService {
constructor(
@InjectRepository(Order) private readonly orderRepo: Repository<Order>,
private readonly stripeClient: StripeSDK // 외부 결제 인프라에 직접 강결합!
) {}
async createAndPay(amount: number) {
const charge = await this.stripeClient.charges.create({ amount });
const order = this.orderRepo.create({ amount, status: 'PAID' });
return await this.orderRepo.save(order);
}
}Order는 비즈니스 규칙이 없는 빈혈 객체이며 TypeORM에 완전히 결합되어 프레임워크 없이는 테스트조차 어렵습니다.OrderService가 외부 StripeSDK와 Repository<Order> 구현체를 직접 참조하여 의존성 역전이 완전히 깨져 있습니다.⚡ 레시피 B 실행 결과: [실전 사례 1] 전자상거래 주문/결제 승인 도메인 클린 아키텍처 패키지 & 코드 명세 (순수 Domain Entity, OrderPaymentUseCase, In/Outbound Port 인터페이스, Prisma 어댑터 분리, 후속 체이닝 2종, 4단계 워크플로우)
1. 패키지 디렉토리 구조 & 순수 Domain Entity
src/order/
├── domain/ # 1. Enterprise Rules (Order.ts: 순수 비즈니스 불변식)
├── application/ # 2. Application Rules (UseCase & In/Outbound Ports)
│ ├── port/in/OrderPaymentUseCase.ts & port/out/OrderRepositoryPort.ts
│ └── service/OrderPaymentService.ts
├── adapter/ # 3. Interface Adapters (Web Controller & Prisma/Stripe)
└── infrastructure/ # 4. Frameworks & Drivers (prisma.ts)// domain/entity/Order.ts (순수 도메인 엔티티: ORM 데코레이터 전무, 자체 불변식 보증)
export class Order {
constructor(
private readonly _id: string,
private readonly _customerId: string,
private _amount: number,
private _status: 'PENDING' | 'AUTHORIZED' | 'CANCELLED'
) {
if (_amount <= 0) throw new Error('Order amount must be positive.');
}
get id(): string { return this._id; }
get amount(): number { return this._amount; }
get status(): string { return this._status; }
public authorizePayment(): void {
if (this._status !== 'PENDING') throw new Error(`Cannot authorize order in status: ${this._status}`);
this._status = 'AUTHORIZED';
}
}2. In/Outbound Port 인터페이스 명세
// application/port/in/OrderPaymentUseCase.ts (인바운드 포트)
export interface OrderPaymentCommand { orderId: string; paymentMethodId: string; }
export interface OrderPaymentUseCase {
execute(command: OrderPaymentCommand): Promise<{ orderId: string; status: string }>;
}
// application/port/out/OrderRepositoryPort.ts & PaymentGatewayPort.ts (아웃바운드 포트)
export interface OrderRepositoryPort {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
export interface PaymentGatewayPort {
authorize(amount: number, paymentMethodId: string): Promise<{ transactionId: string }>;
}3. Use Case Interactor & Outbound Persistence Adapter
// application/service/OrderPaymentService.ts (Use Case: 포트만 의존, DIP 완성)
export class OrderPaymentService implements OrderPaymentUseCase {
constructor(
private readonly orderRepository: OrderRepositoryPort,
private readonly paymentGateway: PaymentGatewayPort
) {}
async execute(command: OrderPaymentCommand): Promise<{ orderId: string; status: string }> {
const order = await this.orderRepository.findById(command.orderId);
if (!order) throw new Error('Order not found');
order.authorizePayment(); // 순수 도메인 상태 변경 (불변식 검증)
await this.paymentGateway.authorize(order.amount, command.paymentMethodId);
await this.orderRepository.save(order);
return { orderId: order.id, status: order.status };
}
}
// adapter/out/persistence/PrismaOrderRepository.ts (아웃바운드 어댑터: Prisma 맵핑)
export class PrismaOrderRepository implements OrderRepositoryPort {
constructor(private readonly prisma: PrismaClient) {}
async findById(id: string): Promise<Order | null> {
const raw = await this.prisma.orderRecord.findUnique({ where: { id } });
return raw ? new Order(raw.id, raw.customerId, raw.amount, raw.status as any) : null;
}
async save(order: Order): Promise<void> {
await this.prisma.orderRecord.upsert({
where: { id: order.id },
update: { amount: order.amount, status: order.status },
create: { id: order.id, customerId: 'cust-1', amount: order.amount, status: order.status }
});
}
}후속 체이닝 2종
OrderPaymentService에서 결제 승인 완료 후 카프카 이벤트를 신뢰성 있게 발행하기 위해 OutboxPatternPort 인터페이스와 Transactional Outbox Adapter 코드를 추가해줘."OrderPaymentService의 Jest 단위 테스트 코드와, Order 도메인 엔티티의 불변식 위반 실패 케이스 테스트를 작성해줘."4단계 워크플로우
🚀 레시피 C 실행 결과: [실전 사례 2] 1,500줄 레거시 스프링 Fat Service의 헥사고날 포트 & 어댑터 리팩토링 (OrderCommandPort, OrderQueryPort, ExternalPaymentAdapter 분리, 후속 체이닝, 3단계 워크플로우)
1. 레거시 해체 및 헥사고날 포트 분리 명세
// 1. 도메인 포트 분리: DB 쓰기(Command)와 외부 결제 통신을 포트로 격리
public interface OrderCommandPort { void updateOrderStatus(UUID orderId, OrderStatus status); }
public interface OrderQueryPort { Optional<OrderSummary> findOrderForProcessing(UUID orderId); }
public interface ExternalPaymentPort { PaymentResult requestAuthorization(UUID orderId, BigDecimal amount); }
// 2. 단일 목적 UseCase로 분해된 Interactor
@Service
@RequiredArgsConstructor
public class ApproveOrderPaymentUseCase {
private final OrderQueryPort orderQueryPort;
private final OrderCommandPort orderCommandPort;
private final ExternalPaymentPort externalPaymentPort;
@Transactional
public void execute(UUID orderId) {
OrderSummary order = orderQueryPort.findOrderForProcessing(orderId)
.orElseThrow(() -> new EntityNotFoundException("Order not found: " + orderId));
PaymentResult result = externalPaymentPort.requestAuthorization(orderId, order.getAmount());
if (!result.isSuccess()) throw new PaymentFailedException(result.getErrorMessage());
orderCommandPort.updateOrderStatus(orderId, OrderStatus.AUTHORIZED);
}
}2. 격리된 외부 결제 어댑터 구현체
// 3. adapter/out/payment/ExternalPaymentAdapter.java
@Component
@RequiredArgsConstructor
public class ExternalPaymentAdapter implements ExternalPaymentPort {
private final LegacyPgClient legacyPgClient; // 레거시 SDK를 어댑터 내부로 은닉
@Override
public PaymentResult requestAuthorization(UUID orderId, BigDecimal amount) {
try {
PgResponse response = legacyPgClient.charge(orderId.toString(), amount.longValue());
return new PaymentResult(response.isOk(), response.getMessage());
} catch (Exception e) {
return new PaymentResult(false, e.getMessage());
}
}
}후속 체이닝: 레거시 병행 실행 검증(Shadow Traffic)
ApproveOrderPaymentUseCase를 동시에 호출하여 결과값 일치 여부를 로깅하는 Spring AOP 기반 Shadow Execution Adapter 코드를 작성해줘."3단계 워크플로우
4. 클린 아키텍처 설계를 위한 4대 엔지니어링 원칙
5. 실전 투입용 플레이스홀더 템플릿
[역할 정의]
당신은 엔터프라이즈 시스템 설계를 총괄하는 Principal Enterprise Software Architect입니다.
[대상 비즈니스 도메인]
- 서비스 도메인: [예: B2B SaaS 구독 및 월간 정산 결제 시스템]
- 핵심 엔티티: [예: Subscription, Invoice, BillingProfile, PaymentTransaction]
- 적용 기술 스택: [예: TypeScript / NestJS / Prisma 또는 Java / Spring Boot 3]
[클린 아키텍처 설계 요구사항]
1. 계층 분리: Entities(순수 비즈니스 룰), UseCases(포트 제어), Interface Adapters(Controller, Repo), Frameworks 4계층 패키지 구조를 정의할 것.
2. 의존성 제약: 도메인 엔티티 내 ORM 어노테이션 사용을 금지하고, UseCase가 참조할 In/Outbound Port 인터페이스를 명시할 것.
3. 구현 코드: (1) 순수 도메인 엔티티, (2) Inbound Port 및 Outbound Port, (3) UseCase 서비스, (4) 영속성 어댑터와 데이터 변환 매퍼 코드를 작성할 것.
4. 검증 방안: 외부 의존성을 Mocking하여 UseCase를 100% 격리 검증하는 단위 테스트 코드를 포함할 것.6. 프롬프트 활용 팁 & 주의사항
registerEvent() 형태로 내부 메모리에 적재하고, Use Case 트랜잭션이 성공적으로 커밋된 후 Outbox Adapter를 통해 비동기 발행되도록 설계하십시오.Mermaid classDiagram 또는 계층 간 의존성 흐름도를 함께 요청하면 AI가 잘못된 역방향 import를 생성하는 오류를 크게 줄일 수 있습니다.