ν…ŒμŠ€νŠΈ μ½”λ“œλ‘œ μ•„ν‚€ν…μ²˜ & μ»¨λ²€μ…˜ 을 κ²€μ¦ν•΄λ³΄μž

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);
    }