ArchUnit은 어플리케이션의 아키텍처를 테스트하고 검증하기 위한 라이브러리입니다.
https://www.archunit.org/getting-started
ArchUnit을 사용하면 코드에서 특정 패키지, 네이밍 규칙 등이 잘 지켜지는지 테스트 해볼 수 있습니다.
예를 들면 아래와 같은 것들 말입니다.
- 패키지 구조
- 클래스 의존성
- 네이밍
- 어노테이션
- ...
Spring 개발자라면 아래 그림이 어떤걸 뜻하는지 한 눈에 알아볼 수 있습니다.
마치 관성처럼 아래와 같이 개발을 할 것입니다.
+-------------------------+ +-----------------+ +-----------------+
| Controller | ----> | Service | ----> | Repository |
| | | | | |
| | +-----------------+ +-----------------+
+-------------------------+
- Controller는 웹 어플리케이션에서 사용자와 상호 작용하며 클라이언트 요청을 받습니다.
- Service는 비즈니스 로직이나 데이터 처리와 같은 백엔드 로직을 담당하는 부분입니다.
- Repository는 DB 연결과 같은 작업을 담당하는 부분입니다.
쉽게 말해, ArchUnit으로 코드베이스에서의 아키텍처가 올바르게 작성 되었는지를 테스트 해볼 수 있습니다.
이번 글에서는 ArchUnit을 찍먹해봅시다.
제 로컬 환경은 아래와 같습니다.
- JDK 17
- Springboot 3.1.4
- Kotlin 1.8.22
1. 의존성 추가
// build.gradle.kts
dependencies {
...(중략)
testImplementation("com.tngtech.archunit:archunit-junit5:1.0.1")
}
위와 같이 의존성을 추가해줍시다.
2. 코드
대략 코드는 아래와 같습니다.
// com.test.archunitexam.api.HelloWorldController
@RestController
class HelloWorldController(
val helloWorldService: HelloWorldService
){
@GetMapping("/helloworld")
fun helloWorld(): ResponseEntity<*> {
return ResponseEntity.ok(helloWorldService.hello())
}
}
// com.test.archunitexam.api.service.HelloWorldService
@Service
class HelloWorldService(){
fun hello(): String {
return "HelloWorld"
}
}
패키지 구조는 아래와 같습니다.
│ HelloWorldController.kt
└─service
HelloWorldService.kt
3. ArchUnit 코드 작성
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
import org.junit.jupiter.api.Test
class ArchTest {
@Test
fun `Controller는 Service만 의존한다`() {
val classes = ClassFileImporter().importPackages("com.test.archunitexam.api")
val rule: ArchRule = classes()
.that().haveNameMatching(".*Controller")
.should().onlyHaveDependentClassesThat().resideInAPackage("..service..")
rule.check(classes)
}
}
ArchUnit은 대략 위와 같이 사용가능합니다.
먼저 importPackages를 통해 임포트할 package를 설정합니다.
그리고 ArchRule을 만듭니다.
여기서는 네이밍 규칙을 통해 Controller 규정하고, service package 내의 클래스를 Service로 규정했습니다.
만약 아래와 같은 패키지 구조일 때는 적절하게 수정해주면 됩니다.
└─controller
│ HelloWorldController.kt
└─service
HelloWorldService.kt
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
import org.junit.jupiter.api.Test
class ArchTest {
@Test
fun `Controller는 Service만 의존한다`() {
val classes = ClassFileImporter().importPackages("com.test.archunitexam.api")
val rule: ArchRule = classes()
.that().resideInAPackage("..controller..")
.should().onlyHaveDependentClassesThat().resideInAPackage("..service..")
rule.check(classes)
}
}
4. ArchUnit 코드 작성(2)
아래와 같이 반대 케이스도 테스트 해볼 수 있습니다.
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses
class ArchTest {
... (중략)
@Test
fun `반대로 Service에서 Controller를 의존할 수는 없다`() {
val classes = ClassFileImporter().importPackages("com.test.archunitexam.api")
val rule: ArchRule = noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat().haveNameMatching(".*Controller")
rule.check(classes)
}
}
Controller - Service 의존 관계를 코드로 작성해보며 찍먹해보았습니다.
공식문서에서 다양한 API를 지원하니
필요한 규칙을 테스트 코드에 녹이고 정적 분석 도구로서 활용하면
코드베이스 품질 관리하는데 수월할 것 같습니다.
'개발 이야기 > Springboot' 카테고리의 다른 글
Springboot Webflux에서 blocking 감지하기(with BlockHound) (0) | 2024.02.08 |
---|---|
Springboot에서 P6Spy 통해 쿼리 로깅 (0) | 2024.02.03 |
Springboot에서 AWS SQS 사용 (with Kotlin) (0) | 2024.01.18 |
Testcontainers로 테스트 코드 만들기 2(with Kotlin) (0) | 2024.01.18 |
Testcontainers로 테스트 코드 만들기 (0) | 2024.01.07 |