ArchUnit으로 반복되는 경계 리뷰 줄이기

Architecture test는 설계를 대신해주는 도구가 아닙니다. 사람이 매번 확인하던 경계 위반을 빌드에서 먼저 드러내는 안전장치에 가깝습니다.

Sample code

ArchUnit을 도입할 때 제일 조심해야 할 건 규칙을 많이 만드는 일입니다. 규칙이 많으면 아키텍처가 단단해진 것처럼 보이지만, 어느 순간 개발 흐름을 막는 또 다른 승인 절차가 됩니다.

제가 ArchUnit을 쓰는 이유는 거창한 설계 검증이 아닙니다. 리뷰 때마다 반복해서 묻던 경계 질문을 빌드로 옮기기 위해서입니다.

리뷰에서 반복되는 질문을 빌드로 옮기기

아키텍처 규칙은 대부분 어렵지 않습니다.

  • domain이 infrastructure를 참조하면 안 됩니다.
  • application이 web framework 타입에 묶이면 안 됩니다.
  • 특정 app module이 관계없는 bounded context를 직접 가져오면 안 됩니다.
  • adapter가 다른 adapter를 우회 호출하면 안 됩니다.

문제는 이 질문이 매 PR마다 사람에게 돌아온다는 데 있습니다. 리뷰어가 피곤하거나, 변경 범위가 크거나, 급한 배포가 끼면 놓칩니다. 놓친 의존성은 당장 장애를 만들지는 않지만 다음 변경의 선택지를 줄입니다.

ArchUnit은 이 반복 질문을 빌드로 옮기는 장치입니다.

noClasses()
    .that()
    .resideInAPackage("..domain..")
    .should()
    .dependOnClassesThat()
    .resideInAnyPackage("..apps..", "..support..")

이 규칙은 domain이 앱이나 adapter를 몰라야 한다는 하나의 정책만 검증합니다. 복잡하지 않습니다. 대신 누군가 실수로 역방향 의존성을 만들면 바로 실패합니다.

테스트로 남길 규칙을 고르기

ArchUnit을 설계 문서 전체의 자동 채점기로 쓰면 금방 피곤해집니다.

예를 들어 클래스 이름, 패키지 이름, 어노테이션 조합까지 과하게 묶으면 리팩터링 비용이 커집니다. 개발자는 설계를 이해해서 코드를 고치는 게 아니라, 테스트를 만족시키기 위해 우회로를 찾기 시작합니다.

그래서 기준은 단순해야 합니다. 저는 아래 정도를 봅니다.

  • 깨졌을 때 영향이 큰가
  • 리뷰에서 반복해서 지적되는가
  • 규칙을 한 문장으로 설명할 수 있는가
  • 예외가 드문가
  • 실패 메시지를 보고 바로 고칠 수 있는가

이 기준을 통과하지 못하면 ArchUnit보다 문서나 코드 리뷰가 낫습니다.

Bounded Context 경계만 작게 막기

bounded context 기준으로 domain module을 나누면 Gradle 의존성만으로도 많은 경계가 드러납니다. 하지만 패키지 수준에서 새는 의존성은 여전히 생길 수 있습니다.

예를 들어 API 앱은 학습 흐름에 필요한 context만 알아도 됩니다.

noClasses()
    .that()
    .resideInAPackage("..apps.api..")
    .should()
    .dependOnClassesThat()
    .resideInAnyPackage("..apps.backoffice..", "..domain.organization..", "..support..")

이 규칙은 API 앱이 backoffice 전용 context나 support adapter를 직접 참조하지 못하게 합니다. 중요한 건 API 앱을 고립시키는 게 아닙니다. API 앱이 지금 책임지는 흐름에 필요 없는 경계를 넘지 않게 하는 쪽에 가깝습니다.

반대로 backoffice 앱에는 다른 규칙을 둡니다.

noClasses()
    .that()
    .resideInAPackage("..apps.backoffice..")
    .should()
    .dependOnClassesThat()
    .resideInAnyPackage("..apps.api..", "..domain.learning..", "..domain.catalog..", "..support..")

규칙은 대칭이 아닙니다. 각 앱이 맡은 책임이 다르기 때문입니다. 그래서 ArchUnit 규칙도 예쁜 구조보다 해당 모듈이 넘지 말아야 할 선을 기준으로 작성해야 합니다.

테스트가 실패했을 때 고칠 수 있어야 한다

좋은 architecture test는 실패했을 때 선택지가 명확합니다.

  • 잘못된 의존성을 제거합니다.
  • 필요한 API를 해당 bounded context의 port로 옮깁니다.
  • 앱의 책임이 바뀐 게 맞다면 규칙을 함께 수정합니다.

나쁜 테스트는 실패했을 때 회의부터 열립니다. 왜 실패했는지 모르겠고, 예외를 추가해야 할지, 패키지를 옮겨야 할지, 규칙을 지워야 할지 판단이 서지 않습니다. 그런 테스트는 오래가지 못합니다.

ArchUnit 규칙은 처음에는 적을수록 낫습니다. 대신 남긴 규칙은 팀이 진짜로 지키고 싶은 경계여야 합니다.

남겨둘 규칙

ArchUnit은 아키텍처를 대신 설계해주는 도구가 아닙니다. 사람이 이미 합의한 경계를 빌드가 대신 확인하게 만드는 도구입니다.

그래서 처음에는 세 개 정도면 충분합니다.

  • domain은 app/support를 모릅니다.
  • app은 자기 bounded context 밖을 직접 가져오지 않습니다.
  • support adapter는 domain port를 구현하되 app을 호출하지 않습니다.

이 정도만 있어도 실수로 생기는 역방향 의존성은 초기에 드러납니다. 처음부터 많은 규칙을 둘 필요는 없습니다. 반복해서 새는 경계가 보일 때 하나씩 추가하면 됩니다.