ArchUnit으로 아키텍쳐 컨밴션 유지하기!

2025. 5. 20. 23:38· 그 외 공부/기타
목차
  1. ArchUnit이란?
  2. 예시 코드
  3. 적용 테스트
  4. 예외 적용하기
  5. 기대효과와 트레이드오프
  6. 마치며
728x90

ArchUnit이란?

현재 프로젝트에 헥사고날(굳이 이 용어를 쓰고싶진 않지만 더 나은 표현이 없으므로) 아키텍처를 서서히 적용하면서, 관련 컨밴션을 테스트를 통해 강제화하기 위해 ArchUnit을 도입하였다.

 

ArchUnit은 프로젝트의 아키텍처 규칙을 테스트 코드 형태로 작성할 수 있게 도와주는 테스트 라이브러리로, 클래스의 패키지 구조, 접근 제어자, 어노테이션 사용 여부 등 아키텍처 컨벤션을 코드로 명시하고, CI/CD 과정에서 지속적으로 검증할 수 있도록 도울 수 있다.

 

이번 글에선 ArchUnit를 사용해 아래와 같은 아키텍쳐 패키지를 강제해보자.

 

1. ..interfaces.. 패키지의 클래스는 ..application.. 패키지의 클래스만 참조할 수 있다.

2. ..application.. 패키지의 클래스는 ..domain.. 패키지의 클래스만 참조할 수 있다.

3. ..domain.. 패키지의 클래스는 다른 레이어의 패키지에 의존할 수 없다.

4. ..infrastructure.. 패키지의 클래스는 ..application.., ..domain.. 패키지의 클래스만 의존할 수 있다.

 

예시 코드

바로 예시 코드로 넘어가보자.

 

ArchUnit을 Junit과 함께 사용하기 위해선 아래의 의존성이 추가되어야한다.

# build.gradle

{
	...
    # 버전은 원하는 것으로!
    testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.0'
}

 

@AnalyzeClasses를 통해 테스트할 대상 클래스들을 지정할 수 있다.

 

예시코드에선 package 경로를 설정해서 시도해본다.

 

테스트 패키지가 분석 경로에 잡히지 않도록 제외해주는 옵션도 존재한다.

@AnalyzeClasses(
        packages = "com.example.archunit", // com.example.archunit 패키지 하위의 클래스를 모두 검사
        importOptions = ImportOption.DoNotIncludeTests.class // 테스트 패키지는 제외
)
public class ArchitectureConvention {

}

 

다음으로, 지정하고자 하는 룰을 ArchRule 클래스로 변수를 선언한 후 해당 값에 @ArchTest를 선언해주면 된다. 

 

테스트 메서드 작성이 아닌 변수 선언이라는 점에서 방법이 좀 낯설 수 있지만 ArchUnit Junit5 예제코드를 참고하며 천천히 따라가면 된다.

 

코드를 우선 완성시켜놓고 하나씩 살펴보자.

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;

@AnalyzeClasses(
        packages = "com.example.archunit",
        importOptions = ImportOption.DoNotIncludeTests.class
)
public class ArchitectureConvention {

    private static final String INTERFACE = "Interface";
    private static final String APPLICATION = "Application";
    private static final String DOMAIN = "Domain";
    private static final String INFRASTRUCTURE = "Infrastructure";

    @ArchTest
    static final ArchRule layeredArchitectures = layeredArchitecture()
            .consideringOnlyDependenciesInLayers()
            .layer(INTERFACE)
            .definedBy("..interfaces..")
            .layer(APPLICATION)
            .definedBy("..application..")
            .layer(DOMAIN)
            .definedBy("..domain..")
            .layer(INFRASTRUCTURE)
            .definedBy("..infrastructure..")

            .whereLayer(INTERFACE)
            .mayOnlyAccessLayers(APPLICATION)

            .whereLayer(APPLICATION)
            .mayOnlyAccessLayers(DOMAIN)

            .whereLayer(DOMAIN)
            .mayNotAccessAnyLayer()

            .whereLayer(INFRASTRUCTURE)
            .mayOnlyAccessLayers(APPLICATION, DOMAIN);
}

1. Architectures.layeredArchitecture()

LayerdArchitecture.DependencySettings 인스턴스를 반환한다.

 

해당 클래스에는 레이어드 아키텍쳐와 관련된 용어를 기반으로 메서드가 정의되어있어 컨밴션을 글 작성하듯 쉽게 작성할 수 있다.

 

코드의 layer(), whereLayer(), mayOnlyAccessLayer() 등이 그 예시이다.

 

레이어드 아키텍처 뿐 아니라 onionArchitecture()를 통해 어니언 아키텍처(처음 들어봄)를 사용할수도 있는데, 해당 인스턴스에는 어니언 아키텍처에서 사용하는 용어를 기반으로 메서드가 작성되어있다.

 

2. consideringOnlyDependenciesInLayers

의존성 규칙을 적용할 범위에 대한 설정이다.

 

아래와 같이 세 가지 설정이 가능하다.

consideringOnlyDependenciesInLayers()는 사용자가 정의한 레이어에서만 의존성을 검사하도록 한다.

 

조금 더 엄격한 방식을 적용하고 싶다면 consideringAllDependencies()를 적용할수도 있는데, 이렇게 되면 외부 라이브러리까지 검사대상에 포함된다.

 

만약 consideringAllDependencies() 적용 후 특정 레이어가 다른 어떠한 레이어에도 접근하지 못하도록 (후술 할)mayNotAccessAnyLayer()가 설정되었다면 LocalDate, BigDecimal 같은 JDK 기본 객체들도 사용이 불가하다.

 

개발 편의 측면에서 사실상 적용하기 매우 어려운 선택일 것으로 보인다.

 

3. access

access 설정은 이해하는데 크게 어렵지 않다. 그냥 그대로 읽으면 된다.

    .layer(INTERFACE)
    .definedBy("..interfaces..")
    .layer(APPLICATION)
    .definedBy("..application..")
    .layer(DOMAIN)
    .definedBy("..domain..")
    .layer(INFRASTRUCTURE)
    .definedBy("..infrastructure..")
    
    .whereLayer(INTERFACE)
    .mayOnlyAccessLayers(APPLICATION)
    
    .whereLayer(APPLICATION)
    .mayOnlyAccessLayers(DOMAIN)
    
    .whereLayer(DOMAIN)
    .mayNotAccessAnyLayer()
    
    .whereLayer(INFRASTRUCTURE)
    .mayOnlyAccessLayers(APPLICATION, DOMAIN);

 

..interfaces.. 패키지로 정의된 INTERFACE는 ..application.. 패키지로 정의된 APPLICATION에만 의존할 수 있고,

 

APPLICATION은 ..domain.. 패키지로 정의된 DOMAIN에만 의존할 수 있으며,

 

DOMAIN은 어느 레이어에도 의존할 수 없다.

 

마지막으로 INFRASTRUCTURE는 APPLICATION과 DOMAIN 모두 접근할 수 있다.

 

적용 테스트

그렇다면 컨밴션이 잘 적용되었는지 테스트해보자.

package com.example.archunit.application;

// application -> domain 의존
import com.example.archunit.domain.Order;

public class OrderService {
    private Order order;
}

먼저 application 영역에서 domain 영역의 객체를 의존해본다. 컨밴션에 허용되므로 테스트에 성공해야한다.

 

이번엔 반대로 테스트 실패 케이스를 만들어보자.

package com.example.archunit.domain;

// domain -> interface 의존
import com.example.archunit.interfaces.OrderController;

public class Order {
    private OrderController orderController;
}

domain 영역의 객체가 interface 영역의 객체를 의존하도록 설정한다. 컨밴션에 허용되지 않아 테스트에 실패해야한다.

 

예상대로 테스트에 실패하였으며, 어떤 컨밴션을 어겼는지에 대한 테스트 로그도 확인할 수 있다.

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'Layered architecture considering only dependencies in layers, consisting of
layer 'Interface' ('..interfaces..')
layer 'Application' ('..application..')
layer 'Domain' ('..domain..')
layer 'Infrastructure' ('..infrastructure..')
where layer 'Interface' may only access layers ['Application']
where layer 'Application' may only access layers ['Domain']
where layer 'Domain' may not access any layer
where layer 'Infrastructure' may only access layers ['Application', 'Domain']' was violated (1 times):
Field <com.example.archunit.domain.Order.orderController> has type <com.example.archunit.interfaces.OrderController> in (Order.java:0)

	at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:94)
	at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:86)
	at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:347)
	at com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor$ArchUnitRuleDescriptor.execute(ArchUnitTestDescriptor.java:168)
	at com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor$ArchUnitRuleDescriptor.execute(ArchUnitTestDescriptor.java:151)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

 

예외 적용하기

컨밴션은 어느 수준의 엄격한 강제성을 협의하고 만들어지지만 실무에선 간혹 이를 회피해야할 코드가 발생하기 마련이다. 

 

위 실패 예시 케이스의 Order <-> OrderController 간의 의존성을 ignore 설정을 통해 회피해보자.

 

ignore 설정은 패키지 혹은 클래스 단위로 지정할 수 있는데, 간단하게 클래스로 지정을 해본다.

    .layer(INTERFACE)
    .definedBy("..interfaces..")
    .layer(APPLICATION)
    .definedBy("..application..")
    .layer(DOMAIN)
    .definedBy("..domain..")
    .layer(INFRASTRUCTURE)
    .definedBy("..infrastructure..")
    ...
    .ignoreDependency(Order.class, OrderController.class);

인자 순서가 중요하다. origin과 target 순서로 파라미터를 입력받는데, 간단히 설명하면 "origin은 target을 주입받아도 된다"로 이해하면 된다.

 

위 입력한 예시 코드는 설명대로 Order가 OrderController에 의존할 수 있다는 의미이다.

 

해당 예외코드를 추가한 후 이전에 실패한 테스트를 다시 한 번 수행해보자.

예상한대로 테스트에 성공했다!

 

기대효과와 트레이드오프

ArchUnit의 기대효과는 명백하다.

 

1. 일관된 코드 품질을 유지할 수 있다.

2. 아키텍처 규칙을 테스트를 통해 문서화할 수 있다.

 

반면에 고려해야할 점도 존재한다.

 

ArchUnit을 통한 아키텍처 검사는 테스트 수행 시점에만 알 수 있다는 것이다.

 

다시 말해, 컨밴션 위반 여부를 컴파일 시점엔 알 수 없으므로 주기적으로 테스트를 돌려주지 않으면 컨밴션 위반 여부를 개발 막바지에 발견하게 되어 코드 전체를 수정해야하는 경우가 발생할 수 있다.

 

이는 곧 개발 생산성의 저하로 이어질 수 있으므로, 컨밴션 유지를 위한 적응 기간이 선행되어야할 것으로 보인다.

 

마치며

위의 예시는 ArchUnit을 아주 제한적으로만 사용한 케이스일 뿐이며, 패키지 구조 뿐 아니라 클래스 네이밍, 어노테이션 강제 여부 등 보다 세밀한 제어 또한 가능하다.

 

아래 네이버 기술 블로그의 글을 참고해도 좋을 것 같다!

 

https://d2.naver.com/helloworld/9222129

 

 

저작자표시 (새창열림)

'그 외 공부 > 기타' 카테고리의 다른 글

Replication Lag 해결전략 정리하기!  (2) 2025.01.17
Static Method를 Mocking 하게 해줘야할까?  (0) 2024.10.24
[Gradle] settings.gradle에 멀티 프로젝트 설정하기  (0) 2023.06.09
메일 서버의 동작 구조와 프로토콜 (SMTP, POP3, IMAP)  (0) 2022.08.05
리눅스 크론탭(crontab) 사용법  (0) 2021.07.31
  1. ArchUnit이란?
  2. 예시 코드
  3. 적용 테스트
  4. 예외 적용하기
  5. 기대효과와 트레이드오프
  6. 마치며
'그 외 공부/기타' 카테고리의 다른 글
  • Replication Lag 해결전략 정리하기!
  • Static Method를 Mocking 하게 해줘야할까?
  • [Gradle] settings.gradle에 멀티 프로젝트 설정하기
  • 메일 서버의 동작 구조와 프로토콜 (SMTP, POP3, IMAP)
SeongOnion
SeongOnion
서버는 꺼지지 않아요
조무래기 코딩서버는 꺼지지 않아요
SeongOnion
조무래기 코딩
SeongOnion
전체
오늘
어제
  • 분류 전체보기 (166)
    • 알고리즘 (81)
      • 이론 (8)
      • 문제풀이 (73)
    • 언어 (15)
      • Python (9)
      • JavaScript (1)
      • JAVA (5)
    • 데이터베이스 (5)
    • 프레임워크 (15)
      • Django (7)
      • Spring (8)
    • 그 외 공부 (37)
      • 운영체제 (1)
      • 자료구조 (14)
      • 네트워크 (5)
      • CS (2)
      • 기타 (6)
      • 트러블 슈팅 (9)
    • 프로젝트 (0)
    • 개발자취 (8)
    • 회고 (3)
    • 주저리주저리 (1)
    • 기타 (비개발) (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 오픈소스
  • 코딩
  • BFS
  • 그리디알고리즘
  • DRF
  • 정렬 알고리즘
  • 프로그래머스
  • 컨트리뷰트
  • Django
  • 파이썬
  • 코딩테스트
  • 장고
  • 스택
  • 자바
  • 트러블 슈팅
  • spring
  • 알고리즘
  • 이진탐색
  • 소수
  • 개발자
  • 브루트포스
  • BFS/DFS
  • 큐
  • 백준
  • 웹
  • 에라토스테네스의 체
  • 투 포인터 알고리즘
  • DP
  • 데이터베이스
  • 회고

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
SeongOnion
ArchUnit으로 아키텍쳐 컨밴션 유지하기!
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.