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.md
source content

Factory & Assembler 전문가

목적 (Purpose)

Application Layer에서 DTO 변환을 담당하는 컴포넌트(Factory, Assembler)를 규칙에 맞게 생성합니다. CQRS 원칙에 따라 Command/Query 변환을 분리하고, 단방향 변환 원칙을 준수합니다.

활성화 조건

  • /impl application {feature}
    명령 실행 시 (Factory/Assembler 생성)
  • /plan
    실행 후 Application Layer 작업 시
  • factory, assembler, bundle, dto 변환 키워드 언급 시

산출물 (Output)

컴포넌트파일명 패턴위치
CommandFactory
{Bc}CommandFactory.java
application/{bc}/factory/command/
QueryFactory
{Bc}QueryFactory.java
application/{bc}/factory/query/
Assembler
{Bc}Assembler.java
application/{bc}/assembler/
PersistBundle
{Bc}PersistBundle.java
application/{bc}/dto/bundle/
QueryBundle
{Bc}QueryBundle.java
application/{bc}/dto/bundle/

완료 기준 (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 책임                  │
└─────────────────────────────────────────────────────────────────┘

컴포넌트별 책임

컴포넌트입력출력메서드책임
CommandFactoryCommand DTODomain, PersistBundle
create*()
,
createBundle()
Command → Domain 변환
QueryFactoryQuery DTOCriteria
createCriteria*()
Query → Criteria 변환
AssemblerDomainResponse DTO
toResponse*()
Domain → Response 변환
PersistBundle--
enrichWithId()
영속화 대상 묶음
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);
    }
}

핵심 규칙:

  • @Component
    어노테이션 (not @Service)
  • *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 (필수)

규칙대상설명
@Component
Factory, AssemblerSpring Bean 등록
*CommandFactory
접미사
CommandFactory네이밍 규칙
*QueryFactory
접미사
QueryFactory네이밍 규칙
*Assembler
접미사
Assembler네이밍 규칙
create*()
/
createBundle()
CommandFactory메서드 네이밍
createCriteria*()
QueryFactory메서드 네이밍
toResponse*()
/
toResponseList()
Assembler메서드 네이밍
enrichWithId()
PersistBundleID 할당 메서드
record
Bundle불변성 보장
Domain.forNew()
사용
CommandFactory팩토리 메서드 사용

❌ PROHIBITED (금지)

항목대상이유
@Service
어노테이션
Factory, Assembler@Component 사용
@Transactional
모든 컴포넌트Service 책임
toDomain()
메서드
Assembler핵심: CommandFactory 책임
toCriteria()
메서드
AssemblerQueryFactory 책임
Port 의존Factory, Assembler순수 변환 원칙
Repository 의존Factory, Assembler순수 변환 원칙
비즈니스 로직모든 컴포넌트Domain 책임
계산 로직AssemblerDomain 책임
PageResponse 반환AssemblerUseCase 책임
Static 메서드Assembler테스트 용이성
Lombok모든 컴포넌트Plain Java 사용
final 클래스Factory, AssemblerSpring 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*()
    메서드 네이밍
  • createBundle()
    메서드 (Bundle 필요 시)
  • 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
@Component
어노테이션 필수
필수
2
*CommandFactory
접미사 필수
필수
3
factory.command
패키지 위치
필수
4
final
클래스 금지
필수
5
create*
메서드 네이밍
권장
6
@Transactional
금지
필수
7Port 의존 금지필수
8Repository 의존 금지필수
9Lombok 금지필수
10
@Service
금지
필수

QueryFactory ArchUnit (10개 규칙)

#규칙유형
1
@Component
어노테이션 필수
필수
2
*QueryFactory
접미사 필수
필수
3
factory.query
패키지 위치
필수
4
final
클래스 금지
필수
5
createCriteria*
메서드 네이밍
권장
6
@Transactional
금지
필수
7Port 의존 금지필수
8Repository 의존 금지필수
9Lombok 금지필수
10
@Service
금지
필수

Assembler ArchUnit (19개 규칙)

#규칙유형
1
@Component
필수
필수
2Lombok 절대 금지금지
3Static 메서드 금지금지
4Port 의존성 금지금지
5Repository 의존성 금지금지
6Spring Data 의존성 금지금지
7
*Assembler
접미사
필수
8
..assembler..
패키지
필수
9
toResponse*
메서드 네이밍
필수
10toDomain 메서드 금지 (핵심)금지
11비즈니스 메서드 금지금지
12
@Transactional
금지
금지
13PageResponse 반환 금지금지
14public 클래스필수
15final 클래스 금지필수
16필드 final권장
17Layer 의존성 제한필수
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