ArchUnit 을 이용해서 컨벤션을 지켜 코드 품질을 높여보자
1. 도입 배경
기업 내 개발은 개인 단위보다는 팀 단위로 진행되며, 그에 따라 공통된 개발 규칙(컨벤션)을 정하고 이를 지속적으로 유지하는 것이 매우 중요합니다. 하지만 현실에서는 다음과 같은 문제가 자주 발생합니다.
컨벤션이 문서로만 정리되어 있고, 실제로는 잘 지켜지지 않음
코드 리뷰에서 반복적으로 동일한 피드백이 발생함
시간이 흐를수록 일관성은 무너지고, 시스템 전체 품질이 하락함
이러한 문제를 해결하기 위해 우리는 컨벤션을 “자동으로 검증”하는 방법을 고민했고, 그 해답 중 하나로 ArchUnit을 도입하게 되었습니다.
2. ArchUnit이란?
ArchUnit은 Java (또는 Kotlin) 기반 프로젝트에서 아키텍처 규칙을 테스트 코드로 작성하고 검증할 수 있게 해주는 도구입니다.
단순한 코드 컨벤션이 아닌, 구조적 설계 규칙을 강제함
테스트 코드처럼 작성하여 CI 파이프라인에도 통합 가능
레이어 구조, 의존성 방향성, 어노테이션 적용 여부 등 다양한 규칙 정의 가능
대표적인 적용 예시
DTO 클래스는 반드시 @Schema 어노테이션을 가져야 한다
dto 패키지는 service 패키지를 참조하면 안 된다
entity 클래스는 특정 패키지에서만 선언되어야 한다
순환 참조가 발생하지 않아야 한다
3. 규칙 정의 사례
3.1. Request/Response DTO 필드에는 반드시 @Schema 어노테이션을 적용해야 한다
목적
Swagger 문서 자동화를 위한 메타 정보 누락을 방지하고, 문서의 일관성을 유지하기 위함
규칙 설명
request, response 패키지에 위치한 모든 클래스 대상
모든 필드는
@Schema어노테이션을 반드시 포함해야 함
위반 예시
public class UserResponseDto {
@Schema(description = "사용자 ID")
private Long id;
private String username; // ← @Schema 누락 → 테스트 실패
}
ArchUnit 구현 코드 (Kotlin)
@AnalyzeClasses(packages = ["org.example"])
class SchemaAnnotationTest {
@Test
fun allDtoFieldsMustHaveSchemaAnnotation() {
val classes = ClassFileImporter().importPackages("org.example")
val rule = classes()
.that().resideInAnyPackage("..request..", "..response..")
.should(object : ArchCondition<JavaClass>("all fields annotated with @Schema") {
override fun check(clazz: JavaClass, events: ConditionEvents) {
if (!clazz.name.contains("Request", true) && !clazz.name.contains("Response", true)) return
clazz.fields.forEach { field ->
val result = if (field.isAnnotatedWith(Schema::class.java)) {
satisfied(field, "Field ${field.name} in ${clazz.name} has @Schema")
} else {
violated(field, "Field ${field.name} in ${clazz.name} is missing @Schema")
}
events.add(result)
}
}
})
rule.check(classes)
}
}
3.2. 감사 로그 테이블 클래스명은 ‘_log’로 끝나야 한다
목적
Audit 데이터의 테이블 네이밍을 통일하여 관리 및 추적 용이성 확보
규칙 설명
@AuditTable("테이블명") 어노테이션이 붙은 클래스 대상
테이블명은 반드시 _log로 끝나야 함
위반 예시
@AuditTable("user_history") // ← 위반: '_log'가 아님
public class UserHistoryEntity { }
ArchUnit 구현 코드 (Kotlin)
class AuditTableNamingTest {
@Test
fun auditTableNameMustEndWithLog() {
val classes = ClassFileImporter().importPackages("org.example")
val rule = classes()
.that().areAnnotatedWith(AuditTable::class.java)
.should(object : ArchCondition<JavaClass>("@AuditTable name ends with '_log'") {
override fun check(clazz: JavaClass, events: ConditionEvents) {
val auditTable = clazz.getAnnotationOfType(AuditTable::class.java)
val tableName = auditTable.value
val result = if (tableName.endsWith("_log")) {
satisfied(clazz, "Table name '$tableName' is valid")
} else {
violated(clazz, "Table name '$tableName' must end with '_log'")
}
events.add(result)
}
})
rule.check(classes)
}
}
5. 기대 효과 및 느낀점
명문화된 규칙이 테스트 코드로 전환되어 유지보수가 쉬워짐
코드 리뷰 단계에서 발생하던 불필요한 피드백 감소
새로운 팀원이 입사하더라도 자동 테스트를 통해 규칙을 학습할 수 있음
추상적이고 흐릿했던 “팀 컨벤션”을 검증 가능한 형태로 명확하게 제어 가능

