1. κ°μ
λ μ΄μ΄λ μν€ν μ², ν΄λ¦° μν€ν μ², ν₯μ¬κ³ λ μν€ν μ² λ± λ§μ μν€ν μ²κ° μ‘΄μ¬νκ³ μν€ν μ² μμ΄ κ°λ°νλ κ°λ°μλ κ±°μ μλ€.
κ·Έλ°λ° μ΄μ²λΌ μ ν΄μ§ μν€ν μ² κ·μΉμ΄ μ€μ νλ‘μ νΈ μ§ν κ³Όμ μμ μ μ§μΌμ§λ κ²μ μ΄λ»κ² 보μ₯ν μ μμκΉ?
κΈνκ² κ°λ°νλ κ³Όμ μμ μ΄λ¬ν κ·μΉμ μ΄κΈ°λ μ½λκ° μ겨λ¬μ λ, κ°λ°μλ€μ΄ μΌμΌμ΄ μ΄λ₯Ό μ‘μλ΄κ³ κ³ μΉλ κ²μ κ·Έ μμ²΄λ‘ λΉμ©μ΄λ€.
κ·Έλ¦¬κ³ μ΄λ¬ν κ·μΉμ΄ νλκ° μλλΌ μ¬λ¬ κ° μ‘΄μ¬νλ€λ©΄ κ°λ°μλ€μ΄ μ€μν κ°λ₯μ±μ ν¨μ¬ λμμ§ κ²μ΄λ€.
κ·Έλ¬λ―λ‘ κ°λ°μλ€μ΄ μ΄λ¬ν κ·μΉμ μλ°°λλ μ½λλ₯Ό μμ±νμ§ μλλ‘ κ°μ ν μ μλ μλ¨μ΄ νμνλ€.
μμ‘΄μ±μ κ°μ νκΈ° μν΄μ Spring Multi Module μ μ¬μ©ν μ μλ€.
λ μ΄μ΄λ³ νΉμ μμλ³λ‘ λͺ¨λμ μμ±νκ³ μλμ κ°μ΄ μμ‘΄μ±μ κ±Έμ΄μ£Όλ©΄ λλ€.
dependencies {
implementation project(':listener:application')
implementation project(':listener:domain')
...
}
νμ§λ§ Springμμλ Beanμ ν΅ν΄ κ°μ²΄ κ°μ μμ‘΄μ±μ κ΄λ¦¬νκ³ μ£Όμ νκΈ° λλ¬Έμ, λ©ν°λͺ¨λ ꡬ쑰λ₯Ό μ¬μ©ν κ²½μ° μλμ κ°μ΄ Beanμ λν μμ‘΄μ±λ μΆκ°λ‘ κ΄λ¦¬ν΄μ€μΌ νλ€.
@Configuration
@ComponentScan(basePackages = "com.example.repository")
public class ServiceConfig {}
Bean λν μμ‘΄μ±μ κ°μ ν μ μκΈ΄ νμ§λ§ κ΄λ¦¬ ν¬μΈνΈκ° λμ΄λκ² λλ€.
λμ κ²½μ°, λͺ¨λλ‘ μν€ν μ²λ₯Ό κ°μ ν νκ³ μΆμ§ μμλ κ°μ₯ ν° μ΄μ λ λμ€κ° λ무 κΉμ΄μ Έμ μ΄λ€.
src -> main -> java -> com -> example -> ...
κ°λ°νλ©° λ΄κ° μ΄λ νμΌμ λ³΄κ³ μλμ§λ ν·κ°λ¦΄ μ λλ‘ λμ€κ° κΉκ³ λ§μμ Έ λ²λ¦°λ€.
κ·Έλμ ArchUnit ν μ€νΈ + ν¨ν€μ§ ꡬ쑰 μν€ν μ²λ‘ μ€κ³νκΈ°λ‘ κ²°μ νλ€.
2. ArchUnit λΌμ΄λΈλ¬λ¦¬
ArchUnit λΌμ΄λΈλ¬λ¦¬λ μν€ν μ² ν μ€νΈλ₯Ό μν λΌμ΄λΈλ¬λ¦¬μ΄λ€.
λ¨μ ν μ€νΈμ κ°μ λ°©λ²μΌλ‘ μμ±λλ©° μν€ν μ²μ ν¨ν€μ§, ν΄λμ€, κ³μΈ΅, μ¬λΌμ΄μ€ κ°μ μμ‘΄μ±μ νμΈνκ³ κ°μ²΄ κ°μ μ°Έμ‘°λ₯Ό νμΈν μ μκ² λμμ€λ€.
- build.gradle μμ‘΄μ± μΆκ°
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
ArchUnit λΌμ΄λΈλ¬λ¦¬μ κ΅μ₯ν λ§μ ν¨μλ€μ μ 곡ν΄μ€λ€. μ 체μ μΈ κ΅¬μ±(μ§μμ)λ§ μλ©΄ ν¨μλͺ μ λ³΄κ³ μΆμΈ‘ν΄μ μμ±ν μ μμ κ²μ΄λ€.
- ν μ€νΈν ν΄λμ€ μ§ν© λΆλ¬μ€κΈ°
private final JavaClasses importedClasses = new ClassFileImporter()
.importPackages("com.example"); // νλ‘μ νΈ λ£¨νΈ ν¨ν€μ§
- λμ λ° μ‘°κ±΄ μ€μ
ArchRuleDefinition.[λμ λ° μ‘°κ±΄]()
// μμ
// μ΄ν 쑰건λ€μ λν΄ λͺ¨λ ν΄λμ€κ° λ§μ‘±ν΄μΌ ν¨
ArchRuleDefinition.classes()
// μ΄ν 쑰건λ€μ λν΄ λͺ¨λ ν΄λμ€κ° λ§μ‘±νμ§ μμμΌ ν¨
ArchRuleDefinition.noClasses()
// μ΄ν 쑰건λ€μ λν΄ λͺ¨λ λ©μλκ° λ§μ‘±ν΄μΌ ν¨
ArchRuleDefinition.methods()
// μ΄ν 쑰건λ€μ λν΄ λͺ¨λ ν΄λμ€κ° λ§μ‘±νμ§ μμμΌ ν¨
ArchRuleDefinition.noMethods()
// μ΄ν 쑰건λ€μ λν΄ λͺ¨λ νλκ° λ§μ‘±ν΄μΌ ν¨
ArchRuleDefinition.fields()
// μ΄ν 쑰건λ€μ λν΄ λͺ¨λ νλκ° λ§μ‘±νμ§ μμμΌ ν¨
ArchRuleDefinition.noFields()
- λμ νν°λ§
ArchRuleDefinition.classes()
.that().[λμ νν°λ§]
// μμ
.that().resideInAPackage("..application..")
.that().resideInAnyPackage("com.example..api..", "com.example..domain..")
- κ·μΉ μ§μ
ArchRuleDefinition.classes()
.that().[λμ νν°λ§]
.should().[κ·μΉ μ§μ ];
// μμ
.should().dependOnClassesThat().resideInAnyPackage(
"com.example..api..",
);
- κ·μΉ κ²μ¬
ArchRule rule = ArchRuleDefinition.classes()
.that().[λμ νν°λ§]
.should().[κ·μΉ μ§μ ];
rule.check([ν΄λμ€ μ§ν©]);
3. ArchTest Code μμ
λ΄κ° μμ±ν ArchTest λͺ κ°μ§λ§ κ°μ Έμλ΄€λ€.
- Application μμμμ Infrastructure, Persistence μμμ μ°Έμ‘°ν μ μμμ κ°μ
@Test
@DisplayName("application μμμ [infrastructure, persistence] μμμ μ°Έμ‘°ν μ μλ€.")
void application_area_test() {
ArchRule rule = noClasses()
.that().resideInAPackage("..application..")
// μ°Έμ‘° κΈμ§ ν¨ν€μ§ μ§μ
.should().dependOnClassesThat().resideInAnyPackage(
"com.example..infrastructure..",
"com.example..persistence.."
);
rule.check(importedClasses);
}
- Domain μμμμ Infrastructure, Persistence, Application μμμ μ°Έμ‘°ν μ μμμ κ°μ
@Test
@DisplayName("domain μμμ [infrastructure, persistence, application] μμμ μ°Έμ‘°ν μ μλ€.")
void domain_area_test() {
ArchRule rule = noClasses()
.that().resideInAPackage("..domain..")
// μ°Έμ‘° κΈμ§ ν¨ν€μ§ μ§μ
.should().dependOnClassesThat().resideInAnyPackage(
"com.example..infrastructure..",
"com.example..persistence..",
"com.example..application.."
);
rule.check(importedClasses);
}
μν€ν μ² μμμ λμ΄ μ»¨λ²€μ λν κ²μ¦ν μ μλ€.
μλ₯Ό λ€μ΄, μν°ν°μ κΈ°λ³Έ μμ±μμ Accss Levelμ Protectedλ‘ κ°μ νκ³ μΆμ μ μλ€.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
μλμ κ°μ΄ Custom 쑰건λ λ§λ€μ΄ μ¬μ©ν μ μλ€.
@Test
@DisplayName("Entity ν΄λμ€μ κΈ°λ³Έ μμ±μλ λ°λμ protectedμ¬μΌ νλ€.")
void entity_default_constructor_should_be_protected() {
ArchCondition<JavaConstructor> defaultConstructorCondition = new ArchCondition<>(
"have protected modifier") {
@Override
public void check(JavaConstructor constructor, ConditionEvents events) {
int modifiers = constructor.reflect().getModifiers();
if (!Modifier.isProtected(modifiers)) {
String message = String.format(
"Class %sμ κΈ°λ³Έ μμ±μλ protectedκ° μλ.",
constructor.getOwner().getFullName()
);
events.add(SimpleConditionEvent.violated(constructor, message));
}
}
};
ArchRule rule = constructors()
.that().areDeclaredInClassesThat().haveSimpleNameEndingWith("Entity")
.and().haveRawParameterTypes(new Class<?>[0]) // κΈ°λ³Έ μμ±μ (νλΌλ―Έν° μμ) λͺ
μ
.should(defaultConstructorCondition);
rule.check(importedClasses);
}