(5) - Hexagonal 아키텍처 기반 설계 - (3)

S-J LEE·2024년 7월 4일
0

OD

목록 보기
5/5

ArchUnit 사용 시 발생했던 문제점 및 해결 스토리

이번 글에서는 ArchUnit을 사용하면서 발생했던 문제점과 이를 해결한 경험에 대해 이야기해 보겠습니다. 아키텍처 규칙을 코드 레벨에서 검증하는 과정에서 여러 도전과제를 만나게 되는데, 이러한 경험을 통해 더 나은 아키텍처 설계와 유지보수성을 확보할 수 있었습니다.

1. 잘 작동하던 ArchUnit 테스트의 갑작스런 실패

기존에 잘 작동하던 ArchUnit 테스트가 새로운 빈 모듈을 추가하면서 에러가 발생하는 상황을 겪었습니다. 해당 테스트는 다음과 같습니다:

@Test
fun `adapters should be annotated with PersistenceAdapter`() {
    val rule =
        classes().that()
            .resideInAPackage("..adapter..out..persistence..")
            .and()
            .haveSimpleNameEndingWith("Adapter")
            .should()
            .beAnnotatedWith(PersistenceAdapter::class.java)
    rule.check(importedClasses)
}

이 테스트의 목적은 adapter/out/persistence 패키지 내의 클래스 중 xxAdapter로 끝나는 클래스가 반드시 PersistenceAdapter 어노테이션을 가져야 한다는 것을 검증하는 것입니다. 그런데 새로운 모듈을 추가하는 과정에서 이 테스트가 실패했습니다. adapter/out/persistence 패키지 내부에는 클래스가 없었기 때문에, 상식적으로는 테스트가 성공해야 했습니다. 그러나 에러는 지속되었습니다.

에러 메시지는 다음과 같았습니다:

java.lang.AssertionError: Rule 'classes that reside in a package '..adapter..out..persistence..' and have simple name ending with 'Adapter' should be annotated with @PersistenceAdapter' failed to check any classes. This means either that no classes have been passed to the rule at all, or that no classes passed to the rule matched the `that()` clause. To allow rules being evaluated without checking any classes you can either use `ArchRule.allowEmptyShould(true)` on a single rule or set the configuration property `archRule.failOnEmptyShould = false` to change the behavior globally.

이 오류 메시지를 분석해본 결과, 테스트 규칙이 일치하는 클래스가 없을 때 기본적으로 실패하도록 설정되어 있다는 것을 알게 되었습니다. 즉, adapter/out/persistence 패키지에 클래스가 없더라도 테스트가 실패하는 구조였던 것입니다.

문제 해결 과정

이 문제를 해결하기 위해 다음과 같은 단계들을 거쳤습니다:

  1. 에러 메시지 분석:
    에러 메시지를 통해, ArchUnit 규칙이 일치하는 클래스를 찾지 못했을 때 발생하는 기본 동작을 이해했습니다. 이를 통해 allowEmptyShould(true) 옵션을 사용하여 규칙이 일치하는 클래스가 없더라도 테스트가 실패하지 않도록 설정할 수 있음을 알게 되었습니다.

  2. 테스트 코드 수정:
    다음과 같이 .allowEmptyShould(true) 옵션을 추가하여 테스트를 수정했습니다:

    @Test
    fun `adapters should be annotated with PersistenceAdapter`() {
        val rule =
            classes().that()
                .resideInAPackage("..adapter..out..persistence..")
                .and()
                .haveSimpleNameEndingWith("Adapter")
                .should()
                .beAnnotatedWith(PersistenceAdapter::class.java)
                .allowEmptyShould(true)
        rule.check(importedClasses)
    }
  3. 테스트 실행:
    수정된 테스트를 실행한 결과, 성공적으로 통과하게 되었습니다. 이를 통해 테스트 코드의 유연성을 높일 수 있었고, 더 이상 빈 패키지로 인한 불필요한 테스트 실패를 방지할 수 있었습니다.
    알겠습니다. 토픽 2를 세련된 프로그래머로서 다시 재구성해 보겠습니다.


2. 새로운 모듈 작성 시 패키지 구조 및 네이밍에 대한 고민과 해결

새로운 모듈을 작성하면서, 특히 외부 서비스 API를 호출하는 어댑터를 작성할 때 패키지 구조와 네이밍에 대한 고민이 많았습니다. 이러한 고민을 해결하는 과정에서 ArchUnit이 큰 도움이 되었습니다.

문제 상황

외부 서비스 API를 호출하여 주식 정보를 가져오는 어댑터를 작성해야 했습니다. adapter/out까지는 명확했지만, 그 아래의 구조를 어떻게 할지 고민이 되었습니다. 최종적으로 다음과 같은 패키지 구조와 클래스를 작성했습니다:

adapter/out/api/stockinformationservice

package com.example.stock.adapter.out.api.stockinformationservice

@ExternalApiAdapter
class StockInformationAdapter : StockInformationPort {
    /** TODO: stock의 information을 가져오는 service는 다양할 수 있어서,
     * 정확히 증권사 open-api를 사용해서 데이터를 가져오는 서비스임을 알리는 네이밍이 필요하다. */
    override fun getCurrentTop20StocksByTradingVolume(): List<SimpleStockDTO> {
        return emptyList() // TODO: 실제 구현 필요
    }
}

여기서 @ExternalApiAdapter라는 어노테이션을 만들어 외부 API 어댑터임을 명시했습니다. 하지만 패키지 구조에 대한 확신이 없었습니다.

ArchUnit을 통한 해결

ArchUnit 테스트를 작성하면서 패키지 구조와 네이밍에 대한 새로운 관점을 얻었습니다. 처음에는 stockinformationservice라는 패키지 구조가 적절하다고 생각했지만, 테스트를 작성하면서 더 범용적이고 일관된 구조가 필요하다는 것을 깨달았습니다. 다음은 ArchUnit 테스트를 작성하면서 얻은 인사이트입니다:

  1. 패키지 구조의 일관성:
    특정 서비스에 국한된 패키지 구조는 확장성에 문제가 있음을 깨달았습니다. 다양한 외부 API를 추가할 때마다 패키지 구조를 변경해야 하는 문제를 피하기 위해, 더 범용적인 패키지 구조가 필요했습니다.

  2. 규칙의 일반화:
    @ExternalApiAdapter 어노테이션을 가지는 모든 클래스가 adapter/out/api 패키지에 있어야 한다는 규칙을 설정했습니다. 이를 통해 새로운 외부 API 어댑터가 추가될 때마다 일관된 구조를 유지할 수 있었습니다.

@Test
fun `external API adapters should reside in adapter/out/api package`() {
    val rule =
        classes().that()
            .areAnnotatedWith(ExternalApiAdapter::class.java)
            .should()
            .resideInAPackage("..adapter..out..api..")
    rule.check(importedClasses)
}

이 테스트를 통해 패키지 구조의 일관성을 강제할 수 있었고, 새로운 외부 API 어댑터를 추가할 때마다 규칙을 쉽게 검증할 수 있었습니다.

최종 패키지 구조

결국 패키지 구조를 adapter/out/api로 설정했습니다. 이는 다양한 외부 API 어댑터를 수용할 수 있는 범용적인 구조로, 유지보수성과 확장성을 높이는 데 큰 도움이 되었습니다.

package com.example.stock.adapter.out.api

@ExternalApiAdapter
class StockInformationAdapter : StockInformationPort {
    /** TODO: stock의 information을 가져오는 service는 다양할 수 있어서,
     * 정확히 증권사 open-api를 사용해서 데이터를 가져오는 서비스임을 알리는 네이밍이 필요하다. */
    override fun getCurrentTop20StocksByTradingVolume(): List<SimpleStockDTO> {
        return emptyList() // TODO: 실제 구현 필요
    }
}

이 경험을 통해 ArchUnit이 단순히 아키텍처 규칙을 검증하는 도구를 넘어, 패키지 구조와 네이밍을 개선하는 데도 큰 도움이 된다는 것을 깨달았습니다.


이와 같이 ArchUnit을 활용하여 새로운 모듈 작성 시 패키지 구조와 네이밍을 정리하는 과정을 공유해 보았습니다. ArchUnit은 단순히 아키텍처 규칙을 검증하는 것을 넘어서, 코드의 일관성과 유지보수성을 높이는 데 큰 역할을 합니다. 앞으로도 ArchUnit을 적극 활용하여 아키텍처를 고도화해 나갈 예정입니다.

다음 포스팅에서는 도메인 계층의 고도화를 진행하고 포스팅 해보려고 합니다.


profile
MSA 와 관련된 기반 기술에 관심이 많습니다.

0개의 댓글