안녕세계
[Kotlin] 아키텍처 테스트 (feat. ArchUnit) 본문
[Kotlin] 아키텍처 테스트 (feat. ArchUnit)
Junhong Kim 2024. 10. 31. 03:48ArchUnit이란?
보통 팀마다 정해져있는 아키텍처가 문서로 작성되어 있거나 기존 코드의 아키텍처를 참조해가며, 개발을 진행하는 경우가 많을 것 입니다. 아키텍처에 위배되는 개발이 되는 것을 미연에 방지하기 위해서는 어떤 방법이 좋을까요? 팀에서 정의한 아키텍처를 위배했을때 코드리뷰를 통해서 잘못된 아키텍처를 바로 잡을 수도 있겠지만, 코드리뷰에서는 비즈니스 로직에 대한 검증을 중점적으로 하고 아키텍처와 관련된 부분은 자동화하는 편이 더 효율적일 것입니다.
ArchUnit은 Java 애플리케이션의 아키텍처 규칙을 코드로 정의하고 테스트할 수 있게 해주는 오픈 소스 라이브러리입니다. 애플리케이션의 아키텍처가 복잡해지고 방대해질수록 각 계층 간의 의존성을 유지하고 코드의 일관성을 보장하는 것이 중요해집니다. ArchUnit을 사용하면 아키텍처 규칙을 코드로 정의하고, 코드가 규칙에 맞게 개발되었는지 검증하는 테스트를 자동으로 수행할 수 있습니다.
ArchUnit 시작하기
ArchUnit 테스트는 Java의 UnitTest와 같은 방식으로 작성되며 모든 Java UnitTest 프레임워크를 사용하여 작성할 수 있습니다. Kotlin으로 ArchUnit 테스트를 작성하려면 우선 `build.gradle.kts`에 ArchUnit 의존성을 추가합니다. ArchUnit은 Junit5 지원을 하고 있으며, 본 포스팅에서는 Junit5를 사용합니다.
dependencies {
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
}
ArchUnit 테스트
아키텍처를 테스트하기 위한 패키지 경로를 지정하여 테스트가 가능합니다.
`org.example.demo` 하위의 패키지들의 아키텍처를 테스트하기 위해서는 다음과 같이 입력합니다.
@AnalyzeClasses(packages = ["org.example.demo"]) // 테스트할 패키지 경로
class ArchitectureTest
Junit 테스트 지원으로 테스트 클래스에 `@ArchTest` 어노테이션이 달린 모든 규칙을 평가합니다. ArchUnit 공식홈페이지를 확인해보면, 아키텍처를 체크할 수 있는 것은 7가지로 소개하고 있습니다. 본 포스팅에서는 모든 예제를 다루지는 않고 `종속성 검사`, `상속 검사`, `레이어 검사`에 대해 알아보겠습니다.
1. 패키지 종속성 검사 (Package Dependency Checks)
2. 클래스 종속성 검사 (Class Dependency Checks)
3. 클래스 및 패키지 포함 검사 (Class And Package Containment Checks)
4. 상속 검사 (Inhteritance Checks)
5. 주석 검사 (Annotation Checks)
6. 레이어 검사 (Layer Checks)
7. 사이클 체크 (Cycle Checks)
패키지 종속성 검사
패키지 종속성 검사를 하기 위해 애플리케이션의 패키지 경로를 다음과 같이 생성합니다.
ArchUnit을 작성할 때는 마치 영어 문장을 작성하는 것처럼 직관적입니다.
다음 archRule의 의미는, "source 패키지에 있는 클래스들은 foo 클래스에 의존하면 안된다" 입니다.
@AnalyzeClasses(packages = ["org.example.demo"])
class ArchitectureTest {
@ArchTest
var archRule: ArchRule = noClasses().that().resideInAPackage("..source..")
.should().dependOnClassesThat().resideInAPackage("..foo..")
}
만약, Source 클래스가 다음과 같이 foo에 대한 의존성을 갖게되면 테스트가 실패하게 됩니다.
class Source(
val foo: Foo,
)
상속 검사
두 번째 예로, 상속 검사를 하기 위해 다음과 같이 인터페이스와 클래스를 생성합니다.
interface Connection
class FtpConnection : Connection
class HtmlConnection : Connection
class SshThing : Connection
다음 archRule의 의미는, "Connection을 구현하는 모든 클래스들은 Connection 으로 이름이 끝나야 한다" 입니다.
@AnalyzeClasses(packages = ["org.example.demo"])
class ArchitectureTest {
@ArchTest
var archRule3: ArchRule = classes().that().implement(Connection::class.java)
.should().haveSimpleNameEndingWith("Connection")
}
이때, SshThing이 Connection 구현하게 되면 테스트가 실패하게 됩니다.
레이어 검사
세 번째 예로, 레어어 검사를 하기 위해 다음과 같이 두 패키지에 각각 클래스를 생성합니다.
극단적인 예시이지만, Service 레이어가 Controller 레이어에 접근하는 경우가 있다고 가정해보겠습니다.
class SomeController
class SomeService(
private val someController: SomeController
)
다음 archRule를 통해서 레이어 검사를 하겠습니다.
@AnalyzeClasses(packages = ["org.example.demo"])
class ArchitectureTest {
@ArchTest
var archRule3: ArchRule =
layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
}
layeredArchitecture()를 통해 레이어드 아키텍처를 정의하는 규칙을 시작할 수 있습니다. 그리고 consideringAllDependecies()는 모든 종류의 의존성을 고려하여, 클래스, 인터페이스, 메서드, 생성자 등 모든 의존 관계를 포함하여 검증하는 것을 의미합니다. 즉, 이 옵션은 일반적인 클래스 의존성뿐 아니라 클래스 간에 발생할 수 있는 메서드 호출, 생성자 호출, 필드 접근, 인터페이스 구현, 상속 등 다양한 종류의 종속성을 검토하는 것을 의미합니다.
layer()에서는 "Controller"라는 이름의 레이어를 정의하고, `..controller..` 패키지에 속하는 모든 클래스가 이 레이어에 포함된다고 지정합니다. 다음으로 "Service"라는 이름의 레이어를 정의하고, `..service..` 패키지에 속하는 모든 클래스가 이 레이어에 포함된다고 지정합니다.
이후 Controller 레이어는 다른 레이어에서 접근할 수 없도록 강제하고, Service 레이어는 Controller 레이어에서만 접근을 제한합니다. 본 예시에서는 Service 레이어가 Controller 레이어에 접근하게되면 테스트가 실패하는 것을 확인하실 수 있습니다.
마무리
이처럼 ArchUnit을 사용하면 팀에서 사용하는 아키텍처 검사를 테스트로 자동화하여, 일관성있는 프로젝트를 만들어 나갈 수 있을 것으로 보입니다. 아키텍쳐의 경우 코드리뷰를 통해 의존하는 것보다, 아키텍처 테스트 도구를 사용하여 보다 좀 더 효율적으로 프로젝트를 관리하는 것이 좋을 것 같습니다. 만약, 새로운 프로젝트를 시작하게 된다면 초기 아키텍처 테스트 구성 시간이 좀 걸리더라도 미래를 위해 ArchUnit 테스트를 적용해보는 것을 추천합니다!
'Language > Kotlin' 카테고리의 다른 글
[Kotlin] 제네릭과 무공변 (feat. 공변, 반공변) (1) | 2024.03.31 |
---|---|
[Kotlin] Higher-order Functions (고차 함수) (0) | 2023.07.02 |
[Kotlin] Delegation (위임) (0) | 2023.06.18 |
[Kotlin] Extension Functions (확장 함수) (0) | 2023.06.04 |
[Kotlin] Scope Functions 차이점 (2) | 2023.05.21 |