본문 바로가기
개발 이야기/Springboot

Springboot에서 ArchUnit 사용해서 아키텍처 테스트

by 농개 2024. 1. 27.
반응형

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를 지원하니 

필요한 규칙을 테스트 코드에 녹이고 정적 분석 도구로서 활용하면

코드베이스 품질 관리하는데 수월할 것 같습니다.

 

반응형