Skip to the content.

Domain Event ArchUnit 검증 가이드

Domain Event의 아키텍처 규칙을 ArchUnit으로 자동 검증

📌 Zero-Tolerance: 모든 규칙 위반 시 빌드 실패


1) 검증 규칙 요약 (11개)

1.1 필수 구조 규칙 (4개)

규칙 검증 내용 설명
규칙 1 DomainEvent 인터페이스 구현 event 패키지 클래스는 DomainEvent 구현 필수
규칙 2 Record 타입 불변성 보장을 위해 record 사용
규칙 3 occurredAt 필드 Instant 타입 이벤트 발생 시각 필수
규칙 4 from() 팩토리 메서드 Aggregate로부터 생성하는 정적 메서드 필수

1.2 네이밍 및 패키지 규칙 (2개)

규칙 검증 내용 설명
규칙 5 과거형 네이밍 *edEvent 또는 불규칙 과거형 (*PaidEvent, *SentEvent)
규칙 6 패키지 위치 domain.[bc].event 패키지에 위치

1.3 금지 규칙 (4개)

규칙 검증 내용 설명
규칙 7 Lombok 금지 @Data, @Value, @Builder 등 사용 금지
규칙 8 JPA 금지 @Entity, @Table, @Embeddable 사용 금지
규칙 9 Spring 어노테이션 금지 @Component, @EventListener 사용 금지
규칙 10 Spring Framework 의존 금지 org.springframework.. 패키지 의존 금지

1.4 레이어 의존성 규칙 (1개)

규칙 검증 내용 설명
규칙 11 외부 레이어 의존 금지 application, adapter, persistence, bootstrap 패키지 의존 금지

2) ArchUnit 테스트 위치

domain/src/test/java/
└── com/ryuqq/domain/architecture/event/
    └── DomainEventArchTest.java    # Domain Event 아키텍처 검증 (11개 규칙)

테스트 태그

@Tag("architecture")
@Tag("domain")
@Tag("event")

실행 방법

# Domain Event ArchUnit 테스트만 실행
./gradlew :domain:test --tests "*DomainEventArchTest" -x jacocoTestCoverageVerification

# 전체 Domain 아키텍처 테스트 실행
./gradlew :domain:test --tests "*ArchTest"

# 태그 기반 실행
./gradlew :domain:test -PincludeTags=event

3) 규칙 상세 설명

3.1 필수 구조 규칙

규칙 1: DomainEvent 인터페이스 구현

// ✅ 올바른 예
public record OrderPlacedEvent(...) implements DomainEvent { ... }

// ❌ 잘못된 예
public record OrderPlacedEvent(...) { ... }  // DomainEvent 미구현

이유:

규칙 2: Record 타입

// ✅ 올바른 예
public record OrderPlacedEvent(
    OrderId orderId,
    Instant occurredAt
) implements DomainEvent { ... }

// ❌ 잘못된 예
public class OrderPlacedEvent implements DomainEvent {
    private OrderId orderId;  // Mutable 가능성
    // ...
}

이유:

규칙 3: occurredAt (Instant) 필드

// ✅ 올바른 예
public record OrderPlacedEvent(
    OrderId orderId,
    Instant occurredAt    // Instant 타입 필수
) implements DomainEvent { ... }

// ❌ 잘못된 예
public record OrderPlacedEvent(
    OrderId orderId
    // occurredAt 누락
) implements DomainEvent { ... }

이유:

규칙 4: from() 팩토리 메서드

// ✅ 올바른 예
public record OrderPlacedEvent(...) implements DomainEvent {
    public static OrderPlacedEvent from(Order order, Instant occurredAt) {
        return new OrderPlacedEvent(
            order.id(),
            order.status(),
            occurredAt
        );
    }
}

// ❌ 잘못된 예
public record OrderPlacedEvent(...) implements DomainEvent {
    // from() 메서드 없음 - 외부에서 직접 생성 유도
}

이유:

3.2 네이밍 및 패키지 규칙

규칙 5: 과거형 네이밍

// ✅ 올바른 예 (규칙 과거형 -ed)
OrderCreatedEvent      // created
OrderPlacedEvent       // placed
OrderCancelledEvent    // cancelled

// ✅ 올바른 예 (불규칙 과거형)
OrderPaidEvent         // pay → paid
OrderSentEvent         // send → sent
OrderSoldEvent         // sell → sold

// ❌ 잘못된 예
OrderEvent             // 과거형 아님
CreateOrderEvent       // 현재형
OrderCreateEvent       // 과거형 아님

이유:

규칙 6: 패키지 위치

// ✅ 올바른 예
package com.ryuqq.domain.order.event;

// ❌ 잘못된 예
package com.ryuqq.domain.order.aggregate;  // aggregate 패키지에 위치
package com.ryuqq.application.order.event; // Application Layer에 위치

이유:

3.3 금지 규칙

규칙 7: Lombok 금지

// ❌ 금지
@Data
@Value
@Builder
@Getter
public class OrderPlacedEvent { ... }

// ✅ 올바른 예: Pure Java Record
public record OrderPlacedEvent(...) implements DomainEvent { ... }

규칙 8: JPA 금지

// ❌ 금지
@Entity
@Embeddable
public class OrderPlacedEvent { ... }

// ✅ 올바른 예: Domain Event는 영속성과 무관
public record OrderPlacedEvent(...) implements DomainEvent { ... }

규칙 9: Spring 어노테이션 금지

// ❌ 금지
@Component
@EventListener
public class OrderPlacedEvent { ... }

// ✅ 올바른 예: EventListener는 Application Layer에 위치
// domain/order/event/
public record OrderPlacedEvent(...) implements DomainEvent { ... }

// application/order/listener/
@Component
public class OrderEventListener {
    @EventListener
    public void handle(OrderPlacedEvent event) { ... }
}

규칙 10: Spring Framework 의존 금지

// ❌ 금지
import org.springframework.context.ApplicationEvent;
public record OrderPlacedEvent(...) extends ApplicationEvent { ... }

// ✅ 올바른 예: Pure Java
import com.ryuqq.domain.common.event.DomainEvent;
public record OrderPlacedEvent(...) implements DomainEvent { ... }

3.4 레이어 의존성 규칙

규칙 11: 외부 레이어 의존 금지

// ❌ 금지: Application Layer 의존
package com.ryuqq.domain.order.event;
import com.ryuqq.application.order.dto.OrderDto;  // 금지!

// ❌ 금지: Adapter Layer 의존
import com.ryuqq.adapter.out.persistence.OrderEntity;  // 금지!

// ✅ 올바른 예: Domain Layer 내부만 참조
package com.ryuqq.domain.order.event;
import com.ryuqq.domain.order.vo.OrderId;
import com.ryuqq.domain.order.aggregate.Order;
import com.ryuqq.domain.common.event.DomainEvent;

이유:


4) 위반 사례 및 해결

사례 1: DomainEvent 인터페이스 미구현

❌ 오류: OrderPlacedEvent does not implement DomainEvent interface

// 수정 전
public record OrderPlacedEvent(OrderId orderId) { }

// 수정 후
public record OrderPlacedEvent(OrderId orderId, Instant occurredAt)
    implements DomainEvent { }

사례 2: from() 메서드 누락

❌ 오류: Class OrderPlacedEvent does not have a public static method named 'from'

// 수정: from() 메서드 추가
public static OrderPlacedEvent from(Order order, Instant occurredAt) {
    return new OrderPlacedEvent(order.id(), occurredAt);
}

사례 3: 과거형 네이밍 위반

❌ 오류: Event OrderCreateEvent should have past tense naming

// 수정 전
public record OrderCreateEvent(...) { }

// 수정 후
public record OrderCreatedEvent(...) { }

사례 4: 외부 레이어 의존

❌ 오류: Domain Event는 Application/Adapter 레이어에 의존하지 않아야 합니다

// 수정 전
import com.ryuqq.application.order.dto.OrderSummary;

// 수정 후: Domain 객체만 사용
import com.ryuqq.domain.order.vo.OrderId;

5) 체크리스트

Domain Event 생성 시

ArchUnit 테스트 실행 시


6) 관련 문서