Claude-skill-registry factory-assembler-expert
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/factory-assembler-expert" ~/.claude/skills/majiayu000-claude-skill-registry-factory-assembler-expert && rm -rf "$T"
manifest:
skills/data/factory-assembler-expert/SKILL.mdsource content
Factory & Assembler 전문가
목적 (Purpose)
Application Layer에서 DTO 변환을 담당하는 컴포넌트(Factory, Assembler)를 규칙에 맞게 생성합니다. CQRS 원칙에 따라 Command/Query 변환을 분리하고, 단방향 변환 원칙을 준수합니다.
활성화 조건
명령 실행 시 (Factory/Assembler 생성)/impl application {feature}
실행 후 Application Layer 작업 시/plan- factory, assembler, bundle, dto 변환 키워드 언급 시
산출물 (Output)
| 컴포넌트 | 파일명 패턴 | 위치 |
|---|---|---|
| CommandFactory | | |
| QueryFactory | | |
| Assembler | | |
| PersistBundle | | |
| QueryBundle | | |
완료 기준 (Acceptance Criteria)
- Factory: @Component 어노테이션, Port 의존 금지
- CommandFactory: create*(), createBundle() 메서드 네이밍
- QueryFactory: createCriteria*() 메서드 네이밍
- Assembler: toResponse*(), toResponseList() 메서드 네이밍
- Assembler: toDomain() 메서드 절대 금지
- Bundle: record 사용, enrichWithId() 메서드
- 비즈니스 로직 없음 (순수 변환/조립만)
- Lombok 금지
- @Transactional 금지
- ArchUnit 테스트 통과
컴포넌트 역할 분리
┌─────────────────────────────────────────────────────────────────┐ │ CQRS 변환 컴포넌트 │ ├───────────────────────────────────┬─────────────────────────────┤ │ COMMAND (인바운드) │ QUERY (아웃바운드) │ ├───────────────────────────────────┼─────────────────────────────┤ │ CommandFactory │ QueryFactory │ │ Command → Domain │ Query → Criteria │ │ create*(), createBundle() │ createCriteria*() │ ├───────────────────────────────────┼─────────────────────────────┤ │ │ Assembler │ │ │ Domain → Response │ │ │ toResponse*() │ ├───────────────────────────────────┴─────────────────────────────┤ │ ❌ Assembler의 toDomain() 절대 금지! │ │ → Command → Domain 변환은 CommandFactory 책임 │ └─────────────────────────────────────────────────────────────────┘
컴포넌트별 책임
| 컴포넌트 | 입력 | 출력 | 메서드 | 책임 |
|---|---|---|---|---|
| CommandFactory | Command DTO | Domain, PersistBundle | , | Command → Domain 변환 |
| QueryFactory | Query DTO | Criteria | | Query → Criteria 변환 |
| Assembler | Domain | Response DTO | | Domain → Response 변환 |
| PersistBundle | - | - | | 영속화 대상 묶음 |
| QueryBundle | - | - | - | 조회 결과 묶음 |
코드 템플릿
1. CommandFactory (Command → Domain 변환)
package com.ryuqq.application.order.factory.command; import org.springframework.stereotype.Component; import com.ryuqq.application.order.dto.command.PlaceOrderCommand; import com.ryuqq.application.order.dto.command.OrderItemCommand; import com.ryuqq.application.order.dto.bundle.OrderPersistBundle; import com.ryuqq.domain.order.aggregate.Order; import com.ryuqq.domain.order.aggregate.OrderItem; import com.ryuqq.domain.order.vo.CustomerId; import com.ryuqq.domain.order.vo.Money; import com.ryuqq.domain.order.vo.ProductId; import com.ryuqq.domain.order.vo.Quantity; import com.ryuqq.domain.outbox.OutboxEvent; import java.util.List; /** * Order Command Factory * - Command → Domain 변환 * - PersistBundle 생성 * - 비즈니스 로직 없음 (순수 변환) */ @Component public class OrderCommandFactory { /** * PlaceOrderCommand → Order 변환 */ public Order create(PlaceOrderCommand command) { List<OrderItem> items = command.items().stream() .map(this::createOrderItem) .toList(); return Order.forNew( new CustomerId(command.customerId()), items ); } /** * OrderItemCommand → OrderItem 변환 */ private OrderItem createOrderItem(OrderItemCommand itemCommand) { return OrderItem.forNew( new ProductId(itemCommand.productId()), new Quantity(itemCommand.quantity()), new Money(itemCommand.unitPrice()) ); } /** * PersistBundle 생성 (Order + Outbox) * - 영속화에 필요한 객체들을 하나로 묶음 * - Event ID는 null (Facade에서 Enrichment) */ public OrderPersistBundle createBundle(PlaceOrderCommand command) { Order order = create(command); OutboxEvent outboxEvent = OutboxEvent.forNew( "Order", null, // ID는 Facade에서 할당 "OrderPlaced", order.toEventPayload() ); return new OrderPersistBundle(order, outboxEvent); } }
핵심 규칙:
어노테이션 (not @Service)@Component
접미사 필수*CommandFactory
,create*()
메서드 네이밍createBundle()
팩토리 메서드 사용Domain.forNew()- Port 의존 금지 (조회 없음)
- 비즈니스 로직 금지 (순수 변환)
2. QueryFactory (Query → Criteria 변환)
package com.ryuqq.application.order.factory.query; import org.springframework.stereotype.Component; import com.ryuqq.application.order.dto.query.OrderDetailQuery; import com.ryuqq.application.order.dto.query.OrderSearchQuery; import com.ryuqq.domain.order.criteria.OrderDetailCriteria; import com.ryuqq.domain.order.criteria.OrderSearchCriteria; import com.ryuqq.domain.order.vo.CustomerId; import com.ryuqq.domain.order.vo.OrderId; import com.ryuqq.domain.order.vo.OrderStatus; /** * Order Query Factory * - Query DTO → Domain Criteria 변환 * - 비즈니스 로직 없음 (순수 변환) */ @Component public class OrderQueryFactory { /** * OrderSearchQuery → OrderSearchCriteria */ public OrderSearchCriteria createSearchCriteria(OrderSearchQuery query) { return OrderSearchCriteria.builder() .customerId(query.customerId() != null ? new CustomerId(query.customerId()) : null) .status(query.status() != null ? OrderStatus.valueOf(query.status()) : null) .fromDate(query.fromDate()) .toDate(query.toDate()) .page(query.page()) .size(query.size()) .build(); } /** * OrderDetailQuery → OrderDetailCriteria */ public OrderDetailCriteria createDetailCriteria(OrderDetailQuery query) { return new OrderDetailCriteria( new OrderId(query.orderId()), query.includeItems(), query.includeShipping(), query.includePayment() ); } /** * 단순 ID 기반 Criteria */ public OrderDetailCriteria createByIdCriteria(Long orderId) { return new OrderDetailCriteria( new OrderId(orderId), true, // includeItems true, // includeShipping true // includePayment ); } }
핵심 규칙:
어노테이션@Component
접미사 필수*QueryFactory
메서드 네이밍createCriteria*()- Domain Criteria 반환 (VO 포함)
- null 안전 처리
- Port 의존 금지
3. Assembler (Domain → Response 변환)
package com.ryuqq.application.order.assembler; import org.springframework.stereotype.Component; import com.ryuqq.application.order.dto.response.OrderResponse; import com.ryuqq.application.order.dto.response.OrderDetailResponse; import com.ryuqq.domain.order.aggregate.Order; import com.ryuqq.domain.order.aggregate.OrderLineItem; import java.util.List; /** * Order Assembler - Domain → Response 변환 전용 * * <p>Assembler는 Domain 객체를 Response DTO로 변환하는 단일 책임을 가집니다.</p> * <p><strong>Command → Domain 변환은 CommandFactory에서 처리합니다.</strong></p> * * @see <a href="https://github.com/Sairyss/domain-driven-hexagon">domain-driven-hexagon</a> */ @Component public class OrderAssembler { /** * Domain → Response 변환 */ public OrderResponse toResponse(Order order) { return new OrderResponse( order.id().value(), order.customerId().value(), order.totalAmount().value(), order.status().name(), order.createdAt() ); } /** * Domain → Detail Response 변환 */ public OrderDetailResponse toDetailResponse(Order order) { return new OrderDetailResponse( order.id().value(), order.customerId().value(), toLineItems(order.lineItems()), order.totalAmount().value(), order.status().name(), order.createdAt() ); } /** * LineItem 목록 변환 (private helper) */ private List<OrderDetailResponse.LineItem> toLineItems(List<OrderLineItem> items) { return items.stream() .map(item -> new OrderDetailResponse.LineItem( item.id().value(), item.productId().value(), item.productName(), item.quantity().value(), item.unitPrice().value() )) .toList(); } /** * List 변환 */ public List<OrderResponse> toResponseList(List<Order> orders) { if (orders == null || orders.isEmpty()) { return List.of(); } return orders.stream() .map(this::toResponse) .toList(); } }
핵심 규칙:
어노테이션@Component
접미사 필수*Assembler
,toResponse*()
메서드 네이밍toResponseList()- toDomain() 메서드 절대 금지!
- Port/Repository 의존 금지 (순수 변환)
- PageResponse 반환 금지 (UseCase 책임)
- Static 메서드 금지
4. PersistBundle (영속화 대상 묶음)
package com.ryuqq.application.order.dto.bundle; import com.ryuqq.domain.order.aggregate.Order; import com.ryuqq.domain.order.aggregate.OrderHistory; import com.ryuqq.domain.order.vo.OrderId; import com.ryuqq.domain.outbox.OutboxEvent; /** * Order 영속화 Bundle * - CommandFactory에서 생성 * - Facade에서 영속화 * - enrichWithId()로 ID 할당 */ public record OrderPersistBundle( Order order, OrderHistory history, OutboxEvent outboxEvent ) { /** * ID 할당 후 새 Bundle 반환 * - 불변성 유지 * - Law of Demeter 준수 (Facade에서 내부 객체 직접 접근 안 함) */ public OrderPersistBundle enrichWithId(OrderId orderId) { return new OrderPersistBundle( order, history.withOrderId(orderId), outboxEvent.withAggregateId(orderId.value()) ); } }
핵심 규칙:
사용 (불변성)record
접미사 필수*PersistBundle
메서드 제공enrichWithId()- Domain 객체만 포함 (DTO/Response 금지)
- 비즈니스 로직 금지
5. QueryBundle (조회 결과 묶음)
package com.ryuqq.application.order.dto.bundle; import com.ryuqq.domain.order.aggregate.Order; import com.ryuqq.domain.member.aggregate.Member; /** * Order 조회 결과 Bundle * - QueryFacade에서 생성 * - Assembler에서 Response 변환 */ public record OrderDetailQueryBundle( Order order, Member member ) { }
핵심 규칙:
사용record
접미사 필수*QueryBundle- 순수 데이터 객체 (특수 메서드 없음)
- Domain 객체만 포함
Zero-Tolerance 규칙
✅ MANDATORY (필수)
| 규칙 | 대상 | 설명 |
|---|---|---|
| Factory, Assembler | Spring Bean 등록 |
접미사 | CommandFactory | 네이밍 규칙 |
접미사 | QueryFactory | 네이밍 규칙 |
접미사 | Assembler | 네이밍 규칙 |
/ | CommandFactory | 메서드 네이밍 |
| QueryFactory | 메서드 네이밍 |
/ | Assembler | 메서드 네이밍 |
| PersistBundle | ID 할당 메서드 |
| Bundle | 불변성 보장 |
사용 | CommandFactory | 팩토리 메서드 사용 |
❌ PROHIBITED (금지)
| 항목 | 대상 | 이유 |
|---|---|---|
어노테이션 | Factory, Assembler | @Component 사용 |
| 모든 컴포넌트 | Service 책임 |
메서드 | Assembler | 핵심: CommandFactory 책임 |
메서드 | Assembler | QueryFactory 책임 |
| Port 의존 | Factory, Assembler | 순수 변환 원칙 |
| Repository 의존 | Factory, Assembler | 순수 변환 원칙 |
| 비즈니스 로직 | 모든 컴포넌트 | Domain 책임 |
| 계산 로직 | Assembler | Domain 책임 |
| PageResponse 반환 | Assembler | UseCase 책임 |
| Static 메서드 | Assembler | 테스트 용이성 |
| Lombok | 모든 컴포넌트 | Plain Java 사용 |
| final 클래스 | Factory, Assembler | Spring Proxy |
패키지 구조
application/ └── {bc}/ ├── factory/ │ ├── command/ │ │ └── {Bc}CommandFactory.java # Command → Domain │ └── query/ │ └── {Bc}QueryFactory.java # Query → Criteria ├── assembler/ │ └── {Bc}Assembler.java # Domain → Response └── dto/ ├── bundle/ │ ├── {Bc}PersistBundle.java # 영속화 대상 묶음 │ └── {Bc}QueryBundle.java # 조회 결과 묶음 ├── command/ │ └── {Action}{Bc}Command.java # Command DTO ├── query/ │ └── {Bc}{Action}Query.java # Query DTO └── response/ └── {Bc}Response.java # Response DTO
체크리스트 (Output Checklist)
CommandFactory
-
어노테이션@Component -
접미사*CommandFactory - 패키지:
application.{bc}.factory.command -
메서드 네이밍create*() -
메서드 (Bundle 필요 시)createBundle() -
팩토리 메서드 사용Domain.forNew() - 생성자 주입 (Lombok 없음)
- Port 의존 없음
-
없음@Transactional - 비즈니스 로직 없음
QueryFactory
-
어노테이션@Component -
접미사*QueryFactory - 패키지:
application.{bc}.factory.query -
메서드 네이밍createCriteria*() - Domain Criteria 반환
- null 안전 처리
- 생성자 주입 (Lombok 없음)
- Port 의존 없음
-
없음@Transactional - 비즈니스 로직 없음
Assembler
-
어노테이션@Component -
접미사*Assembler - 패키지:
application.{bc}.assembler -
메서드 네이밍toResponse*() -
메서드toResponseList() - toDomain() 메서드 없음 (핵심!)
- toCriteria() 메서드 없음
- Port/Repository 의존 없음
- PageResponse 반환 없음
- Static 메서드 없음
- 비즈니스 로직/계산 로직 없음
- Lombok 없음
-
없음@Transactional
PersistBundle
-
사용record -
접미사*PersistBundle - 패키지:
application.{bc}.dto.bundle -
메서드 제공enrichWithId() - Domain 객체만 포함
- DTO/Response 포함 금지
- 비즈니스 로직 없음
QueryBundle
-
사용record -
접미사*QueryBundle - 패키지:
application.{bc}.dto.bundle - Domain 객체만 포함
ArchUnit 테스트 체크리스트
CommandFactory ArchUnit (10개 규칙)
| # | 규칙 | 유형 |
|---|---|---|
| 1 | 어노테이션 필수 | 필수 |
| 2 | 접미사 필수 | 필수 |
| 3 | 패키지 위치 | 필수 |
| 4 | 클래스 금지 | 필수 |
| 5 | 메서드 네이밍 | 권장 |
| 6 | 금지 | 필수 |
| 7 | Port 의존 금지 | 필수 |
| 8 | Repository 의존 금지 | 필수 |
| 9 | Lombok 금지 | 필수 |
| 10 | 금지 | 필수 |
QueryFactory ArchUnit (10개 규칙)
| # | 규칙 | 유형 |
|---|---|---|
| 1 | 어노테이션 필수 | 필수 |
| 2 | 접미사 필수 | 필수 |
| 3 | 패키지 위치 | 필수 |
| 4 | 클래스 금지 | 필수 |
| 5 | 메서드 네이밍 | 권장 |
| 6 | 금지 | 필수 |
| 7 | Port 의존 금지 | 필수 |
| 8 | Repository 의존 금지 | 필수 |
| 9 | Lombok 금지 | 필수 |
| 10 | 금지 | 필수 |
Assembler ArchUnit (19개 규칙)
| # | 규칙 | 유형 |
|---|---|---|
| 1 | 필수 | 필수 |
| 2 | Lombok 절대 금지 | 금지 |
| 3 | Static 메서드 금지 | 금지 |
| 4 | Port 의존성 금지 | 금지 |
| 5 | Repository 의존성 금지 | 금지 |
| 6 | Spring Data 의존성 금지 | 금지 |
| 7 | 접미사 | 필수 |
| 8 | 패키지 | 필수 |
| 9 | 메서드 네이밍 | 필수 |
| 10 | toDomain 메서드 금지 (핵심) | 금지 |
| 11 | 비즈니스 메서드 금지 | 금지 |
| 12 | 금지 | 금지 |
| 13 | PageResponse 반환 금지 | 금지 |
| 14 | public 클래스 | 필수 |
| 15 | final 클래스 금지 | 필수 |
| 16 | 필드 final | 권장 |
| 17 | Layer 의존성 제한 | 필수 |
| 18 | 필드명 소문자 시작 | 권장 |
| 19 | 계산 로직 금지 | 금지 |
Service에서 호출 패턴
Command Service → Factory + Assembler
@Component public class PlaceOrderService implements PlaceOrderUseCase { private final OrderCommandFactory orderCommandFactory; private final OrderFacade orderFacade; private final OrderAssembler orderAssembler; public PlaceOrderService( OrderCommandFactory orderCommandFactory, OrderFacade orderFacade, OrderAssembler orderAssembler ) { this.orderCommandFactory = orderCommandFactory; this.orderFacade = orderFacade; this.orderAssembler = orderAssembler; } @Override public OrderResponse execute(PlaceOrderCommand command) { // 1. Command → Domain (Factory) OrderPersistBundle bundle = orderCommandFactory.createBundle(command); // 2. 영속화 (Facade) Order saved = orderFacade.persistOrderBundle(bundle); // 3. Domain → Response (Assembler) return orderAssembler.toResponse(saved); } }
Query Service → Factory + Assembler
@Component public class GetOrderDetailService implements GetOrderDetailUseCase { private final OrderQueryFactory orderQueryFactory; private final OrderReadManager orderReadManager; private final OrderAssembler orderAssembler; public GetOrderDetailService( OrderQueryFactory orderQueryFactory, OrderReadManager orderReadManager, OrderAssembler orderAssembler ) { this.orderQueryFactory = orderQueryFactory; this.orderReadManager = orderReadManager; this.orderAssembler = orderAssembler; } @Override @Transactional(readOnly = true) public OrderDetailResponse execute(OrderDetailQuery query) { // 1. Query → Criteria (Factory) OrderDetailCriteria criteria = orderQueryFactory.createDetailCriteria(query); // 2. 조회 (Manager) Order order = orderReadManager.getByCriteria(criteria); // 3. Domain → Response (Assembler) return orderAssembler.toDetailResponse(order); } }
참조 문서
- CommandFactory Guide:
docs/coding_convention/03-application-layer/factory/command/command-factory-guide.md - QueryFactory Guide:
docs/coding_convention/03-application-layer/factory/query/query-factory-guide.md - Assembler Guide:
docs/coding_convention/03-application-layer/assembler/assembler-guide.md - PersistBundle Guide:
docs/coding_convention/03-application-layer/dto/bundle/persist-bundle-guide.md - QueryBundle Guide:
docs/coding_convention/03-application-layer/dto/bundle/query-bundle-guide.md - CommandFactory ArchUnit:
docs/coding_convention/03-application-layer/factory/command/command-factory-archunit.md - QueryFactory ArchUnit:
docs/coding_convention/03-application-layer/factory/query/query-factory-archunit.md - Assembler ArchUnit:
docs/coding_convention/03-application-layer/assembler/assembler-archunit.md