スキル一覧に戻る
ryu-qqq

factory-assembler-expert

by ryu-qqq

0🍴 0📅 2026年1月19日
GitHubで見るManusで実行

SKILL.md


name: factory-assembler-expert version: 3.0.0 description: | Factory & Assembler 변환 전문가. CommandFactory Command→Domain 변환, QueryFactory Query→Criteria 변환, Assembler Domain→Response 변환. PersistBundle 영속화 묶음, QueryBundle 조회 결과 묶음. 단방향 변환 원칙: Factory는 생성만, Assembler는 조립만. 비즈니스 로직 절대 금지. author: claude-spring-standards created: 2024-11-01 updated: 2025-12-05 tags: [project, application, factory, assembler, bundle, dto-conversion, cqrs]

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.javaapplication/{bc}/factory/command/
QueryFactory{Bc}QueryFactory.javaapplication/{bc}/factory/query/
Assembler{Bc}Assembler.javaapplication/{bc}/assembler/
PersistBundle{Bc}PersistBundle.javaapplication/{bc}/dto/bundle/
QueryBundle{Bc}QueryBundle.javaapplication/{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, PersistBundlecreate*(), createBundle()Command → Domain 변환
QueryFactoryQuery DTOCriteriacreateCriteria*()Query → Criteria 변환
AssemblerDomainResponse DTOtoResponse*()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 (필수)

규칙대상설명
@ComponentFactory, AssemblerSpring Bean 등록
*CommandFactory 접미사CommandFactory네이밍 규칙
*QueryFactory 접미사QueryFactory네이밍 규칙
*Assembler 접미사Assembler네이밍 규칙
create*() / createBundle()CommandFactory메서드 네이밍
createCriteria*()QueryFactory메서드 네이밍
toResponse*() / toResponseList()Assembler메서드 네이밍
enrichWithId()PersistBundleID 할당 메서드
recordBundle불변성 보장
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 접미사 필수필수
3factory.command 패키지 위치필수
4final 클래스 금지필수
5create* 메서드 네이밍권장
6@Transactional 금지필수
7Port 의존 금지필수
8Repository 의존 금지필수
9Lombok 금지필수
10@Service 금지필수

QueryFactory ArchUnit (10개 규칙)

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

Assembler ArchUnit (19개 규칙)

#규칙유형
1@Component 필수필수
2Lombok 절대 금지금지
3Static 메서드 금지금지
4Port 의존성 금지금지
5Repository 의존성 금지금지
6Spring Data 의존성 금지금지
7*Assembler 접미사필수
8..assembler.. 패키지필수
9toResponse* 메서드 네이밍필수
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

スコア

総合スコア

50/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です