Skip to the content.

Facade ArchUnit — 자동 검증 규칙

Facade의 아키텍처 규칙을 ArchUnit으로 자동 검증합니다.

빌드 시 규칙 위반이 감지되면 빌드 실패로 강제합니다.


1) 검증 규칙 요약

카테고리 규칙 심각도
기본 구조 @Component 어노테이션 필수 필수
기본 구조 *Facade 접미사 필수 필수
기본 구조 facade 패키지 위치 필수
기본 구조 final 클래스 금지 필수
메서드 persist* 메서드 네이밍 권장
금지 @Transactional 클래스 레벨 금지 필수
금지 @Service 금지 필수
금지 Lombok 금지 필수
금지 Port 의존 금지 필수
의존성 Application/Domain Layer만 의존 필수

2) ArchUnit 테스트 코드

파일 위치

application/src/test/java/
└─ com/ryuqq/application/architecture/facade/
   └─ FacadeArchTest.java

전체 코드

package com.ryuqq.application.architecture.facade;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.stereotype.Component;

import static com.tngtech.archunit.core.domain.JavaModifier.FINAL;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

/**
 * Facade ArchUnit 검증 테스트 (Zero-Tolerance)
 *
 * <p>핵심 철학: Facade는 여러 Manager 조합만, 비즈니스 로직 금지</p>
 */
@DisplayName("Facade ArchUnit Tests (Zero-Tolerance)")
@Tag("architecture")
@Tag("facade")
class FacadeArchTest {

    private static JavaClasses classes;
    private static boolean hasFacadeClasses;

    @BeforeAll
    static void setUp() {
        classes = new ClassFileImporter()
            .importPackages("com.ryuqq.application");

        hasFacadeClasses = classes.stream()
            .anyMatch(javaClass -> javaClass.getSimpleName().endsWith("Facade"));
    }

    // ==================== 기본 구조 규칙 ====================

    @Nested
    @DisplayName("기본 구조 규칙")
    class BasicStructureRules {

        @Test
        @DisplayName("[필수] Facade는 @Component 어노테이션을 가져야 한다")
        void facade_MustHaveComponentAnnotation() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = classes()
                .that().haveSimpleNameEndingWith("Facade")
                .should().beAnnotatedWith(Component.class)
                .because("Facade는 Spring Bean으로 등록되어야 합니다");

            rule.check(classes);
        }

        @Test
        @DisplayName("[필수] facade 패키지의 클래스는 'Facade' 접미사를 가져야 한다")
        void facade_MustHaveCorrectSuffix() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = classes()
                .that().resideInAPackage("..application..facade..")
                .and().areNotInterfaces()
                .and().areNotEnums()
                .should().haveSimpleNameEndingWith("Facade")
                .because("facade 패키지의 클래스는 'Facade' 접미사를 사용해야 합니다");

            rule.check(classes);
        }

        @Test
        @DisplayName("[필수] Facade는 ..application..facade.. 패키지에 위치해야 한다")
        void facade_MustBeInCorrectPackage() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = classes()
                .that().haveSimpleNameEndingWith("Facade")
                .should().resideInAPackage("..application..facade..")
                .because("Facade는 application.*.facade 패키지에 위치해야 합니다");

            rule.check(classes);
        }

        @Test
        @DisplayName("[필수] Facade는 final 클래스가 아니어야 한다")
        void facade_MustNotBeFinal() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = classes()
                .that().haveSimpleNameEndingWith("Facade")
                .should().notHaveModifier(FINAL)
                .because("Spring 프록시 생성을 위해 Facade가 final이 아니어야 합니다");

            rule.check(classes);
        }
    }

    // ==================== 메서드 규칙 ====================

    @Nested
    @DisplayName("메서드 규칙")
    class MethodRules {

        @Test
        @DisplayName("[권장] Facade의 public 메서드는 persist로 시작해야 한다")
        void facade_MethodsShouldStartWithPersist() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = methods()
                .that().areDeclaredInClassesThat().haveSimpleNameEndingWith("Facade")
                .and().arePublic()
                .and().doNotHaveFullName(".*<init>.*")
                .should().haveNameMatching("persist.*")
                .because("Facade 메서드는 persist*() 네이밍을 권장합니다 (save, create 금지)");

            rule.check(classes);
        }

        @Test
        @DisplayName("[필수] Facade의 @Transactional 메서드는 메서드 레벨에서만 허용")
        void facade_TransactionalOnMethodLevelOnly() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            // 메서드 레벨 @Transactional 존재 확인 (권장)
            ArchRule rule = methods()
                .that().areDeclaredInClassesThat().haveSimpleNameEndingWith("Facade")
                .and().areAnnotatedWith("org.springframework.transaction.annotation.Transactional")
                .should().bePublic()
                .because("Facade의 @Transactional은 public 메서드에서만 유효합니다");

            rule.check(classes);
        }
    }

    // ==================== 금지 규칙 (Zero-Tolerance) ====================

    @Nested
    @DisplayName("금지 규칙 (Zero-Tolerance)")
    class ProhibitionRules {

        @Test
        @DisplayName("[금지] Facade는 @Service 어노테이션을 가지지 않아야 한다")
        void facade_MustNotHaveServiceAnnotation() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = noClasses()
                .that().haveSimpleNameEndingWith("Facade")
                .should().beAnnotatedWith("org.springframework.stereotype.Service")
                .because("Facade는 @Service가 아닌 @Component를 사용해야 합니다");

            rule.check(classes);
        }

        @Test
        @DisplayName("[금지] Facade는 클래스 레벨 @Transactional을 가지지 않아야 한다")
        void facade_MustNotHaveClassLevelTransactional() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = noClasses()
                .that().haveSimpleNameEndingWith("Facade")
                .should().beAnnotatedWith("org.springframework.transaction.annotation.Transactional")
                .because("Facade는 클래스 레벨 @Transactional 금지. " +
                         "메서드 단위로 트랜잭션을 관리해야 합니다.");

            rule.check(classes);
        }

        @Test
        @DisplayName("[금지] Facade는 Lombok 어노테이션을 가지지 않아야 한다")
        void facade_MustNotUseLombok() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = noClasses()
                .that().haveSimpleNameEndingWith("Facade")
                .should().beAnnotatedWith("lombok.Data")
                .orShould().beAnnotatedWith("lombok.Builder")
                .orShould().beAnnotatedWith("lombok.Getter")
                .orShould().beAnnotatedWith("lombok.Setter")
                .orShould().beAnnotatedWith("lombok.AllArgsConstructor")
                .orShould().beAnnotatedWith("lombok.NoArgsConstructor")
                .orShould().beAnnotatedWith("lombok.RequiredArgsConstructor")
                .because("Facade는 Plain Java를 사용해야 합니다 (Lombok 금지)");

            rule.check(classes);
        }

        @Test
        @DisplayName("[금지] Facade는 Port 인터페이스를 직접 의존하지 않아야 한다")
        void facade_MustNotDependOnPorts() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = noClasses()
                .that().haveSimpleNameEndingWith("Facade")
                .should().dependOnClassesThat().haveNameMatching(".*Port")
                .because("Facade는 Port를 직접 주입받지 않습니다. " +
                         "TransactionManager를 통해 Port에 접근합니다.");

            rule.check(classes);
        }

        @Test
        @DisplayName("[금지] Facade는 Repository를 직접 의존하지 않아야 한다")
        void facade_MustNotDependOnRepositories() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = noClasses()
                .that().haveSimpleNameEndingWith("Facade")
                .should().dependOnClassesThat().haveNameMatching(".*Repository")
                .because("Facade는 Repository를 직접 주입받지 않습니다. " +
                         "TransactionManager를 통해 접근합니다.");

            rule.check(classes);
        }
    }

    // ==================== 의존성 규칙 ====================

    @Nested
    @DisplayName("의존성 규칙")
    class DependencyRules {

        @Test
        @DisplayName("[필수] Facade는 Application Layer와 Domain Layer만 의존해야 한다")
        void facade_MustOnlyDependOnApplicationAndDomainLayers() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = classes()
                .that().haveSimpleNameEndingWith("Facade")
                .should().onlyAccessClassesThat()
                .resideInAnyPackage(
                    "com.ryuqq.application..",
                    "com.ryuqq.domain..",
                    "org.springframework..",
                    "java..",
                    "jakarta.."
                )
                .because("Facade는 Application Layer와 Domain Layer만 의존해야 합니다");

            rule.check(classes);
        }

        @Test
        @DisplayName("[필수] Facade는 TransactionManager에 의존해야 한다")
        void facade_MustDependOnTransactionManager() {
            assumeTrue(hasFacadeClasses, "Facade 클래스가 없어 테스트를 스킵합니다");

            ArchRule rule = classes()
                .that().haveSimpleNameEndingWith("Facade")
                .should().dependOnClassesThat().haveNameMatching(".*TransactionManager")
                .because("Facade는 TransactionManager를 조합해야 합니다");

            rule.check(classes);
        }
    }
}

3) 규칙 상세 설명

기본 구조 규칙

규칙 설명 위반 시
@Component 필수 Spring Bean 등록 빌드 실패
*Facade 접미사 일관된 네이밍 빌드 실패
facade 패키지 올바른 위치 빌드 실패
final 금지 프록시 생성 빌드 실패

메서드 규칙

규칙 설명 위반 시
persist* 네이밍 save, create 금지 경고
@Transactional 메서드 레벨 클래스 레벨 금지 빌드 실패

금지 규칙

규칙 이유 위반 시
@Service 금지 @Component 사용 빌드 실패
클래스 레벨 @Transactional 금지 메서드 단위 관리 빌드 실패
Lombok 금지 Plain Java 빌드 실패
Port 직접 의존 금지 Manager 통해 접근 빌드 실패
Repository 직접 의존 금지 Manager 통해 접근 빌드 실패

의존성 규칙

규칙 이유 위반 시
Layer 제한 아키텍처 준수 빌드 실패
Manager 의존 필수 Facade 핵심 역할 빌드 실패

4) Facade vs TransactionManager 비교

구분 TransactionManager Facade
역할 단일 Port 트랜잭션 여러 Manager 조합
의존성 Persistence Port 1개 Manager 2개 이상
어노테이션 @Component @Component
트랜잭션 메서드 단위 @Transactional 메서드 단위 @Transactional
ArchUnit Port 의존 허용 Port 의존 금지

5) 관련 문서


작성자: Development Team 최종 수정일: 2025-12-04 버전: 1.0.0