1. 개요

이 기사에서는 ArchUnit을 사용하여 시스템의 아키텍처를 확인하는 방법을 보여줍니다 .

2. ArchUnit 이란 무엇입니까 ?

아키텍처 특성과 유지 관리 가능성 간의 연결 은 소프트웨어 산업에서 잘 연구된 주제 입니다. 하지만 우리 시스템을 위한 건전한 아키텍처를 정의하는 것만으로는 충분하지 않습니다. 구현된 코드가 코드를 준수하는지 확인해야 합니다.

간단히 말해서, ArchUnit 은 애플리케이션이 주어진 아키텍처 규칙 세트를 준수하는지 확인할 수 있는 테스트 라이브러리입니다 . 그러나 아키텍처 규칙이란 무엇입니까? 더욱이 이 맥락에서 건축 이란 무엇을 의미 합니까?

후자부터 시작하겠습니다. 여기에서 아키텍처 라는 용어 는 애플리케이션의 다양한 클래스를 패키지로 구성하는 방식  을 나타냅니다 .

시스템 아키텍처는 또한 패키지 또는 패키지 그룹( 레이어 라고도 함)이 상호 작용 하는 방식을 정의합니다 . 보다 실용적인 용어로 주어진 패키지의 코드가 다른 패키지에 속한 클래스의 메서드를 호출할 수 있는지 여부를 정의합니다. 예를 들어, 애플리케이션 아키텍처가 presentation , servicepersistence 의 세 가지 계층을 포함한다고 가정해 보겠습니다 .

이러한 레이어가 상호 작용하는 방식을 시각화하는 한 가지 방법은 각 레이어를 나타내는 패키지와 함께 UML 패키지 다이어그램을 사용하는 것입니다.

이 다이어그램을 보면 몇 가지 규칙을 알 수 있습니다.

  • 프레젠테이션 클래스는 서비스 클래스에만 의존해야 합니다.
  • 서비스 클래스는 지속성 클래스에만 의존해야 합니다.
  • 지속성 클래스는 다른 사람에게 의존해서는 안 됩니다.

이러한 규칙을 살펴보면 이제 원래 질문으로 돌아가서 답할 수 있습니다. 이 컨텍스트에서 아키텍처 규칙은 애플리케이션 클래스가 서로 상호 작용하는 방식에 대한 주장입니다.

이제 구현이 이러한 규칙을 준수하는지 어떻게 확인합니까? 여기에서 ArchUnit 이 필요합니다. 이를 통해 유창한 API를 사용하여 아키텍처 제약 조건을 표현하고 일반 빌드 중에 다른 테스트와 함께 유효성을 검사할 수 있습니다.

3. ArchUnit 프로젝트 설정

ArchUnitJUnit 테스트 프레임워크 와 잘 통합 되므로 일반적으로 함께 사용됩니다. JUnit 버전 과 일치하도록 archunit-junit4 의존성을 추가하기만 하면 됩니다 .

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit4</artifactId>
    <version>0.14.1</version>
    <scope>test</scope>
</dependency>

artifactId가 암시하듯이 이 의존성은 JUnit 4 프레임워크에 따라 다릅니다 .

JUnit 5를 사용하는 경우 archunit-junit5 의존성 도 있습니다 .

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

4. ArchUnit 테스트 작성 하기

프로젝트에 적절한 의존성을 추가했으면 아키텍처 테스트 작성을 시작하겠습니다. 우리의 테스트 애플리케이션은 Smurfs 를 쿼리하는 간단한 SpringBoot REST 애플리케이션이 될 것 입니다. 단순화를 위해 이 테스트 애플리케이션에는 Controller , ServiceRepository 클래스 만 포함되어 있습니다.

이 애플리케이션이 이전에 언급한 규칙을 준수하는지 확인하고 싶습니다. 따라서 "프레젠테이션 클래스는 서비스 클래스에만 의존해야 함" 규칙에 대한 간단한 테스트부터 시작하겠습니다.

4.1. 첫 번째 테스트

첫 번째 단계는 규칙 위반을 검사할 Java 클래스 집합을 만드는 것 입니다. ClassFileImporter 클래스 를 인스턴스화 한 다음 importXXX() 메서드 중 하나를 사용하여 이를 수행 합니다.

JavaClasses jc = new ClassFileImporter()
  .importPackages("com.baeldung.archunit.smurfs");

이 경우  JavaClasses 인스턴스에는 기본 애플리케이션 패키지와 해당 하위 패키지의 모든 클래스가 포함됩니다. 이 객체는 규칙 평가의 대상이 되기 때문에 일반 단위 테스트에서 사용되는 일반적인 테스트 대상과 유사하다고 생각할 수 있습니다.

아키텍처 규칙은 ArchRuleDefinition 클래스 의 정적 메서드 중 하나를  유창한 API 호출 의 시작점으로 사용 합니다. 이 API를 사용하여 위에서 정의한 첫 번째 규칙을 구현해 보겠습니다. 우리는 사용할 것이다 클래스 () 우리의 닻과 같은 방법을 거기에서 추가적인 제약 조건을 추가합니다 :

ArchRule r1 = classes()
  .that().resideInAPackage("..presentation..")
  .should().onlyDependOnClassesThat()
  .resideInAPackage("..service..");
r1.check(jc);

검사 를 실행하기 위해 만든 규칙 check() 메서드 를 호출해야 합니다  . 이 메서드는  JavaClasses 개체를 사용하고 위반이 있는 경우 예외를 throw합니다.

이 모든 것이 좋아 보이지만 코드에 대해 실행하려고 하면 오류 List이 표시됩니다.

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - 
  Rule 'classes that reside in a package '..presentation..' should only 
  depend on classes that reside in a package '..service..'' was violated (6 times):
... error list omitted

왜요? 이 규칙의 주요 문제는 onlyDependsOnClassesThat() 입니다. 패키지 다이어그램에 넣은 내용에도 불구하고 실제 구현에는 JVM 및 Spring 프레임워크 클래스에 대한 의존성이 있으므로 오류가 발생합니다.

4.2. 첫 번째 테스트 다시 작성하기

이 오류를 해결하는 한 가지 방법은 이러한 추가 의존성을 고려하는 절을 추가하는 것입니다.

ArchRule r1 = classes()
  .that().resideInAPackage("..presentation..")
  .should().onlyDependOnClassesThat()
  .resideInAPackage("..service..", "java..", "javax..", "org.springframework..");

이 변경으로 인해 검사 실패가 중지됩니다. 그러나 이 접근 방식은 유지 관리 문제가 있고 약간 해킹된 느낌이 듭니다. noClasses() 정적 메서드를 시작점으로 사용하여 규칙을 다시 작성하는 문제를 피할 수 있습니다 .

ArchRule r1 = noClasses()
  .that().resideInAPackage("..presentation..")
  .should().dependOnClassesThat()
  .resideInAPackage("..persistence..");

물론 이 접근 방식이  이전에 허용 기반 방식이 아닌 거부 기반 방식이라는 점도 지적할 수 있습니다 . 중요한 점은 우리가 어떤 접근 방식을 선택 하든 ArchUnit 은 일반적으로 규칙을 표현할 수 있을 만큼 충분히 유연하다는 것 입니다.

5. 라이브러리 API 사용

ArchUnit 은 내장된 규칙 덕분에 복잡한 아키텍처 규칙을 쉽게 만들 수 있습니다. 이는 차례로 결합될 수도 있으므로 더 높은 수준의 추상화를 사용하여 규칙을 만들 수 있습니다. 박스 아웃, ArchUnit를 제공하는 라이브러리 API , 사전 패키지 된 규칙의 집합 주소 일반적인 아키텍처 문제가 :

  • 아키텍처 : 계층 및 양파(육각형 또는 "포트 및 어댑터"라고도 함) 아키텍처 규칙 검사 지원
  • Slices : 순환 의존성 또는 "주기"를 감지하는 데 사용됩니다.
  • 일반 : 로깅, 예외 사용 등과 같은 최상의 코딩 방법과 관련된 규칙 모음입니다.
  • PlantUML : 우리의 코드 베이스가 주어진 UML 모델을 준수하는지 확인합니다.
  • 고정 아치 규칙 : 나중에 사용할 수 있도록 위반 사항을 저장하여 새로운 위반 사항만 보고할 수 있습니다. 기술 부채를 관리하는 데 특히 유용합니다.

이러한 모든 규칙을 다루는 것은 이 소개의 범위를 벗어나지만 아키텍처 규칙 패키지를 살펴보겠습니다 . 특히, 계층화된 아키텍처 규칙을 사용하여 이전 섹션의 규칙을 다시 작성해 보겠습니다. 이 규칙을 사용하려면 두 단계가 필요합니다. 먼저 애플리케이션의 계층을 정의합니다. 그런 다음 허용되는 계층 액세스를 정의합니다.

LayeredArchitecture arch = layeredArchitecture()
   // Define layers
  .layer("Presentation").definedBy("..presentation..")
  .layer("Service").definedBy("..service..")
  .layer("Persistence").definedBy("..persistence..")
  // Add constraints
  .whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
  .whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation")
  .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");
arch.check(jc);

여기서  layeredArchitecture()Architectures 클래스 의 정적 메서드입니다 . 호출되면 새 LayeredArchitecture 객체를 반환하며 , 이 객체를 사용하여 의존성과 관련된 이름 레이어 및 주장을 정의합니다. 이 객체는 다른 규칙처럼 사용할 수 있도록 ArchRule 인터페이스를 구현합니다  .

이 특정 API의 멋진 점은 그렇지 않으면 여러 개별 규칙을 결합해야 하는 몇 줄의 코드 규칙으로 생성할 수 있다는 것입니다.

6. 결론

이 기사에서는 프로젝트에서 ArchUnit 을 사용하는 기본 사항을 살펴보았습니다 . 이 도구를 채택하는 것은 전반적인 품질에 긍정적인 영향을 미치고 장기적으로 유지 관리 비용을 줄일 수 있는 비교적 간단한 작업입니다.

평소와 같이 모든 코드는 GitHub에서 사용할 수  있습니다 .

Generic footer banner