Java & Spring / / 2023. 5. 26. 14:39

[Java Test] 6. ArchUnit 아키텍처 테스트

728x90
 

 


 

Contents

     

     

    아키텍처는 어플리케이션의 패키지 구조, 클래스 관계, 메소드 참조 등의 여러 형태를 가질 수 있다.

     

    아래 글에 아키텍처를 소개해야하는 이유와 ArchUnit에 대한 내용이 실려있다.

    https://blogs.oracle.com/javamagazine/unit-test-your-architecture-with-archunit

     

    Unit Test Your Architecture with ArchUnit

    Discover architectural defects at build time.

    blogs.oracle.com

     

    1. ArchUnit

    ArchUnit은 애플리케이션의 아키텍처를 테스트 할 수 있는 오픈 소스 라이브러리이다. 패키지, 클래스, 레이어, 슬라이스 간의 의존성을 확인할 수 있다.

     

    - ArchUnit 가이드

    https://www.archunit.org/userguide/html/000_Index.html

     

    ArchUnit User Guide

    ArchUnit is a free, simple and extensible library for checking the architecture of your Java code. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing giv

    www.archunit.org

     

    아키텍처 테스트 유즈 케이스

    • 특정 패키지 사용 확인 : A 패키지가 B (또는 C, D) 패키지에서만 사용 되고 있는지 확인 가능.
      • 상호 참조가 생기는지 확인 가능 -> 상호참조 발생 시 코드 파악이 어렵기 떄문에 최대한 피해야한다
    • *Serivce라는 이름의 클래스들이 *Controller 또는 *Service라는 이름의 클래스에서만 참조하고 있는지 확인.
    • *Service라는 이름의 클래스들이 ..service.. 라는 패키지에 들어있는지 확인.
      • Repository나 다른 패키지에 들어있지 않은지. 맞는 패키지에 들어가 있는지
    • A라는 애노테이션을 선언한 메소드만 특정 패키지 또는 특정 애노테이션을 가진 클래스를 호출하고 있는지 확인.
    • 특정한 스타일의 아키텍처를 따르고 있는지 확인.
      • 계층형, Onion 아키텍처 등

     

    ArchUnit 설치

    JUnit 5용 ArchUnit을 설치한다

    - Maven

    <dependency>
        <groupId>com.tngtech.archunit</groupId>
        <artifactId>archunit-junit5-engine</artifactId>
        <version>1.0.1</version>
        <scope>test</scope>
    </dependency>

    - Gradle

    testImplementation group: 'com.tngtech.archunit', name: 'archunit-junit5-engine', version: '1.0.1'

     

    ArchUnit 사용법

    (1) 테스트 메서드 작성

    1. 자바 클래스 읽기 : 특정 패키지에 해당하는 클래스를 (바이트코드를 통해) 읽어들이고
    2. 규칙(Rule) 정의 : 확인할 규칙을 정의
    3. 확인 : 읽어들인 클래스들이 그 규칙을 잘 따르는지 확인한다.

    @Test
    public void Services_should_only_be_accessed_by_Controllers() {
    	// 1. 클래스 읽기 
        JavaClasses importedClasses = new ClassFileImporter().importPackages("com.mycompany.myapp");
    
    	// 2. 룰 정의 
        ArchRule myRule = classes()
            .that().resideInAPackage("..service..")
            .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
    
    	// 3. 확인
        myRule.check(importedClasses);
    }

     

    (2) JUnit 5 확장팩 사용

    어노테이션으로 사용도 가능하다.

    - @AnalyzeClasses: 클래스를 읽어들여서 확인할 패키지 설정

    - @ArchTest: 확인할 규칙 정의

    @RunWith(ArchUnitRunner.class)
    @AnalyzeClasses(
        packages = "com.company.app", 
        importOptions = 
            ImportOption.DoNotIncludeTests.class
    )
    public class ArchitectureRulesTest {
    
        @ArchTest
        public static final ArchRule ruleAsStaticField = 
                  ArchRuleDefinition.classes()
                                    .should()...
    
        @ArchTest
        public static void ruleAsStaticMethod(JavaClasses classes) {
            ArchRuleDefinition.classes()
                               .should()...
        }
    }

     

     

     

    2. 예시 1 - 패키지 의존성 확인

    테스트용 패키지 수정

    테스트를 위해서 패키지를 일부 정리했다.

    Team 패키지가 Membe 패키지를 참조하고, Team과 Member가 Domain 패키지를 참조하는 구조를 테스트한다. 

    (Robot, Part는 우선 제외)

    • domain 패키지에 있는 클래스는 team, member 클래스에서 참조 가능
    • member 패키지에 있는 클래스는 team, member 클래스에서만 참조 가능
    • domain 패키지는 team, member 패키지를 참조하지 못함
    • team 패키지에 있는 클래스는 team에서만 참조 가능
    • 순환 참조 없어야 한다.

     

    테스트 소스

    위의 테스트 내용을 소스로 작성하면 이렇다.

    public class TeamArchUnitTest {
    
        // ..xx.. 패키지
        @Test
        public void packageDependencyTests(){
            // 1. 클래스 읽기
            JavaClasses classes = new ClassFileImporter().importPackages("com.mc.testexample");
    
            // 2. 룰 정의
            ArchRule myRule = classes().that().resideInAPackage("..domain..")
                    .should().onlyBeAccessed().byAnyPackage("..team..", "..member..", "..domain..");
    
            // 3. 확인
            myRule.check(classes);
        }
    }

    테스트를 해보면, domain의 Robot, Part가 다른 패키지에서도 참조중이기 때문에 에러가 뜨는 걸 볼 수 있다.

    .byAnyPackage("..team..", "..member..", "..robot..", "..part..", "..domain..");

    로봇과 part 패키지도 넣어주면 테스트가 성공한다. 

     

    - 순환참조 확인

    slice().matching을 써서 각 calss들을 개별적인 패키지로 인식시키고, 그 값들이 순환참조 하는 지를 확인한다.

    @Test
    public void packageDependencyTests(){
        // 1. 클래스 읽기
        JavaClasses classes = new ClassFileImporter().importPackages("com.mc.testexample");
    
        // 순환참조 확인 - slice를 해서 개별적인 패키지로 인식을 시키고, 그 값들이 각각 순환참조를 하는지 확인
        ArchRule freeOfCycles = slices().matching("..testexample.(*)..")
                .should().beFreeOfCycles();
        freeOfCycles.check(classes);
    }

    순환참조를 일으키기 위해서 MemberService를 구현해서 임시로 teamService를 참조해준다.

    public class DefaultMemberService implements MemberService{
    
        TeamService teamService;
    
        @Override
        public Optional<Member> findById(Long partId) {
            return Optional.empty();
        }
    
        @Override
        public void validate(Long partId) {
            teamService.createNewTeam(partId, null);
        }
    
        @Override
        public void notify(Team team) {
    
        }
    
        @Override
        public void notify(Member member) {
    
        }
    }

    결과는 아래와 같다

     

    ArchUnit & JUnit 5 연동하기

    @AnalyzeClasses

    : 클래스를 읽어들여서 확인할 패키지 설정

    @AnalyzeClasses(
            packages = "com.mc.testexample",
            importOptions = ImportOption.DoNotIncludeTests.class
    )

    패키지를 지정해줘도 되고, 아래처럼 application main 클래스가 있는 패키지를 전체 지정해줘도 된다.

    @AnalyzeClasses(packagesOf = App.class)


    @ArchTest : 확인할 규칙 정의

    확실히 메소드를 만드는 것보다는 줄어들었으나, display name을 적어줄 수가 없다.

    @AnalyzeClasses(packagesOf = App.class)
    public class TeamArchUnitJUnitTest {
    
        @ArchTest
        ArchRule domainRule = classes().that().resideInAPackage("..domain..")
                .should().onlyBeAccessed().byAnyPackage("..team..", "..member..", "..robot..", "..part..", "..domain..");
    
    
        @ArchTest
        ArchRule memberPackageRule = classes().that().resideInAPackage("..domain..")
                .should().accessClassesThat().resideInAPackage("..member..");
    
        @ArchTest
        ArchRule teamPackageRule = classes().that().resideOutsideOfPackage("..team..")
                .should().accessClassesThat().resideInAnyPackage("..team..");
    
        @ArchTest
        ArchRule freeOfCycles = slices().matching("..testexample.(*)..")
                .should().beFreeOfCycles();
    }

    ArchUnit은 Junit 엔진을 확장해서 Junit 모듈을 만든 거라서 Jupiter로 실행하는 것이 아니라 Arch Unit 엔진을 실행한다.

     

     

     

    3. 예시 2 - 클래스 의존성 확인

    TeamController는 TeamService, TeamRepository를 참조하고, TeaService는 TeamRepository를 참조한다. 

    • TeamController는 TeamService와 TeamRepository를 사용할 수 있다.
    • Team* 으로 시작하는 클래스는 ..team.. 패키지에 있어야 한다.
      • Team domain 클래스는 ..domain.. 패키지에 있다.
    • TeamRepository는 TeamService와 TeamController를 사용할 수 없다.

     

    테스트 소스

    @AnalyzeClasses(packagesOf = App.class)
    public class TeamArchUnitClassTest {
    
        // 컨트롤러는 서비스와 Repository를 참조할 수 있음.
        @ArchTest
        ArchRule controllerClassRule = classes().that().haveSimpleNameEndingWith("Controller")
                .should().accessClassesThat().haveSimpleNameEndingWith("Service")
                .orShould().accessClassesThat().haveSimpleNameEndingWith("Repository");
    
        // Repository는 서비스를 참조할 수 없음.
        @ArchTest
        ArchRule repositoryClassRule = classes().that().haveSimpleNameEndingWith("Repository")
                .should().accessClassesThat().haveSimpleNameEndingWith("Service");
    
        @ArchTest
        ArchRule teamClassesRule = classes().that().haveSimpleNameStartingWith("Team")
                .and().areNotEnums()
                .and().areNotAnnotatedWith(Entity.class)
                .should().resideInAnyPackage("..team..");
    }
    

     

    추가적인 검증

    Freezing Arch Rules

    만약 어플리케이션 개발이 많이 진행되었는데, 아키텍처 룰이 많이 깨진 상태라면,

    추가적으로 깨지는 것만 점차적으로 맞춰 나갈 때 쓴다.

     

    Architecture Rule

    OnionArchitecture, LayeredArchitecture 등 원하는 아키텍처 구조를 따르고 있는지를 확인할 수도 있다.

     

     

     

    출처 : '더 자바, 애플리케이션을 테스트하는 다양한 방법'(백기선), 자료&37~41강

    300x250
    • 네이버 블로그 공유
    • 네이버 밴드 공유
    • 페이스북 공유
    • 카카오스토리 공유