본문 바로가기
프로그래밍/Spring

[Spring Boot] package(패키지)의 역할과 archUnit를 이용하여 구조 정립하기

by 사바라다 2021. 12. 26.

안녕하세요. 오늘은 객체지향에서 package의 역할과 archUnit을 이용하여 정립한 패키지 구조에 대해서 제대로 사용할 수 있도록 제한하는 방법에 대해서 알아보도록 하겠습니다.

패키지(package)의 역할

패키지는 단순하게 말하면 클래스를 위치시키는 디렉토리의 역할을 할 수 있습니다. 클래스는 패키지 아무곳이나 아무렇게 위치를 시켜도 될까요? 기능적으로 안되진 않습니다. 하지만 이렇게 했을 때 괜찮을까요? 개인의 PC에 사용하는 파일을 폴더에 위치시킬때를 생각해봅시다. 파일들을 아무 디렉토리에 두신 경험이 있으시죠? 그럴때 다시 찾고자 할때 찾기 쉬우셨나요? 어렵습니다. 그래서 우리는 파일들도 디렉토리를 나누어서 정리하여 사용합니다. 코딩의 경우는 어떨까요? 코딩은 혼자 하는 일이 아닙니다. 그렇기 때문에 앞서 보았던 개인 PC의 파일을 정리하는 것 보다 정리되지 않았을 때 훨씬 그 파급력은 강하다고 할 수 있습니다. 다른 사람이 만들어둔 클래스를 잘 활용하려면 패키지를 잘 구성하는것은 반드시 중요합니다. 이렇게 관련있는 데이터들을 함께 모아두었을 때 우리는 응집성(cohesion)이 높다고 표현합니다.

class는 관련있는 함수(메서드)와 변수에 대해 응집성있게 조직되고 관리될 수 있도록 합니다. package는 이보다 큰 범위의 의미를 가집니다. package는 관련있는 여러 클래스들에 대해서 조직되고 관리될 수 있게하여 응집성을 높여주는 역할을 합니다. package 안에는 관련있는 class 들이 함께 들어있어야 응집성이 높다고 할 수 있을 것입니다. 아래는 이미지는 외부와 통신하는 interface 패키지입니다. device 패키지안에 DeviceController 클래스와 관련있는 DTO 모델들을 함께 패키지에 위치해둠으로써 device와 관련된 interface에 대해서 해당 패키지는 응집성을 가진것입니다.

패키지 격리하기

위에서 패키지가 하는 역할에 대해서 알아보았습니다. 그리고 우리는 이제 패키지를 잘 활용할 수 있을것 같습니다. 그런데 실제로 회사에서 일은 혼자만 하는게 아닙니다. 팀원들도 있죠. 팀원들과 이야기를 해서 팀에 맞는 적절한 패키지 구조를 잡았고 컨벤션으로 만들어 문서도 만들었습니다. 이제 모두가 컨벤션을 지켜주겠죠?... 라는 낙관론만으로는 괜찮지 않습니다. 누군가의 실수, 새롭게 팀에 들어온사람, 아니면 거기까지 생각이 미치지 못하여 등등 어떠한 이유로라도 우리가 컨벤션으로 새운 구조는 금이갈 수 있습니다. 금이가면 조금씩 조금씩 망가지기 시작하고 세워놓은 정책은 언젠가 안지켜지게 될 수 있습니다. 따라서 우리는 우리가 세운 구조를 지키기위해서 패키지 구조와 이 의존성을 강제할 필요가 있습니다.

archUnit 개요

클래스가 한가지 책임에 집중할 수 있도록(SRP) 유지하기 위해서 archUnit 이라는 테스트 프레임워크를 소개드립니다. archUnit은 package와 class간의 의존성을 허용 및 거부할 수 있고 이게 잘 유지되는지 판단해주는 테스트 프레임워크입니다.

아래는 archUnit 프레임워크의 개발 동기(Motivation)에 대해서 적혀있던 글귀를 제나름대로 번역한 부분입니다. 원문 링크

왜 당신의 아키텍처를 테스트(검증) 해야하는가 ?

대규모 프로젝트에서 작업하는 대부분의 개발자는 경험이 있는 사람이 코드를 보고 시스템이 구성되어야 하는 구성 요소와 상호 작용 방식을 보여주는 멋진 아키텍처 다이어그램을 그린 이야기를 알고 있을 것입니다.

대부분의 큰 프로젝트에서 일하고 있는 개발자들은 개발하게되면서 먼저 프로젝트에 참여한 개발자들의 코드, 그리고 시스템의 상호 작용 방식 및 아키텍처 다이어그램에 대한 스토리를 알게됩니다. 이러한 상황에서 점점더 프로젝트가 커져가며, 유즈케이스는 더 복잡해지며 새로운 개발자는 들어오고 기존에 있던 개발자는 나가는 상황이 반복됩니다. 이러한 상황에서 새로운 기능 개발은 끊임없이 들어옵니다. 이러한 상황이 되면 소스를 수정할 때 영향도에 대한 파악을 할 수 없는 상황에 이르기도합니다. 물론 경험이 많은 시니어 개발자와 아키텍트 분들이 시간을 쏟아 이러한 상황을 바로잡을 수 있습니다. 하지만 만약 컴포넌트를 구성함에 있어서 강제적인 룰이 있고 자동으로 테스트된다면 이러한 상황은 사전에 피할 수 있었을 것입니다.

특히 애자일 프로젝트에서는 아키텍트의 역할이 여러명에서 나누어질 수 있는데 개발자 모두가 컴포넌트 관계에 대해서 이해하고 있어야합니다. 프로젝트가 발전되면 컴포넌트 구조또한 발전됩니다. 당신이 자동화 아키텍처 테스트를 진행하고 있다면 컴포넌트 구조의 규칙을 발전시키면 됩니다. 그러면 자연스럽게 컴포넌트의 구조의 발전으로 이루어지게 됩니다. 이러한 컴포넌트 구조를 신경쓰며 개발한다면 물론 당장의 개발 속도는 감소될 수 있습니다. 하지만 팀의 개발자들이 쉽게 시스템을 이해하고 개발함으로써 결과적으로 개발의 속도가 향상될 것입니다.

archUnit 사용하기

위에서 archUnit을 사용해야하는 동기에 대해서 알아보았습니다. 그렇다면 이제 실제로 archUnit을 사용해보도록 하겠습니다.

환경

이번 샘플에서 사용한 환경은 아래와 같습니다.

  • java : 11
  • spring boot : 2.5.5
  • archunit-junit5 : 0.21.0

테스트 하고자하는 패키지 구조 및 컴포넌트 구조 조건

테스트의 패키지 구조 및 내부 클래스는 아래와 같이 이루어져 있다고 가정해보도록 하겠습니다.

api
 ㄴ interfaces
    ㄴ VersionController 
    ㄴ VersionAdminController
 ㄴ scheduler
    ㄴ VersionAdminScheduler
 ㄴ services
    ㄴ common
       ㄴ CommonService
       ㄴ CommonAdminService

그리고 간단하게 아래와 같은 4가지 케이스에 대해서 archUnit을 이용해서 테스트를 작성해보도록 하겠습니다.

  • api.service 패키지의 클래스는 api.interfaces 패키지 및 api.scheduler 패키지에 있는 클래스에 의해서만 의존을 받을 수 있다.
  • api.intefaces 및 api.scheduler 패키지는 다른 클래스에서 의존할 수 없다.
  • 클래스 이름의 postfix가 AdminApiService라면 AdminController만 접근할 수 있다.
  • api.service 패키지의 클래스 이름은 postfix를 Service로 가져야한다.

테스트 코드 작성

archUnit을 사용하기 위해서는 아래의 모듈을 가져올 필요가 있습니다.

dependencies {
    testImplementation("com.tngtech.archunit:archunit-junit5:0.21.0")
}

그리고 archUnit 테스트를 진행할 테스트 클래스에 @AnalyzeClasses를 통해서 분석하고자하는 package를 아래처럼 넣어주도록 합시다. 만약 멀티 모듈로 되어있다면 해당하는 패키지 path를 모두 넣어주시면됩니다.

@AnalyzeClasses(
   packages = {
       "api"
    }
)
public class ArchitectureTest {
    [..중략..]
}

사전준비는 이것으로 끝입니다. 이제 실제로 한번 테스트를 만들어보겠습니다.

  • api.service 패키지의 클래스는 api.interfaces 패키지 및 api.scheduler 패키지에 있는 클래스에 의해서만 의존을 받을 수 있다.
@ArchTest
public final void rule_1(JavaClasses: importedClasses) {
    ClassesShouldConjunction rule = classes().that().resideInAPackage("..api.service..")
        .should().onlyBeAccessed().byAnyPackage("..api.interfaces..", "..api.scheduler..")

    rule.check(importedClasses)
}
  • api.intefaces 및 api.scheduler 패키지는 다른 클래스에서 의존되어질 수 없다.
@ArchTest
public final void rule_2(JavaClasses: importedClasses) {
    ClassesShouldConjunction rule_1 = classes().that().resideInAPackage("..api.controller..")
            .should().onlyBeAccessed().byAnyPackage()

    ClassesShouldConjunction rule_2 = classes().that().resideInAPackage("..api.scheduler..")
            .should().onlyBeAccessed().byAnyPackage()

    rule_1.check(importedClasses)
    rule_2.check(importedClasses)
}
  • 클래스 이름의 postfix가 AdminApiService라면 AdminController만 접근할 수 있다.
@ArchTest
public final void rule_3(JavaClasses: importedClasses) {
    ClassesShouldConjunction rule = classes().that().haveSimpleNameEndingWith("AdminApiService")
            .should().onlyBeAccessed().byClassesThat().haveSimpleNameEndingWith("AdminController")

    rule.check(importedClasses)
}
  • api.service 패키지의 클래스 이름은 postfix를 Service로 가져야한다.
@ArchTest
public void rule_4(JavaClasses importedClasses) {
    ClassesShouldConjunction rule = classes().that().haveSimpleNameEndingWith("Service")
        .should().resideInAPackage("..api.service..")

    rule.check(importedClasses);
}

테스트 결과 확인

만약 패키지 구조가 rule에 맞지 않는다면 아래와 같이 룰을 충족하지 못하는 class에 대해서 이유와 설명이 함께 노출되어 구조를 다시 잡아줄 수 있습니다.

Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package '..api.service..' should only be accessed by any package ['..api.controller..', '..api.service..', '..api.scheduler..']' was violated (8 times):
Method <com.sabarada.api.interfaces.common.device.DeviceController.createDevice$suspendImpl(com.sabarada.api.interfaces.common.device.DeviceController, java.util.Locale, com.sabarada.core.value.AppType, com.sabarada.api.interfaces.common.device.CreateDeviceRequest, kotlin.coroutines.Continuation)> calls method <com.sabarada.api.service.common.DeviceApiService.createDevice(com.sabarada.core.value.AppType, java.lang.String, java.util.Locale, java.time.LocalTime, kotlin.coroutines.Continuation)> in (DeviceController.kt:25)

마무리

오늘은 이렇게 package의 역할과 이를 잘 활용할 수 있도록 구조를 제한하는 archUnit에 대해서 알아보는 시간을 가져보았습니다.

archUnit에 대해서 좀더 많은 컴포넌트 제한 방법에 대해서는 공식문서의 userGuide를 참고해주세요.

감사합니다.

참조

클린 아키텍처

https://en.wikipedia.org/wiki/Package_principles

라인 블로그 ( port & adapter )

archUnit

댓글