スキル一覧に戻る
ryu-qqq

entity-mapper-expert

by ryu-qqq

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

SKILL.md


name: entity-mapper-expert version: 3.0.0 description: | JPA Entity Long FK 전략, BaseAuditEntity/SoftDeletableEntity 상속, EntityMapper Domain⇄Entity 변환. JPA 관계 어노테이션(@ManyToOne, @OneToMany) 금지. Lombok 금지, Setter 금지. Entity는 of() static factory method만 public, 생성자는 protected/private. author: claude-spring-standards created: 2024-11-01 updated: 2025-12-05 tags: [project, persistence, entity, mapper, jpa, long-fk, baseauditentity]

Entity & Mapper 전문가 (Entity-Mapper Expert)

목적 (Purpose)

Persistence Layer에서 JPA EntityEntityMapper를 규칙에 맞게 생성합니다. Domain Layer와 Infrastructure(JPA)를 깔끔하게 분리하여 헥사고날 아키텍처를 준수합니다.

활성화 조건

  • /impl persistence {feature} 명령 실행 시
  • /plan 실행 후 Persistence Layer 작업 시
  • entity, mapper, jpa, BaseAuditEntity 키워드 언급 시
  • /kb-entity 명령 시 자동 활성화

산출물 (Output)

컴포넌트파일명 패턴위치
JPA Entity{Domain}JpaEntity.javaadapter-out/persistence-mysql/{bc}/entity/
EntityMapper{Domain}JpaEntityMapper.javaadapter-out/persistence-mysql/{bc}/mapper/

완료 기준 (Acceptance Criteria)

  • Long FK 전략 적용 (JPA 관계 어노테이션 금지)
  • BaseAuditEntity 또는 SoftDeletableEntity 상속 검토
  • protected 기본 생성자 + private 전체 필드 생성자
  • public static of() 팩토리 메서드만 노출
  • Getter만 제공 (Setter 금지)
  • *JpaEntity 네이밍 규칙
  • EntityMapper는 @Component Bean
  • EntityMapper는 toEntity() + toDomain() 필수
  • Lombok 금지
  • ArchUnit 테스트 통과

JPA Entity 코드 템플릿

1. BaseAuditEntity 상속 (시간 정보만)

package com.ryuqq.adapter.out.persistence.order.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import com.ryuqq.adapter.out.persistence.common.entity.BaseAuditEntity;
import com.ryuqq.domain.order.vo.OrderStatus;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * OrderJpaEntity - 주문 JPA Entity
 *
 * <p><strong>BaseAuditEntity 상속:</strong></p>
 * <ul>
 *   <li>공통 감사 필드 상속: createdAt, updatedAt</li>
 * </ul>
 *
 * <p><strong>Long FK 전략:</strong></p>
 * <ul>
 *   <li>JPA 관계 어노테이션 사용 금지 (@ManyToOne, @OneToMany 등)</li>
 *   <li>모든 외래키는 Long 타입으로 직접 관리</li>
 * </ul>
 */
@Entity
@Table(name = "orders")
public class OrderJpaEntity extends BaseAuditEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "order_number", nullable = false, length = 50)
    private String orderNumber;

    @Column(name = "customer_id", nullable = false)
    private Long customerId;  // ✅ Long FK (관계 어노테이션 금지)

    @Column(name = "total_amount", nullable = false, precision = 15, scale = 2)
    private BigDecimal totalAmount;

    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false, length = 20)
    private OrderStatus status;

    /**
     * JPA 기본 생성자 (protected)
     *
     * <p>JPA 스펙 요구사항으로 반드시 필요합니다.</p>
     */
    protected OrderJpaEntity() {
    }

    /**
     * 전체 필드 생성자 (private)
     *
     * <p>직접 호출 금지, of() 스태틱 메서드로만 생성하세요.</p>
     */
    private OrderJpaEntity(
        Long id,
        String orderNumber,
        Long customerId,
        BigDecimal totalAmount,
        OrderStatus status,
        LocalDateTime createdAt,
        LocalDateTime updatedAt
    ) {
        super(createdAt, updatedAt);  // ✅ 부모 필드 초기화 필수
        this.id = id;
        this.orderNumber = orderNumber;
        this.customerId = customerId;
        this.totalAmount = totalAmount;
        this.status = status;
    }

    /**
     * of() 스태틱 팩토리 메서드 (Mapper 전용)
     *
     * <p>Entity 생성은 반드시 이 메서드를 통해서만 가능합니다.</p>
     */
    public static OrderJpaEntity of(
        Long id,
        String orderNumber,
        Long customerId,
        BigDecimal totalAmount,
        OrderStatus status,
        LocalDateTime createdAt,
        LocalDateTime updatedAt
    ) {
        return new OrderJpaEntity(
            id, orderNumber, customerId, totalAmount, status, createdAt, updatedAt
        );
    }

    // ===== Getters (Setter 제공 금지) =====

    public Long getId() { return id; }
    public String getOrderNumber() { return orderNumber; }
    public Long getCustomerId() { return customerId; }
    public BigDecimal getTotalAmount() { return totalAmount; }
    public OrderStatus getStatus() { return status; }
    // createdAt, updatedAt은 BaseAuditEntity에서 제공
}

2. SoftDeletableEntity 상속 (소프트 딜리트)

package com.ryuqq.adapter.out.persistence.product.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import com.ryuqq.adapter.out.persistence.common.entity.SoftDeletableEntity;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * ProductJpaEntity - 상품 JPA Entity (Soft Delete 지원)
 *
 * <p><strong>SoftDeletableEntity 상속:</strong></p>
 * <ul>
 *   <li>공통 감사 필드 상속: createdAt, updatedAt, deletedAt</li>
 *   <li>isDeleted(), isActive() 메서드 제공</li>
 * </ul>
 */
@Entity
@Table(name = "products")
public class ProductJpaEntity extends SoftDeletableEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "name", nullable = false, length = 200)
    private String name;

    @Column(name = "price", nullable = false, precision = 15, scale = 2)
    private BigDecimal price;

    @Column(name = "stock", nullable = false)
    private Integer stock;

    @Column(name = "category_id", nullable = false)
    private Long categoryId;  // ✅ Long FK

    protected ProductJpaEntity() {
    }

    private ProductJpaEntity(
        Long id,
        String name,
        BigDecimal price,
        Integer stock,
        Long categoryId,
        LocalDateTime createdAt,
        LocalDateTime updatedAt,
        LocalDateTime deletedAt  // ✅ Soft Delete
    ) {
        super(createdAt, updatedAt, deletedAt);
        this.id = id;
        this.name = name;
        this.price = price;
        this.stock = stock;
        this.categoryId = categoryId;
    }

    public static ProductJpaEntity of(
        Long id,
        String name,
        BigDecimal price,
        Integer stock,
        Long categoryId,
        LocalDateTime createdAt,
        LocalDateTime updatedAt,
        LocalDateTime deletedAt
    ) {
        return new ProductJpaEntity(
            id, name, price, stock, categoryId, createdAt, updatedAt, deletedAt
        );
    }

    // ===== Getters (Setter 제공 금지) =====

    public Long getId() { return id; }
    public String getName() { return name; }
    public BigDecimal getPrice() { return price; }
    public Integer getStock() { return stock; }
    public Long getCategoryId() { return categoryId; }
    // deletedAt, isDeleted(), isActive()는 SoftDeletableEntity에서 제공
}

3. 상속 없음 (시간/삭제 불필요)

package com.ryuqq.adapter.out.persistence.session.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.time.LocalDateTime;

/**
 * SessionTokenJpaEntity - 세션 토큰 JPA Entity (감사 정보 없음)
 *
 * <p>임시 데이터로 시간 정보 불필요</p>
 */
@Entity
@Table(name = "session_tokens")
public class SessionTokenJpaEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "token", nullable = false, length = 255)
    private String token;

    @Column(name = "user_id", nullable = false)
    private Long userId;

    @Column(name = "expires_at", nullable = false)
    private LocalDateTime expiresAt;

    protected SessionTokenJpaEntity() {
    }

    private SessionTokenJpaEntity(Long id, String token, Long userId, LocalDateTime expiresAt) {
        this.id = id;
        this.token = token;
        this.userId = userId;
        this.expiresAt = expiresAt;
    }

    public static SessionTokenJpaEntity of(Long id, String token, Long userId, LocalDateTime expiresAt) {
        return new SessionTokenJpaEntity(id, token, userId, expiresAt);
    }

    public Long getId() { return id; }
    public String getToken() { return token; }
    public Long getUserId() { return userId; }
    public LocalDateTime getExpiresAt() { return expiresAt; }
}

EntityMapper 코드 템플릿

1. BaseAuditEntity 상속 경우

package com.ryuqq.adapter.out.persistence.order.mapper;

import org.springframework.stereotype.Component;

import com.ryuqq.adapter.out.persistence.order.entity.OrderJpaEntity;
import com.ryuqq.domain.order.aggregate.Order;
import com.ryuqq.domain.order.vo.CustomerId;
import com.ryuqq.domain.order.vo.Money;
import com.ryuqq.domain.order.vo.OrderAudit;
import com.ryuqq.domain.order.vo.OrderId;
import com.ryuqq.domain.order.vo.OrderNumber;

/**
 * OrderJpaEntityMapper - Entity ↔ Domain 변환 Mapper
 *
 * <p><strong>변환 책임:</strong></p>
 * <ul>
 *   <li>Order → OrderJpaEntity (저장용)</li>
 *   <li>OrderJpaEntity → Order (조회용)</li>
 * </ul>
 *
 * <p><strong>핵심 규칙:</strong></p>
 * <ul>
 *   <li>비즈니스 로직 금지 (단순 변환만)</li>
 *   <li>시간 생성 금지 (Domain의 시간 전달)</li>
 *   <li>Entity.of() 메서드 사용</li>
 *   <li>Domain.reconstitute() 메서드 사용</li>
 * </ul>
 */
@Component
public class OrderJpaEntityMapper {

    /**
     * Domain → Entity 변환 (저장 시 사용)
     *
     * @param domain Order 도메인
     * @return OrderJpaEntity
     */
    public OrderJpaEntity toEntity(Order domain) {
        return OrderJpaEntity.of(
            domain.id() != null ? domain.id().value() : null,
            domain.orderNumber().value(),
            domain.customerId().value(),
            domain.totalAmount().value(),
            domain.status(),
            domain.audit().createdAt(),
            domain.audit().updatedAt()
        );
    }

    /**
     * Entity → Domain 변환 (조회 시 사용)
     *
     * @param entity OrderJpaEntity
     * @return Order 도메인
     */
    public Order toDomain(OrderJpaEntity entity) {
        return Order.reconstitute(
            OrderId.of(entity.getId()),
            OrderNumber.of(entity.getOrderNumber()),
            CustomerId.of(entity.getCustomerId()),
            Money.of(entity.getTotalAmount()),
            entity.getStatus(),
            OrderAudit.of(entity.getCreatedAt(), entity.getUpdatedAt())
        );
    }
}

2. SoftDeletableEntity 상속 경우

package com.ryuqq.adapter.out.persistence.product.mapper;

import org.springframework.stereotype.Component;

import com.ryuqq.adapter.out.persistence.product.entity.ProductJpaEntity;
import com.ryuqq.domain.product.aggregate.Product;
import com.ryuqq.domain.product.vo.CategoryId;
import com.ryuqq.domain.product.vo.Money;
import com.ryuqq.domain.product.vo.ProductAudit;
import com.ryuqq.domain.product.vo.ProductId;
import com.ryuqq.domain.product.vo.ProductName;
import com.ryuqq.domain.product.vo.Stock;

/**
 * ProductJpaEntityMapper - Entity ↔ Domain 변환 Mapper (Soft Delete 지원)
 */
@Component
public class ProductJpaEntityMapper {

    public ProductJpaEntity toEntity(Product domain) {
        return ProductJpaEntity.of(
            domain.id() != null ? domain.id().value() : null,
            domain.name().value(),
            domain.price().value(),
            domain.stock().value(),
            domain.categoryId().value(),
            domain.audit().createdAt(),
            domain.audit().updatedAt(),
            domain.audit().deletedAt()  // ✅ deletedAt 전달
        );
    }

    public Product toDomain(ProductJpaEntity entity) {
        return Product.reconstitute(
            ProductId.of(entity.getId()),
            ProductName.of(entity.getName()),
            Money.of(entity.getPrice()),
            Stock.of(entity.getStock()),
            CategoryId.of(entity.getCategoryId()),
            ProductAudit.of(
                entity.getCreatedAt(),
                entity.getUpdatedAt(),
                entity.getDeletedAt()  // ✅ deletedAt 전달
            )
        );
    }
}

상속 클래스 선택 기준

BaseAuditEntity vs SoftDeletableEntity

상황상속 클래스제공 필드
시간 정보만 필요BaseAuditEntitycreatedAt, updatedAt
소프트 딜리트 필요SoftDeletableEntitycreatedAt, updatedAt, deletedAt
시간/삭제 불필요상속 안 함없음

적용 케이스

┌─────────────────────────────────────────────────────────┐
│            상속 클래스 선택 Decision Tree               │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  소프트 딜리트 필요? ─── YES ─→ SoftDeletableEntity    │
│          │                                              │
│          NO                                             │
│          ↓                                              │
│  시간 정보 필요? ───── YES ─→ BaseAuditEntity          │
│          │                                              │
│          NO                                             │
│          ↓                                              │
│      상속 안 함                                         │
│                                                         │
└─────────────────────────────────────────────────────────┘

Zero-Tolerance 규칙

✅ MANDATORY (필수)

규칙설명적용 대상
Long FK 전략모든 외래키는 Long 타입Entity
*JpaEntity 네이밍Entity 클래스명 접미사Entity
protected 기본 생성자JPA 스펙 요구사항Entity
private 전체 필드 생성자무분별한 생성 방지Entity
public static of()팩토리 메서드만 노출Entity
Getter만 제공Setter 금지Entity
@ComponentSpring Bean 등록Mapper
*Mapper 네이밍Mapper 클래스명 접미사Mapper
toEntity()Domain → Entity 변환Mapper
toDomain()Entity → Domain 변환Mapper
Entity.of() 호출new 생성자 직접 호출 금지Mapper
Domain.reconstitute() 호출DB 재구성용 메서드 사용Mapper

❌ PROHIBITED (금지)

항목이유대안
@ManyToOneN+1 문제, 복잡도Long FK
@OneToManyN+1 문제, 복잡도Long FK
@OneToOneN+1 문제, 복잡도Long FK
@ManyToManyN+1 문제, 복잡도중간 테이블 Entity
Lombok명시적 코드 원칙Plain Java
Setter불변성 보장of() 메서드
new Entity()생성자 숨김Entity.of()
LocalDateTime.now() (Mapper)시간 생성은 Domain 책임Domain 시간 전달
Static 메서드 (Mapper)Spring Bean 필요Instance 메서드
비즈니스 로직 (Entity)Domain Layer 책임Domain으로 이동
비즈니스 로직 (Mapper)단순 변환만Domain으로 이동

패키지 구조

adapter-out/persistence-mysql/
└── src/main/java/com/ryuqq/adapter/out/persistence/
    ├── common/
    │   └── entity/
    │       ├── BaseAuditEntity.java
    │       └── SoftDeletableEntity.java
    └── {bc}/
        ├── entity/
        │   └── {Domain}JpaEntity.java
        └── mapper/
            └── {Domain}JpaEntityMapper.java

체크리스트 (Output Checklist)

JPA Entity 체크리스트

  • @Entity, @Table(name = "...") 어노테이션
  • *JpaEntity 네이밍 규칙
  • BaseAuditEntity 또는 SoftDeletableEntity 상속 검토
  • Long FK 전략 (JPA 관계 어노테이션 없음)
  • @Id @GeneratedValue(strategy = GenerationType.IDENTITY) PK 설정
  • @Column 어노테이션으로 컬럼 매핑
  • @Enumerated(EnumType.STRING) Enum 매핑
  • protected 기본 생성자 (JPA 스펙)
  • private 전체 필드 생성자 (무분별한 생성 방지)
  • public static of() 메서드 (Mapper 전용)
  • 전체 필드 생성자에서 super(createdAt, updatedAt) 호출 (상속 시)
  • Getter만 제공 (Setter 없음)
  • Lombok 사용 없음
  • 비즈니스 로직 없음
  • Javadoc 작성

EntityMapper 체크리스트

  • @Component 어노테이션
  • *Mapper 네이밍 규칙 (*JpaEntityMapper 권장)
  • public toEntity(Domain) 메서드 존재
    • Entity.of() 메서드 사용
    • Domain의 시간 필드 직접 전달
  • public toDomain(Entity) 메서드 존재
    • Domain.reconstitute() 메서드 사용
    • Value Object 재구성
  • Static 메서드 없음 (Instance 메서드만)
  • 비즈니스 로직 없음 (단순 변환만)
  • 검증 로직 없음 (Domain에서 검증)
  • LocalDateTime.now() 사용 없음
  • Lombok 사용 없음
  • Javadoc 작성

테스트 체크리스트

Entity ArchUnit 테스트 (16개 규칙)

  • Lombok 금지 (6개: @Data, @Getter, @Setter, @Builder, @AllArgsConstructor, @NoArgsConstructor)
  • JPA 관계 금지 (4개: @ManyToOne, @OneToMany, @OneToOne, @ManyToMany)
  • Setter 메서드 금지
  • 비즈니스 로직 메서드 금지
  • protected/public 기본 생성자 필수
  • private 전체 필드 생성자 필수
  • public static of() 메서드 필수
  • *JpaEntity 네이밍 필수

Mapper ArchUnit 테스트 (15개 규칙)

  • @Component 필수
  • Lombok 금지 (9개)
  • Static 변환 메서드 금지
  • 비즈니스 로직 메서드 금지
  • toEntity() 메서드 필수
  • toDomain() 메서드 필수
  • *Mapper 네이밍 필수

단위 테스트

  • toEntity(): Domain → Entity 정확히 변환
  • toDomain(): Entity → Domain 정확히 변환
  • Null 처리 확인 (신규 생성 시 id = null)
  • Value Object 재구성 확인

참조 문서

  • Entity Guide: docs/coding_convention/04-persistence-layer/mysql/entity/entity-guide.md
  • Entity ArchUnit: docs/coding_convention/04-persistence-layer/mysql/entity/entity-archunit.md
  • Entity Test Guide: docs/coding_convention/04-persistence-layer/mysql/entity/entity-test-guide.md
  • Mapper Guide: docs/coding_convention/04-persistence-layer/mysql/mapper/mapper-guide.md
  • Mapper ArchUnit: docs/coding_convention/04-persistence-layer/mysql/mapper/mapper-archunit.md
  • Mapper Test Guide: docs/coding_convention/04-persistence-layer/mysql/mapper/mapper-test-guide.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

レビュー

💬

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