본문 바로가기

Dot Programming/Spring

[Spring Test] ArchUnit로 패키지 및 레이어 아키텍처 검사하기

    아키텍처

    아키텍처(architecture)는 고대 그리스어에서 건축 혹은 석공 명인(Master)을 의미하는 아키텍톤(Architecton) 이라는 용어로부터 유래되었다. 당시의 아키텍처는 건축물의 골격을 제공하는 설계도 역할을 했다. 좋은 아키텍처는 훌륭한 건축물을 탄생시켰고, 이들은 인류의 훌륭한 유산으로 남겨지고 있다.

     

    고대의 아키텍처 개념이 화강암과 대리석으로 건축물을 짓는데 적용되었다면, 산업 시대에는 건축뿐만 아니라 첨단 과학기술을 이용하여 항공기, 자동차, 선박 등을 개발하는데 적용되어 왔다. 오늘날 디지털 정보화 시대에는 첨단 정보기술을 이용하여 정보체계, 소프트웨어 내장형 체계, 지휘 통제 통신체계 등을 구축하는데 적용되고 있다.

     

    특히, 컴퓨터나 네트워크에서 말하는 아키텍처란, 프로세스와 전체적인 구조나 논리적 요소들 그리고 컴퓨터와 운영체계, 네트워크 및 다른 개념 간의 논리적 상호관계 등을 생각해내고 정의하는 등, 모든 곳에 적용되는 용어이다. 아키텍처는 OSI 7 계층처럼 하나의 참조 모델이 될 수도 있지만, 특정 제품의 구조를 위한 모델을 의미하거나, 특정 제품의 구조가 될 수도 있다. 또한, 아키텍처는 대상에 대한 구조뿐만 아니라, 대상 구조의 유지 관리를 위한 원칙과 지침, 그리고 향후 목표 아키텍처로 가기 위한 계획을 포함하고 있다.

     

    애플리케이션 아키텍처

    애플리케이션 아키텍처는 애플리케이션을 설계하고 구축하는 데 사용하는 패턴과 기술을 말한다. 아키텍처는 애플리케이션을 구축할 때 따라야 할 로드맵과 모범 사례를 제공하여 체계적으로 구성된 애플리케이션을 완성할 수 있게 해준다. 애플리케이션 아키텍처에는 다양한 유형이 있지만 서비스 간 관계를 기준으로 오늘날 가장 널리 사용되는 유형은 모놀리식(monolithic) 및 레이어(N-티어) 아키텍처, 마이크로서비스(분리됨; MSA), 이벤트 기반 아키텍처 및 서비스 지향 아키텍처(탄력적 결합)이다.

     

    레이어 아키텍처 (계층화 또는 N-티어 아키텍처)

    레이어 아키텍처는 엔터프라이즈 애플리케이션을 구축할 때 흔히 사용하는 전통적인 아키텍처로, 레거시 애플리케이션과 연관된 경우가 많다. 이는 주로 3개 티어로 구성되지만 이보다 많을 수 있으며, 각각 책임이 지정된 티어가 앱을 구성한다. 레이어는 종속성을 관리하고 논리적 기능을 수행하도록 지원한다. 계층화된 아키텍처에서 레이어는 수평으로 배열되기 때문에 아래 층에 해당하는 레이어만 호출할 수 있다.

     

    아키텍처를 테스트해야 하는 이유

    소프트웨어 아키텍처는 코드 이해도와 확장성 그리고 품질 향상을 위해서 꼭 필요한 중요 요소이다. 이를 잘 준수함으로써 팀 협업 간에 유지 관리, 리팩토링, 확장을 빠르게 이뤄져 생각보다 엄청난 생산성을 보여줄 수 있다. 즉, 애플리케이션이 성장 및 확장할 때마다 새로운 기능을 추가하면서 버그도 신속하게 캐치하여 핸들링한다. 여기서 객체지향 이념을 내세워 소프트웨어 시스템을 유지보수, 교체 및 확장 가능하게 유지하려면 높은 응집력과 느슨한 결합으로 이어지는지 확인하여 모듈 간의 상호 의존성이 최대한 작고 정확하게 필요한 부분에만 이루어져 있는지 확인해야 한다. 

     

    이러한 목표는 특정 패턴 및 규칙(아키텍처)을 도입함으로써 충족될 수 있다. 그리고 간단한 테스트를 통해 이러한 아키텍처 진행 상황을 모니터링하고 확인할 수 있다. (자동 아키텍처 검증은 최대한 나중에 도입함으로써 기술 부채를 단계적으로 줄일 수 있다.)

     

    ArchUnit로 아키텍처 검사하기

    ArchUnit은 plain Java unit test 프레임워크를 사용하여 Java 코드의 아키텍처를 검사하기 위한 심플하면서 확장 가능한 오픈소스이다. 즉, 애플리케이션의 아키텍처를 테스트 할 수 있는 오픈 소스 라이브러리로, 패키지, 클래스, 레이어, 슬라이스 간의 의존성을 확인하고 순환 참조를 체크할 수 있는 기능을 제공한다. 주어진 Java 바이트코드를 분석하여 모든 클래스를 Java 코드 구조로 가져옴으로써 이를 수행한다. 

    • 패키지 및 클래스 종속성, 의존관계 검사 (격리 확인)
    • 상속 관계, 순환 참조 검사
    • 레이어 아키텍처 검사
    • 직접 규칙을 정의해 코딩 컨벤션 준수 여부 검사

     

    ArchUnit을 스프링 프로젝트에 도입하기 위해서 특별한 인프라나 새로운 언어가 전혀 필요하지 않다. 의존성만 추가해주면 쉽게 사용이 가능하다. (참고)

    dependencies {
        // archunit
        testImplementation 'com.tngtech.archunit:archunit-junit5:0.23.1'
    }

     

    1. 패키지와 클래스 의존성 검사하기

    foo 패키지에서 bar 패키지에 의존할 수 없도록 즉, foo 패키지에서는 bar 패키지에 접근할 수 없도록 규칙을 정의했다고 하자.

    패키지 의존성

     

    이에 대한 의존성을 ArchUnit을 사용하여 간단하게 검사할 수 있다.

     

    방법 1. 선언형 검사

    선언형 검사는 아키텍처에 맞도록 규칙을 먼저 선언해놓고, rule.check()을 사용해 모든 클래스의 규칙을 한 번에 검사하는 방식이다. ArchUnit으로 다음과 같이 테스트 코드를 작성한다.

    @Test
    void packageDependencyTest(){
        JavaClasses importedClasses = new ClassFileImporter().importPackages("com.example.springarchunit.example");
        ArchRule rule = noClasses().that().resideInAPackage("..foo..")
            .should().dependOnClassesThat().resideInAPackage("..bar..");
        rule.check(importedClasses);
    }

     

    패키지 의존성 규칙 어겨보기 

    만약 다음과 같이 foo패키지 내부에 있는 Foo 클래스에서 bar 패키지에 있는 Bar 클래스를 참조하면 어떻게 될까? 한 번 직접 작성해보자.

     

    패키지 의존성 규칙 어겨보기

     

    다음은 Foo 클래스에서 Bar 클래스의 의존성을 주입받는 코드이다.

    @Component
    public class Foo {
    
    	/**
    	 * rule check error!
    	 */
    	@Autowired
    	private Bar bar;
    }

     

    ArchUnit으로 패키지 의존성 검사를 하면 다음과 같이 foo 패키지에서 bar 패키지에 의존하고 있다고 에러가 발생하는 것을 확인할 수 있다. 이렇게 의존성 규칙을 정의하여 테스트 코드를 작성하면 패키지 의존성 검사를 쉽게 진행할 수 있다.

     

    의존성 에러 발생

     

    2. 레이어 아키텍처 검사하기

    다음과 같이 레이어 간의 규칙을 정의해보자.

    • Controller → Service
    • Controller → Repository
    • Servoice → Repository
    • 즉, Controller는 Service와 Repository 의존성을 주입받을 수 있고 Service는 Repository 의존성을 주입받을 수 있다. 반대 방향으로는 의존성 참조가 불가능하다.

     

    레이어간 규칙 정의

     

    ArchUnit라이브러리를 사용하여 다음과 같이 검사할 수 있다.

     

    방법 1. 선언형 검사

    아키텍처에 맞도록 규칙을 먼저 선언해놓고, rule.check()을 사용해 모든 클래스의 규칙을 한 번에 검사하는 방식이다. ArchUnit으로 다음과 같이 테스트 코드를 작성한다

    @Test
    void layeredArchitectureTest(){
    	JavaClasses importedClasses = new ClassFileImporter().importPackages("com.example.springarchunit.layer");
    	Architectures.LayeredArchitecture rule = layeredArchitecture()
    		.layer("Controller").definedBy("..controller..")
    		.layer("Service").definedBy("..service..")
    		.layer("Repository").definedBy("..repository..")
    		.whereLayer("Controller").mayNotBeAccessedByAnyLayer() // 다른 계층에 접근 불가
    		.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    		.whereLayer("Repository").mayOnlyBeAccessedByLayers("Controller", "Service");
    	rule.check(importedClasses);
    }

     

    방법 2. ArchUnit 테스트 클래스 생성

    아키텍처 검사를 하는 클래스를 따로 생성하여 관리할 수도 있다.

    • 다음과 같이 클래스 위에 @AnalyzeClasses(packagesOf = App.class)을 명시한다. App.class는 스프링 앱을 실행하는 클래스를 입력해주면 된다.
    • @ArchTest를 명시하고 똑같이 아키텍처 검사 코드를 작성하면 rule.check()을 따로 작성하지 않아도 자동으로 검사해준다.
    @AnalyzeClasses(packagesOf = App.class)
    public class LayeredArchitectureTest {
    	@ArchTest
    	LayeredArchitecture layeredArchitectureRule = layeredArchitecture()
    		.layer("Controller").definedBy("..controller..")
    		.layer("Service").definedBy("..service..")
    		.layer("Repository").definedBy("..repository..")
    		.whereLayer("Controller").mayNotBeAccessedByAnyLayer() // 다른 계층에 접근 불가
    		.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    		.whereLayer("Repository").mayOnlyBeAccessedByLayers("Controller", "Service");
    }

     

    레이어 간의 규칙 어겨보기

    레이어 아키텍처간 규칙을 검사하는 테스트가 정상적으로 동작하고 있는지 Service 레이어에서 Controller의 코드를 의존하는 코드를 한 번 작성해보자.

    @Service
    public class MemberService {
    
    
       @Autowired
       private MemberRepository memberRepository;
    
       /**
        * rule check error!
        */
       @Autowired
       private MemberController memberController;
    }

     

    테스트 코드를 실행해보면 다음과 같이 에러가 발생한다. 게다가 정확히 어디서 레이어 간의 의존 규칙을 어겼는지 알려주는 것을 확인할 수 있다. 이로써 레이어 간의 규칙 검사가 제대로 이루어지고 있음을 알 수 있다. 이렇게 레이어, 계층 간의 이동도 ArchUnit으로 쉽게 검사할 수 있다. 

     

    레이어 아키텍처 규칙 에러 발생

     

    마무리

    ArchUnit은 아키텍처 및 코드 품질을 단위 테스트하는 데 아주 용이해서 이를 통해 빠르고 쉽게 코드 품질을 높일 수 있는 라이브러리이다. 개인 프로젝트에 도입하여 직접 사용하였는데 기존에 설정해둔 아키텍처 규칙을 빌드(테스트) 단위에서 바로 캐치해주어 빠르게 코드 품질을 높이고 확장 및 리팩토링을 진행할 수 있어서 이렇게 블로그에도 글을 남기게 되었다. 생산성을 중요시하는 데 이를 위해서라도 앞으로 모든 프로젝트에 ArchUnit을 사용하여 아키텍처 검사를 진행할 것 같다.

     

    실습한 내용은 github에서 확인이 가능합니다.

    참고

    https://www.archunit.org/

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

    https://www.redhat.com/ko/topics/cloud-native-apps/what-is-an-application-architecture

    https://d2.naver.com/helloworld/9222129