Search
Duplicate

아키텍처 테스트

목차

Archunit 소개

테스팅에 좀 익숙하다는 개발자들이면 단위 테스트나 인수테스트, 탐색테스트 정도는 익숙할 것이다.
그런데 그 밖에도 속성 테스팅이나 아키텍처 테스팅등의 테스트도 존재하는데, 속성 테스팅은 이전 챕터(운영 이슈 테스트)에서 다룬 Chaos Monkey를 사용하는 응답 시간, 확장성, 예외등등 테스트가 있다.
그리고 여기에 아키텍처 테스트라 해서 애플리케이션의 아키텍처를 테스트하는 오픈 소스 라이브러리인 ArchUnit에 대해 알아보고 사용해볼 것이다. 이 라이브러리를 사용해서 우리는 아키텍츠의 패키지, 클래스, 레이어, 슬라이스간의 의존성을 확인할 수 있다. 이렇게 의존성을 확인한다면, 객체간의 순환참조를 확인해 리팩토링할 수도 있다.

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

A 패키지가 B(or C, D)패키지에서만 사용되고 있는지 확인할 수 있다.
xxxService가 Controller나 Service계층에서만 참조되고 있는지 확인할 수 있다.
Service 계층이 service패키지에 포함되어있는지 확인할 수 있다.
A라는 애노테이션을 선언한 메소드만 특정 패키지 또는 특정 애노테이션을 가진 클래스를 호출하고 있는지 확인할 수 있다.
특정한 스타일의 아키텍처를 따르고 있는지 확인할 수 있다.
→ ex: 계층형 아키텍처, 도메인 아키텍처, 양파 아키텍처 등...

ArchUnit 설치

의존성 추가

<dependency> <groupId>com.tngtech.archunit</groupId> <artifactId>archunit-junit5-engine</artifactId> <version>0.12.0</version> <scope>test</scope> </dependency>
XML
복사
pom.xml에 해당 의존성을 추가해준다.

주요 사용법

위에서 작성한 아키텍처 테스트 유즈케이스 모두 사용이 가능한데, 그 원리는 특정 패키지에 해당하는 클래스를 바이트코드를 통해 읽어들여서 확인할 규칙들(내가 라이브러리 메서드로 선언한)을 정의하고 읽어들인 클래스들이 이러한 규칙을 잘 지키고 있는지 확인한다.
다음 코드는 간단한 사용법 예제이다
@Test public void Services_should_only_be_accessed_by_Controllers() { JavaClasses importedClasses = new ClassFileImporter().importPackages("com.mycompany.myapp"); ArchRule myRule = classes() .that() .resideInAPackage("..service..") .should() .onlyBeAccessed() .byAnyPackage("..controller..", "..service.."); myRule.check(importedClasses); }
Java
복사
new ClassFileImporter().importPackages("com.mycompany.myapp");
⇒ 특정 패키지(com.mycompany.myapp)를 읽어온다.
classes().that()...byAnyPackage(...)
⇒ 내가 확인할 규칙들을 정의해놓는다.
⇒ 위 코드의 규칙을 분석하면, ..service.. 패키지 내에 있는 클래스들은 ..controller....service.. 패키지 내에서만 참조되야한다는 규칙이 된다.
myRule.check(importedClasses);
⇒ 읽어들인 클래스들이 내가 선언한 규칙들(myRule)을 잘 따르는지 확인한다.

JUnit5 확장팩 제공

이러한 기능은 JUnit5에서 확장팩을 제공하기에 애노테이션 기반으로 위의 기능들이나 규칙들을 사용할 수 있다.
@AnalyzeClasses: 클래스를 읽어들여 확인할 패키지 설정
@ArchTest: 확인할 규칙 정의

ArchUnit 사용해보기

1. ArchUnit으로 패키지 의존성 확인해보기

지금 진행중인 프로젝트에서 제공하는 패키지구조는 크게 domain, member, study 로 나눠져있다.
도메인은 Member, Study와 같은 도메인객체가 있고 member, study 패키지는 각각의 도메인에 필요한 서비스, 컨트롤러, 레파지토리가 있다. 이번 챕터에서 ArchUnit으로 확인해볼 내용은 다음과 같다.
패키지 구조
프로젝트 패키지 구조
..member.. 패키지에 있는 클래스는 ..study....member..에서만 참조가 가능하다.
(반대로) ..domain.. 패키지는 ..member.. 패키지를 참조하지 못한다
..study.. 패키지에 있는 클래스는 ..study.. 에서만 참조가 가능하다.
순환참조는 없어야 한다.

ArchUnit 라이브러리를 사용한 테스트 코드 작성

package me.catsbi.inflearnthejavatest.study; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.junit.ArchRules; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.syntax.elements.ClassesShouldConjunction; import org.junit.jupiter.api.Test; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; public class ArchTests { @Test void packageDependencyTests() { JavaClasses classes = new ClassFileImporter().importPackages("me.catsbi.inflearnthejavatest"); //..member.. 패키지에 있는 클래스는 ..study..와 ..member..에서만 참조가 가능하다. ArchRule domainPackageRule = classes().that().resideInAPackage("..domain..") .should().onlyBeAccessed().byClassesThat() .resideInAnyPackage("..study..", "..member..", "..domain.."); domainPackageRule.check(classes); //TODO (반대로) ..domain.. 패키지는 ..member.. 패키지를 참조하지 못한다. ArchRule memberPackageRule = noClasses().that().resideInAPackage("..domain..") .should().accessClassesThat().resideInAPackage("..member"); memberPackageRule.check(classes); //TODO ..study.. 패키지에 있는 클래스는 ..study.. 에서만 참조가 가능하다. ArchRule studyPackageRule = noClasses().that().resideOutsideOfPackage("..study..") .should().accessClassesThat().resideInAnyPackage("..study.."); studyPackageRule.check(classes); //TODO 순환참조는 없어야 한다. ArchRule freeOfCycles = slices().matching("..inflearnthejavatest.(*)..") .should().beFreeOfCycles(); freeOfCycles.check(classes); }
Java
복사
new ClassFileImporter().importPackages("me.catsbi.inflearnthejavatest");
⇒ 작성한 패키지(String) 내에 있는 모든 클래스들을 가져와 반환한다.
domain 패키지 내의 클래스들은(classes().that().resideInAPackage("..domain.."))
오직, study, member, domain패키지 내의 클래스에서만 접근이 가능하다.
(.should().onlyBeAccessed().byClassesThat().resideInAnyPackage("..study..", "..member..", "..domain..");)
domain패키지내의 클래스는 (noClasses().that().resideInAPackage("..domain.."))
member 패키지내의 클래스를 참조할 수 없다.(.should().accessClassesThat().resideInAPackage("..member");)
study패키지 외부 클래스는 (noClasses().that().resideOutsideOfPackage("..study..")) study 패키지 내부에 접근할 수 없다.(.should().accessClassesThat().resideInAnyPackage("..study..");)
inflearnthejavatest패키지 하위의 패키지상관없이 모든 클래스는(slices().matching("..inflearnthejavatest.(*)..")) 순환참조가 없어야 한다.(.should().beFreeOfCycles();)

JUnit 5 에 연동하기

ArchUnit 라이브러리 API를 위와같이 직접 호출해서 구현하는것은 양이 많아질수록 비용소모가 너무 크기에 애노테이션 기반의 기능도 제공을 한다.
@AnalyzeClasses 애노테이션으로 클래스를 읽어들여 확인할 패키지를 설정하 ㄹ수 있고
@ArchTest 애노테이션으로 확인할 규칙을 정의할수도 있다.
사용법은 정말 간단하다. 위에서 작성한 테스트 클래스의 클래스 레벨에 애노테이션으로 @AnalyzeClasses을 붙혀서 속성으로 사용할 클래스를 지정해주고, 각각의 규칙들을 테스트 메서드가 아니라 바로 룰을 작성한 뒤 @ArchTest 애노테이션을 붙혀주면 된다.
@AnalyzeClasses(packagesOf = App.class) public class ArchTests { @ArchTest ArchRule domainPackageRule = classes().that().resideInAPackage("..domain..") .should().onlyBeAccessed().byClassesThat() .resideInAnyPackage("..study..", "..member..", "..domain.."); @ArchTest ArchRule memberPackageRule = noClasses().that().resideInAPackage("..domain..") .should().accessClassesThat().resideInAPackage("..member"); @ArchTest ArchRule studyPackageRule = noClasses().that().resideOutsideOfPackage("..study..") .should().accessClassesThat().resideInAPackage("..study.."); @ArchTest ArchRule freeOfCycles = slices().matching("..inflearnthejavatest.(*)..") .should().beFreeOfCycles(); }
Java
복사
→ 하나하나의 규칙은 위에서 작성한 규칙들이다.
→ App.class가 위치한 위치로부터 하위 패키지 모든 클래스를 포함한다.

클래스 의존성 확인하기

ArchUnit을 이용하면 패키지 의존성 뿐 아니라 클래스의 의존성도 확인할 수 있다.

확인하려는 의존성과 테스트할 내용

확인하려는 클래스 의존성
1.
StudyController는 StudyService와 StudyRepository를 사용할 수 있다.
2.
Study로 끝나는 클래스는 ..study.. 패키지 내에 있어야 한다.
3.
StudyRepository는 StudyService와 StudyController를 사용할 수 없다.

테스트할 내용(규칙)들을 코드로 구현하기

package me.catsbi.inflearnthejavatest.study; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; import me.catsbi.inflearnthejavatest.App; import javax.persistence.Entity; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; @AnalyzeClasses(packagesOf = App.class) public class ArchClassTests { @ArchTest ArchRule controllerClassRule = classes().that().haveSimpleNameEndingWith("Controller") .should().accessClassesThat().haveSimpleNameEndingWith("Service") .orShould().accessClassesThat().haveSimpleNameEndingWith("Repository"); @ArchTest ArchRule repositoryClassRule = noClasses().that().haveSimpleNameEndingWith("Repository") .should().accessClassesThat().haveSimpleNameEndingWith("Service"); @ArchTest ArchRule studyClassesRule = classes().that().haveSimpleNameEndingWith("Study") .and().areNotEnums() .and().areNotAnnotatedWith(Entity.class) .should().resideInAnyPackage("..study.."); }
Java
복사
havSimpleNameEndingWith(...)
⇒ 파라미터로 넘겨준 문자가 클래스의 끝이 일치해야 한다.
.are().areNotEnums()
⇒ Enum 클래를 제외한다.
.are().areNotAnnotatedWith(Entity.class)
⇒ @Entity 애노테이션이 붙은 클래스를 제외한다.

참고: 해당 코드 Git Repository

checkout number: 4edc6e68dc1d6b720bf846b06e98fa1613b9c926

이전 챕터로