domain-expert
Spring DDD Domain Layer 전문가. Aggregate 설계, Law of Demeter, Tell Don't Ask 원칙을 준수하는 Domain 객체를 생성합니다. Lombok 금지, Pure Java, Invariant 보호를 보장합니다.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: "domain-expert"
description: "Spring DDD Domain Layer 전문가. Aggregate 설계, Law of Demeter, Tell Don't Ask 원칙을 준수하는 Domain 객체를 생성합니다. Lombok 금지, Pure Java, Invariant 보호를 보장합니다."
---
# Spring DDD Domain Layer Expert
Domain Layer 전문가 Skill입니다. 비즈니스 로직의 핵심인 Domain 객체 설계를 담당합니다.
## 전문 분야
1. **Aggregate 설계**: Aggregate Root, Entity, ValueObject
2. **Law of Demeter**: Tell Don't Ask, Getter 체이닝 금지
3. **Domain 캡슐화**: Invariant 보호, 행동 메서드
4. **DDD 패턴**: Factory, Repository Interface, Domain Service
## 사용 시점
- Domain Aggregate 생성 또는 수정
- Entity, ValueObject 설계
- Domain 비즈니스 메서드 구현
- Domain Event 처리
## 핵심 규칙
### 1. Aggregate 설계 원칙
**위치**: `docs/coding_convention/02-domain-layer/aggregate-design/`
**Aggregate Root 예시**:
```java
public class Order { // Aggregate Root
private Long id;
private String orderNumber;
private OrderStatus status;
private Long customerId;
private List<OrderItem> items; // Entity
private Address shippingAddress; // ValueObject
private LocalDateTime createdAt;
// ✅ Constructor: Invariant 보호
public Order(
String orderNumber,
Long customerId,
List<OrderItem> items,
Address shippingAddress
) {
validateOrderNumber(orderNumber);
validateCustomerId(customerId);
validateItems(items);
validateAddress(shippingAddress);
this.orderNumber = orderNumber;
this.customerId = customerId;
this.items = new ArrayList<>(items); // Defensive copy
this.shippingAddress = shippingAddress;
this.status = OrderStatus.PLACED;
this.createdAt = LocalDateTime.now();
}
// ✅ 비즈니스 메서드: Tell, Don't Ask
public void confirm() {
if (status != OrderStatus.PLACED) {
throw new OrderStatusException(
"주문 확인은 PLACED 상태에서만 가능합니다"
);
}
this.status = OrderStatus.CONFIRMED;
}
public void cancel() {
if (status == OrderStatus.DELIVERED) {
throw new OrderStatusException(
"배송 완료된 주문은 취소할 수 없습니다"
);
}
this.status = OrderStatus.CANCELLED;
}
// ❌ Getter 체이닝 방지용 메서드
public String getCustomerName() {
// ❌ return customer.getName(); // Getter 체이닝
// ✅ Domain Service 또는 Application Layer에서 조합
throw new UnsupportedOperationException(
"Customer 정보는 Application Layer에서 조합하세요"
);
}
// ✅ Pure Java Getter
public Long getId() { return id; }
public String getOrderNumber() { return orderNumber; }
public OrderStatus getStatus() { return status; }
public Long getCustomerId() { return customerId; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
public Address getShippingAddress() { return shippingAddress; }
public LocalDateTime getCreatedAt() { return createdAt; }
// Private validation methods
private void validateOrderNumber(String orderNumber) {
if (orderNumber == null || orderNumber.isBlank()) {
throw new IllegalArgumentException("주문 번호는 필수입니다");
}
}
private void validateCustomerId(Long customerId) {
if (customerId == null || customerId <= 0) {
throw new IllegalArgumentException("유효하지 않은 고객 ID입니다");
}
}
private void validateItems(List<OrderItem> items) {
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("주문 항목은 1개 이상이어야 합니다");
}
}
private void validateAddress(Address address) {
if (address == null) {
throw new IllegalArgumentException("배송 주소는 필수입니다");
}
}
}
```
**규칙**:
- ✅ **Lombok 절대 금지**: `@Data`, `@Builder`, `@Getter`, `@Setter` 사용 불가
- ✅ **Pure Java**: 모든 getter/setter 직접 작성
- ✅ **Invariant 보호**: Constructor에서 모든 유효성 검증
- ✅ **Tell, Don't Ask**: 상태 묻지 말고 행동 시키기
- ✅ **Defensive Copy**: 컬렉션은 방어적 복사
- ✅ **Unmodifiable Collection**: 외부에 컬렉션 반환 시 불변
### 2. Law of Demeter
**위치**: `docs/coding_convention/02-domain-layer/law-of-demeter/`
**Getter 체이닝 금지**:
```java
// ❌ Bad: Getter 체이닝
String zip = order.getCustomer().getAddress().getZip();
// ✅ Good: Tell Don't Ask
String zip = order.getCustomerZipCode();
// Order 클래스에 추가
public String getCustomerZipCode() {
// Application Layer에서 Customer 정보를 조합하여 전달
throw new UnsupportedOperationException(
"Customer 정보는 Application Layer에서 조합하세요"
);
}
```
**Tell Don't Ask 원칙**:
```java
// ❌ Bad: 상태를 물어서 외부에서 판단
if (order.getStatus() == OrderStatus.PLACED) {
order.setStatus(OrderStatus.CONFIRMED);
}
// ✅ Good: 행동을 시킴
order.confirm();
// Domain 클래스에 추가
public void confirm() {
if (status != OrderStatus.PLACED) {
throw new OrderStatusException(
"주문 확인은 PLACED 상태에서만 가능합니다"
);
}
this.status = OrderStatus.CONFIRMED;
}
```
### 3. Entity 설계
**Entity 예시** (`OrderItem.java`):
```java
public class OrderItem { // Entity (Aggregate 내부)
private Long id;
private Long productId;
private int quantity;
private BigDecimal price;
// Constructor
public OrderItem(Long productId, int quantity, BigDecimal price) {
validateProductId(productId);
validateQuantity(quantity);
validatePrice(price);
this.productId = productId;
this.quantity = quantity;
this.price = price;
}
// 비즈니스 메서드
public void changeQuantity(int newQuantity) {
validateQuantity(newQuantity);
this.quantity = newQuantity;
}
public BigDecimal calculateTotal() {
return price.multiply(BigDecimal.valueOf(quantity));
}
// Pure Java Getters
public Long getId() { return id; }
public Long getProductId() { return productId; }
public int getQuantity() { return quantity; }
public BigDecimal getPrice() { return price; }
// Private validation
private void validateProductId(Long productId) {
if (productId == null || productId <= 0) {
throw new IllegalArgumentException("유효하지 않은 상품 ID입니다");
}
}
private void validateQuantity(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("수량은 1개 이상이어야 합니다");
}
}
private void validatePrice(BigDecimal price) {
if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("가격은 0보다 커야 합니다");
}
}
}
```
### 4. ValueObject 설계
**ValueObject 예시** (`Address.java`):
```java
public record Address( // ✅ Java 21 Record 패턴
String zipCode,
String street,
String city,
String state
) {
// Compact Constructor: Validation
public Address {
Objects.requireNonNull(zipCode, "zipCode must not be null");
Objects.requireNonNull(street, "street must not be null");
Objects.requireNonNull(city, "city must not be null");
Objects.requireNonNull(state, "state must not be null");
if (zipCode.isBlank()) {
throw new IllegalArgumentException("우편번호는 필수입니다");
}
if (street.isBlank()) {
throw new IllegalArgumentException("주소는 필수입니다");
}
}
// 비즈니스 메서드
public String fullAddress() {
return String.format("%s, %s, %s %s",
street, city, state, zipCode);
}
}
```
**규칙**:
- ✅ **Record 패턴** 사용 (Java 21)
- ✅ **Immutable**: 모든 필드 final
- ✅ **Compact Constructor**: Validation 포함
- ✅ **Value Equality**: Record가 자동 제공
### 5. Domain Exception
**Domain Exception 예시**:
```java
public abstract class DomainException extends RuntimeException {
private final String errorCode;
private final HttpStatus httpStatus;
protected DomainException(
String errorCode,
String message,
HttpStatus httpStatus
) {
super(message);
this.errorCode = errorCode;
this.httpStatus = httpStatus;
}
public String getErrorCode() { return errorCode; }
public HttpStatus getHttpStatus() { return httpStatus; }
}
public class OrderStatusException extends DomainException {
public OrderStatusException(String message) {
super("ORDER_STATUS_INVALID", message, HttpStatus.BAD_REQUEST);
}
}
public class OrderNotFoundException extends DomainException {
public OrderNotFoundException(Long orderId) {
super(
"ORDER_NOT_FOUND",
"주문을 찾을 수 없습니다: " + orderId,
HttpStatus.NOT_FOUND
);
}
}
```
## 패키지 구조
```
domain/
├── order/
│ ├── Order.java (Aggregate Root)
│ ├── OrderItem.java (Entity)
│ ├── OrderStatus.java (Enum)
│ ├── Address.java (ValueObject)
│ ├── OrderId.java (ValueObject)
│ ├── OrderFactory.java (Factory)
│ └── OrderRepository.java (Port/Out Interface)
├── customer/
│ ├── Customer.java
│ └── ...
└── exception/
├── DomainException.java
├── OrderStatusException.java
└── OrderNotFoundException.java
```
## Zero-Tolerance 체크리스트
Domain Layer 코드 작성 후 반드시 확인:
- [ ] **Lombok 절대 금지**: `@Data`, `@Builder`, `@Getter`, `@Setter` 없음
- [ ] **Getter 체이닝 금지**: `.get().get()` 패턴 없음
- [ ] **Tell Don't Ask**: 상태 묻지 말고 행동 시키기
- [ ] **Invariant 보호**: Constructor에서 모든 검증
- [ ] **Defensive Copy**: 컬렉션 방어적 복사
- [ ] **Unmodifiable**: 외부 반환 컬렉션은 불변
- [ ] **Pure Java**: 모든 getter/setter 직접 작성
- [ ] **비즈니스 메서드**: Aggregate에 행동 메서드 포함
## 추가 리소스
상세 규칙:
```bash
cat .claude/skills/domain-expert/REFERENCE.md
```
검증 스크립트:
```bash
bash .claude/skills/domain-expert/scripts/validate-domain.sh [file_path]
```
## 참고 문서
- `docs/coding_convention/02-domain-layer/`
- `docs/coding_convention/02-domain-layer/aggregate-design/00_domain-object-creation-guide.md`
- `docs/coding_convention/02-domain-layer/law-of-demeter/01_getter-chaining-prohibition.md`
- `docs/coding_convention/02-domain-layer/law-of-demeter/03_domain-encapsulation.md`