DTO Record ArchUnit Guide
Application Layer DTO (Command, Query, Response)의 ArchUnit 검증 규칙
📌 핵심: DTO는 Record 타입, 순수 Java, 비즈니스 로직 금지
1) 핵심 원칙
DTO = 순수한 불변 데이터 전달 객체
| 규칙 |
설명 |
| Record 타입 필수 |
Command, Query, Response 모두 public record |
| 순수 Java |
Lombok, jakarta.validation 금지 |
| 비즈니스 로직 금지 |
데이터 전달만, 검증/계산 로직 없음 |
| 의존성 제로 |
Port, Repository 의존 금지 |
2) 테스트 위치
application/src/test/java/com/ryuqq/application/architecture/dto/
└── DtoRecordArchTest.java ← 17개 규칙, 통합 테스트
3) 검증 규칙 (17개)
Command 규칙 (3개)
| 규칙 |
설명 |
| Record 타입 |
dto.command 패키지의 *Command는 Record |
| 접미사 |
*Command 접미사 필수 |
| 패키지 위치 |
..application..dto.command.. 패키지 |
Query 규칙 (3개)
| 규칙 |
설명 |
| Record 타입 |
dto.query 패키지의 *Query는 Record |
| 접미사 |
*Query 접미사 필수 |
| 패키지 위치 |
..application..dto.query.. 패키지 |
Response 규칙 (3개)
| 규칙 |
설명 |
| Record 타입 |
dto.response 패키지의 *Response는 Record |
| 접미사 |
*Response 접미사 필수 |
| 패키지 위치 |
..application..dto.response.. 패키지 |
금지 규칙 (4개)
| 규칙 |
설명 |
| Lombok 금지 |
@Data, @Builder 등 Lombok 어노테이션 금지 |
| jakarta.validation 금지 |
@NotNull, @Min 등 검증 어노테이션 금지 |
| @Transactional 금지 |
DTO에서 트랜잭션 금지 |
| 비즈니스 메서드 금지 |
validate*, calculate* 등 금지 |
의존성 규칙 (3개)
| 규칙 |
설명 |
| Port 의존 금지 |
*Port 인터페이스 의존 금지 |
| Repository 의존 금지 |
*Repository 의존 금지 |
| Domain 반환 금지 |
Domain 객체를 반환하는 메서드 금지 |
ℹ️ Bundle 예외: application..dto.bundle.. 패키지는 Facade ↔ DomainFactory 사이에서 Domain 객체 묶음을 전달해야 하므로 ArchUnit의 “Domain 반환 금지” 검증 대상에서 제외됩니다. (자세한 내용은 Bundle Guide 참고)
기본 구조 규칙 (1개)
| 규칙 |
설명 |
| Public 접근 제어 |
DTO는 public 타입 필수 |
4) 클래스 존재 여부 체크
테스트는 해당 DTO 클래스가 존재하지 않으면 자동 스킵(SKIPPED)됩니다:
assumeTrue() vs return;: SKIP 방식을 사용하면 테스트 리포트에서 “스킵됨”으로 명확히 표시되어 개발자가 왜 테스트가 실행되지 않았는지 파악할 수 있습니다.
private static boolean hasCommandClasses;
private static boolean hasQueryClasses;
private static boolean hasResponseClasses;
private static boolean hasDtoClasses;
@BeforeAll
static void setUp() {
classes = new ClassFileImporter()
.importPackages("com.ryuqq.application");
hasCommandClasses = classes.stream()
.anyMatch(javaClass -> javaClass.getPackageName().contains(".dto.command")
&& javaClass.getSimpleName().endsWith("Command"));
hasQueryClasses = classes.stream()
.anyMatch(javaClass -> javaClass.getPackageName().contains(".dto.query")
&& javaClass.getSimpleName().endsWith("Query"));
hasResponseClasses = classes.stream()
.anyMatch(javaClass -> javaClass.getPackageName().contains(".dto.response")
&& javaClass.getSimpleName().endsWith("Response"));
hasDtoClasses = hasCommandClasses || hasQueryClasses || hasResponseClasses;
}
@Test
void command_MustBeRecord() {
assumeTrue(hasCommandClasses, "Command 클래스가 없어 테스트를 스킵합니다");
// ... 실제 테스트 로직
}
5) 테스트 실행
# DTO Record 테스트만
./gradlew test --tests "*DtoRecordArchTest"
# 전체 ArchUnit 테스트
./gradlew test --tests "*ArchTest"
# 특정 테스트
./gradlew test --tests "*DtoRecordArchTest.command_MustBeRecord"
6) 위반 예시 및 해결
위반 1: Class 타입 사용
// ❌ Bad
public class CreateOrderCommand {
private Long customerId;
}
// ✅ Good
public record CreateOrderCommand(
Long customerId
) {}
위반 2: Lombok 사용
// ❌ Bad
@Data
public class CreateOrderCommand { ... }
// ✅ Good
public record CreateOrderCommand(...) {}
위반 3: jakarta.validation 사용
// ❌ Bad (Application Layer DTO)
public record CreateOrderCommand(
@NotNull Long customerId
) {}
// ✅ Good (순수 Record, 검증은 REST API Layer에서)
public record CreateOrderCommand(
Long customerId
) {}
7) 관련 문서
작성자: Development Team
최종 수정일: 2025-12-04
버전: 2.0.0 (통합 및 간소화)