밤 늦게까지 여는 카페

코드 컨벤션, 아키텍처도 잘 지키고 있는지 테스트 케이스를 만들 수 있습니다! – ArchUnit vs Konsist 본문

For Fun/잡학 지식

코드 컨벤션, 아키텍처도 잘 지키고 있는지 테스트 케이스를 만들 수 있습니다! – ArchUnit vs Konsist

Jㅐ둥이 2026. 1. 12. 23:45
반응형

안녕하세요. 오늘은 코드 컨벤션, 구조를 테스트할 수 있는 도구인 ArchUnit과 Konsist에 대해 공부한 내용을 정리해보려고 합니다.


1. ArchUnit

1.1. ArchUnit이란?


ArchUnit은 Java 및 Kotlin 코드베이스의 아키텍처 규칙을 테스트하기 위한 정적 분석 라이브러리입니다.

Java 바이트코드를 분석해서

  • 패키지 의존성
  • 레이어 구조
  • 클래스/메서드 명명 규치4

등 다양한 아키텍처 제약사항을 코드로 표현하고 테스트할 수 있습니다.

  • 그래서 "Arch"Unit 인 것 같습니다!

Kotlin도 지원하므로, JVM 기반 프로젝트 전반에 적용할 수 있습니다.

1.2. ArchUnit이 Java 바이트코드를 분석하는 방법


그렇다면 ArchUnit은 Java 바이트코드를 어떻게 분석할까요?

ArchUnit은 importPackages("..") 메서드에 넘겨준 패키지 경로를 기준으로 .class 파일들을 찾아 파싱합니다.

내부적으로는 대략 다음 순서로 동작합니다.


1. importPackages("..")가 호출되면

2. classpath 기반으로 해당 패키지에 대응되는 위치(폴더 또는 jar)를 찾고

3. 그 위치에서 .class 파일들을 리스팅한 뒤

4. 각 .class를 바이트코드(ASM 등)를 이용해 파싱해서 JavaClasses 모델 / 의존성 그래프를 구성합니다.

이렇게 만들어진 JavaClasses를 대상으로 noClasses(), classes(), that() 같은 DSL을 사용해 규칙을 정의하고 검증하게 됩니다.

1.3. 장점

바이트코드 기반 분석


Java 바이트코드를 직접 분석하기 때문에, 컴파일 결과 기준으로 정확한 클래스/메서드 정보를 얻을 수 있습니다.

Lombok, Kotlin 등 소스 레벨에서 보이지 않는 요소들도 컴파일 결과 기준으로 다룰 수 있다는 장점이 있습니다.

성숙한 프로젝트


2017년부터 개발된 프로젝트로, 비교적 안정성이 높고 레퍼런스와 유즈케이스가 풍부합니다.

Java 진영에서 “아키텍처 테스트” 하면 ArchUnit이 가장 먼저 언급될 정도로 생태계가 잘 구성되어 있습니다.

1.4. 단점

Kotlin 고유 기능에 대한 표현력이 떨어짐


바이트코드 기준으로 분석하다 보니, Kotlin의 data class, sealed class, inline class 같은 개념을 검증할 때 직관적이지 않습니다.

예를 들어 “data class인지” 확인하려면 data 키워드를 보는 게 아니라

final 여부, componentN 메서드, copy 메서드 등 바이트코드 패턴을 조합해서 추론해야 합니다.

Kotlin의 top-level 선언 처리


Kotlin에서는 클래스 밖에 top-level 함수/프로퍼티를 선언할 수 있는데,
JVM 바이트코드로 컴파일될 때는 {파일이름}Kt와 같은 synthetic 클래스의 static 멤버로 변환됩니다.

ArchUnit 입장에서는 이런 synthetic 클래스 기준으로만 보이기 때문에, “원래 Kotlin 소스에서 어떻게 생겼는지”를 직관적으로 보기가 어렵습니다.

“바이트코드 기준으로 정확하게 본다”는 장점이
Kotlin 특성을 풍부하게 활용하고 싶은 상황에서는 오히려 단점으로 작용되는 것이 아닌가 싶네요.

2. Konsist

2.1. Konsist란?

Konsist는 Kotlin 코드베이스의 일관성과 아키텍처 규칙을 검증하는 정적 분석 라이브러리입니다.

ArchUnit이 JVM 바이트코드를 분석하는 반면
Konsist는 Kotlin 전용으로 설계되어

  • data class
  • sealed class
  • companion object
  • extension function

같은 Kotlin 고유 기능을 더 자연스럽게 다룰 수 있습니다.

2.2. Konsist가 소스 파일을 분석하는 방법

Konsist는 Kotlin 소스 파일(.kt)을 직접 파싱해서 선언(Declaration) 트리를 만들고, 그 위에서 규칙 검증을 수행합니다.

대략적인 흐름은 다음과 같습니다.


1. Konsist.scopeFromProject(), scopeFromModule(), scopeFromPackage() 등의 API로 스코프를 생성하면

2. (프로젝트/모듈/패키지 기준으로) 분석 대상 Kotlin 파일(.kt)들을 수집하고

3. 수집한 각 .kt 파일을 Kotlin Compiler PSI(파서)로 파싱한 뒤

4. 파싱 결과로 KoFileDeclaration(= 파일 단위)을 만들고, 그 안에

  • KoClass
  • KoFunction
  • KoProperty

등 선언(Declaration) 트리를 구성합니다.

5. 만들어진 선언 트리를 기반으로 classes(), functions(), properties() 같은 API로 탐색/필터링 하면서 규칙을 검증합니다.

즉, Konsist는 Kotlin 소스 코드 자체를 기준으로 규칙을 정의하는 방식입니다.

2.3. 장점

Kotlin 네이티브 모델


data class, sealed class, companion object, object 등
Kotlin 고유 기능에 대한 네이티브 지원을 제공합니다.

예를 들어 “모든 sealed class는 특정 패키지 안에 있어야 한다” 같은 규칙을 비교적 직관적으로 쓸 수 있습니다.

소스 레벨 표현력이 좋음

바이트코드가 아니라 소스 코드 기준이라

  • 파일 단위
  • top level 함수/프로퍼티
  • KDoc, 주석

등 ArchUnit에서 다루기 까다로운 요소들도 자연스럽게 다룰 수 있습니다.

2.4. 단점

Java 코드와 혼재된 프로젝트에서는 제약


Kotlin 소스코드를 직접 파싱하는 방식이라, Java 코드까지 한 번에 커버하기는 어렵습니다.

Java + Kotlin 혼합 프로젝트에서는 ArchUnit과 병행해서 써야 할 수도 있습니다.

성숙도

2023년에 시작된 비교적 신규 프로젝트라,
ArchUnit에 비해 커뮤니티와 레퍼런스가 적습니다.

타입 추론 한계

현재 기준으로는 타입 추론이 완전하지 않습니다.

예를 들어
private val field1 = JacksonConverter<TestType1>(objectMapper)
와 같은 필드가 있을 때, Konsist는 이 프로퍼티의 타입을 null로 반환하기도 합니다.

이런 경우에는 문자열/정규표현식으로 JacksonConverter와 TestType1을 직접 파싱해야 해서, 규칙이 복잡해질 수 있습니다.

3. ArchUnit vs Konsist – 언제 무엇을 쓸까?

3.1. Konsist 쪽이 나은 경우

Kotlin 고유 개념을 직접 다루고 싶을 때

  • 모든 data class는 domain.model 패키지 아래에만 있어야 한다.
  • sealed class 계층 구조를 특정 디렉터리 구조와 맞추고 싶다.

파일/선언 단위로 규칙을 정의하고 싶을 때

  • xxxRepository.kt 파일에는 반드시 @Repository 클래스가 하나 있어야 한다.
  • 특정 패키지의 모든 함수는 suspend 이어야 한다.

단, 위에서 언급한 것처럼 타입 정보(특히 제네릭 타입) 를 정확하게 읽어야 하는 규칙은
현 시점에서는 다소 불편할 수 있습니다.

3.2. ArchUnit 쪽이 나은 경우

Java + Kotlin 혼합 프로젝트


전체 JVM 바이트코드를 기준으로 보기 때문에 언어에 상관없이 규칙을 강제할 수 있습니다.


패키지/레이어/의존성 구조 검증

  • ..controller.. 패키지는 ..service..까지만 의존해야 한다.
  • ..domain.. 패키지는 ..infra..에 의존하면 안 된다.


이런 “레이어링/의존성” 규칙은 ArchUnit이 훨씬 강력하고 표현도 간단합니다.


정적 타입 정보가 중요한 규칙


ArchUnit은 바이트코드 기준이라 제네릭 타입, 상속 관계, 인터페이스 구현 여부 등을 정확하게 확인할 수 있습니다.

Konsist에서 타입 추론이 안 되는 지점도 ArchUnit에서는 정확히 볼 수 있는 경우가 많습니다.

여기까지가 ArchUnit과 Konsist를 비교해보면서 정리한 내용입니다.

둘 다 장단점이 있어서 하나만 고르기보다는

  • 아키텍처/레이어/의존성 → ArchUnit
  • Kotlin 소스 구조/언어 특화 규칙 → Konsist

처럼 적절히 나눠 쓰는 게 좋은 것 같습니다!

반응형