[ArchUnit] ArchUnit + JUnit5 아키텍처 품질 검증하기

아두치·2023년 1월 16일
0

최근에 읽던 책에서 ArchUnit 이라는 테스트 라이브러리를 접했다.
사실 그동안 테스트라 함은 단위 테스트, 통합 테스트 같은 "기능 테스트" 로만 알고 있었기에 아키텍처 테스트라는 단어는 와닿지가 않았다.

책에서는 아키텍처 테스트라고 하지만, ArchUnit 공식 홈페이지에서 몇가지 예제를 실행해보니 아키텍처 테스트보다 아키텍처 품질 검증으로 표현하는게 더 와닿을 것 같다.

여기서 말하는 아키텍처는 시스템 아키텍처가 아니라, 소프트웨어 아키텍처이다.
진행하는 프로젝트의 매니저가 (혹은 누군가가) 소프트웨어의 아키텍처를 구상하고, 조직원들과의 미팅을 통해 아키텍처를 공유하고, 모두의 동의를 얻은 뒤 프로젝트 개발이 시작될 것이다.

하지만, 위에서 말한 "소프트웨어 아키텍처에 대한 동의" 는 엄밀히 말하면 암묵적인 약속이다.
예를 들어서 프로젝트 매니저가 "Service 는 반드시 Controller 에서만 접근해야 한다" 라는 규칙을 정했다고 가정하면, 회의를 통해 모두 숙지했기 때문에 대부분의 경우 Service 를 Controller 에서만 접근하도록 개발하겠지만, 개발은 결국 사람이 하기 때문에 누군가가 실수로 다른 컴포넌트에서 Service 에 접근할 수 있다.
이 경우 순환 참조만 되지 않으면 컴파일 및 빌드, 실행에 전혀 문제가 없다.

코드 리뷰를 통해 드러날 수 있겠지만, 코드 리뷰가 조금 강력하지 않게 진행되거나 아예 코드 리뷰 자체가 없을 경우 지속적 통합에서 걸리지 않는 문제가 되기 때문에 아키텍처 품질(일관성)이 저하되는 원인이 된다.

이러한 아키텍처 품질을 단위 테스트 처럼 지속적 통합, 배포 이전에 검증을 수행하고 실패할 경우 작업을 중단하도록 할 수 있으면 편리할 것 같다.

이때 ArchUnit 을 사용할 수 있다.

자세히 다루진 않고, 공식 홈페이지를 조금 번역해서 몇가지 예제만 보이겠다.

우선 의존성 관리 도구에 ArchUnit 에 대한 의존성을 추가해야한다.
(참고로 내가 테스트한 환경은 Spring Boot 3.0.1, Java 17, Maven 이다.)

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

이제 ArchUnit 을 사용할 준비가 되었다.

아까 위에서 언급한 "Service 는 Controller 에서만 접근가능하다" 라는 아키텍처를 검증해보자.

먼저, 테스트 케이스의 뼈대부터 만들어보자.

@Test
public void 서비스는_컨트롤러에서만_접근이_가능하다(){
	// TODO
}

ArchUnit 의 동작 원리는 검증 대상 클래스들을 패키지 단위로 ArchUnit 내부에 Import 한 뒤 지정한 검증을 수행한다.
따라서 우리가 검증하려는 소스 코드(클래스)가 들어있는 패키지를 Import 시켜줘야한다.

	@Test
    public void 서비스는_컨트롤러에서만_접근이_가능하다(){
        JavaClasses classes = new ClassFileImporter().importPackages("com.example.arch");
    }

패키지명은 본인 프로젝트의 패키지명에 맞게 수정하면 된다.

이제 JavaClasses 라는 ArchUnit 라이브러리에 클래스들이 Import 되어 있다.
나중에 이 JavaClasses 에 검증 방법을 알려주기만 하면 된다.

그렇다는 말은, 검증 방법을 먼저 만들어야 한다는 뜻이다.

	import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

	@Test
    public void 서비스는_컨트롤러에서만_접근이_가능하다(){
        JavaClasses classes = new ClassFileImporter().importPackages("com.example.arch");
        ArchRule rule = classes()
                .that().resideInAPackage("..service..")
                .should().onlyBeAccessed().byAnyPackage("..controller..");
        rule.check(classes);
    }

ArchUnit 은 Fluent API 로 작성하기 때문에 이해하기가 쉬울 것이다.
주목할 곳은 마지막 rule.check(classes) 를 실행해야 작성한 Rule이 classes 를 대상으로 검증이 된다는 것이다.

테스트를 실행했을때 service 패키지가 있고, service 패키지 내에 있는 모든 클래스를 controller 패키지 내에서만 접근한다면 성공할 것이고, 그렇지 않다면 실패할 것이다.

근데 코드가 조금 장황하다.
검증해야할 아키텍처 규칙이 많다면 모든 테스트 메소드마다 같은 패키지를 import 하는 코드가 중복되고, rule.check(..) 는 모든 테스트 메소드마다 반드시 실행해야 하기 때문에 이 또한 중복이 된다.

이때 JUnit 에 ArchUnitRunner 를 확장시키면 코드를 간결하게 작성할 수 있다.
(JUnit4 까지는 @RunWith 를 이용해 명시적으로 ArchUnitRunner 를 확장시켜줘야 했지만 JUnit5 부터는 명시적으로 확장시켜주지 않아도 된다.)

@AnalyzeClasses(packages = "com.example.arch")
public class ArchTests {
    @ArchTest
    public static final ArchRule 서비스는_컨트롤러에서만_접근이_가능하다 = classes()
            .that().resideInAPackage("..service..")
            .should().onlyBeAccessed().byAnyPackage("..controller..");
}

코드를 보면 @AnalyzeClasses 애너테이션을 이용해 딱 한번만 패키지를 import 했다.
그리고 검증 룰을 메소드가 필드로 작성했다.
이것은 @ArchTest 애너테이션이 테스트 실행 시 해당 필드를 이용해 rule.check(..) 와 같은 코드를 실행시켜주기 때문에 가능한 일이다.

어떤 방법을 선택하든 자유지만, 검증 룰이 많다면 이 방법이 더 간결할 것 같다.

ArchUnit 은 이런 유형의 아키텍처 검증 외에도 패키지 간의 의존성도 테스트할 수 있다.
예를 들어서, "Controller 는 반드시 Service 에 의존해야한다" 와 같은 룰을 검증할 수 있다.

	@ArchTest
    public ArchRule 컨트롤러는_서비스에_의존해야한다 = classes().that().resideInAPackage("..controller..")
            .should().dependOnClassesThat().resideInAPackage("..service..");

참고로 그 반대인 "Service 는 Controller 에 의존하면 안된다" 도 검증 가능하다.

	@ArchTest
    public ArchRule 서비스는_컨트롤러에_의존하면_안된다 = noClasses().that().resideInAPackage("..service..")
            .should().dependOnClassesThat().resideInAPackage("..controller..");

패키지간의 의존성 뿐만 아니라 클래스간의 의존성도 검증할 수 있다.
단, 기본적으로 ArchUnit 은 패키지 단위로 조작하기 때문에 패키지 내에서 특정 클래스를 선택해서 검증해야 한다.

	@ArchTest
    public ArchRule 유저_서비스에는_유저_컨트롤러만_접근한다 = classes().that().haveSimpleName("UserService")
                                                    .should().onlyHaveDependentClassesThat().haveSimpleName("UserController");

또한 클래스의 네이밍 규칙도 검증할 수 있다.

	@ArchTest
    public ArchRule 컨트롤러는_controller_패키지에_존재한다 = classes().that().haveSimpleNameEndingWith("Controller")
                                                    .should().resideInAPackage("com.example.arch.controller");

마지막으로 내가 느끼기에 가장 유용해 보였던 레이어 계층 검증을 소개하고 마무리 하겠다.

이번엔 다음 항목들을 레이어 계층 단위로 한번에 검증해보겠다.

  1. Controller Layer 는 controller 패키지에만 있다.
  2. Service Layer 는 service 패키지에만 있다.
  3. Controller Layer 는 어떤 다른 Layer 에서 접근하면 안된다.
  4. Service Layer 는 Controller Layer 에서만 접근한다.
	import static com.tngtech.archunit.library.Architectures.layeredArchitecture;

	@ArchTest
    public ArchRule 레이어_단위_검증 = layeredArchitecture()
            .consideringAllDependencies()
            .layer("Controller").definedBy("..controller..")
            .layer("Service").definedBy("..service..")
            .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
            .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller");

팀 전체가 아키텍처에 대한 룰이 궁금할 때 이런 아키텍처 품질 검증 코드만 보아도 전체적으로 파악할 수 있을 것 같다.

아직 실무에서 ArchUnit 을 본적은 없지만, 한번 도입을 의논해볼만하다고 생각한다.

참고로, 이외에도 다양한 기능들이 있으니 자세한 내용은 ArchUnit 공식 홈페이지에서 찾아보길 바란다.

profile
HAVE YOU TRIED IT?

0개의 댓글