문서로만 지키던 아키텍처 규칙, 테스트 코드로 강제하기
들어가며
제가 속한 파트너셀프서비스팀은 코딩 컨벤션을 사내 위키 문서와 AI 에이전트가 참조할 수 있는 규칙 문서에 정의해놓고 개발하고 있습니다. 하지만 그 규칙을 잘 지키고 있는지 확인하는 방법이 코드 리뷰뿐이었다는 점이 문제였습니다. 문서에 있는 규칙은 팀원들이 의식적으로 인지해서 코드를 작성하거나 리뷰를 받는 과정에서 반영해야 했습니다. AI 에이전트를 이용해 규칙을 주입하더라도 세션별 컨텍스트에 따라 규칙을 위반하는 경우가 있었습니다.
저희 팀은 헥사고날 아키텍처를 실용적으로 변형한 형태로 사용하고 있습니다. 인바운드 포트(유스케이스 인터페이스)를 별도로 두지 않고 웹계층이 도메인(domain) 모듈의 서비스(*Service)를 직접 호출한다는 점에서 교과서적 헥사고날 아키텍처와는 조금 다릅니다. 대신 아웃포트(outport) 쪽 구조는 그대로 차용해서 외부 의존을 역전시켰습니다. 실제 구현은 아웃바운드 어댑터(infra·external)에 두고 아웃포트(outport)에 구현체를 제공하도록 했습니다. 외부 API와 캐시 연동이 많고 자주 바뀌는 팀 특성상 외부의 변화가 핵심 비즈니스 로직이 있는 도메인(domain)까지 번지지 않게 하기 위해서입니다. 덕분에 도메인은 외부 API와 독립적으로 유지되고, 테스트하기도 수월해집니다.
이 구조를 지키기 위한 팀 규칙은 .claude/rules/에 문서로 정리돼 있습니다. 아키텍처(레이어 의존) 규칙뿐 아니라 네이밍, 코딩 스타일, 테스트 코드, 예외, 로깅, 커밋 규칙 등 개발 전반의 컨벤션이 있습니다.
팀 내 규칙 파일
architecture.mdc 파일 예시
이 글은 아키텍처, 코딩 스타일 등의 팀 규칙을 아크유닛(ArchUnit)이라는 도구를 사용해 테스트 코드로 강제한 과정을 다룹니다. 아크유닛(ArchUnit)을 들어는 봤지만 직접 써본 적은 없는 분, "우리 팀에도 적용해볼까" 고민하는 분을 위한 글입니다. 특히 다음 다섯 가지 질문 위주로 작성했습니다.
- 적용하면 뭐가 좋나요?
- 어떻게 적용하나요?
- 시행착오는 없었나요?
- 적용 전/후 무엇이 달라졌나요?
- 주의사항은 없나요?
개발 환경: Java 11, Gradle 6.8, Spring Boot 2.4.5, Lombok 1.18.20, ArchUnit 1.3.0, JUnit 5.7.2
아크유닛(ArchUnit)으로 살아있는 규칙 만들기
1. 적용하면 좋은점
문서가 아닌 코드 레벨에서 규칙을 강제할 수 있습니다. 아키텍처 규칙은 한 번 정해두면 끝나는 것이 아니라, 작게는 커밋 단위부터 크게는 배포 단위에서 일관성 있게 지켜져야 합니다. 그런데 규칙 검증을 팀원들의 리뷰에만 의존하면 아래와 같은 이유로 지키기 어렵습니다.
첫째, 리뷰에서 누락됩니다. 모든 MR에서 팀 규칙 위반사항을 매번 확인하지는 못합니다. 더구나 배포가 급하면 "이번엔 일단 머지하고 다음에 고치죠"와 같은 합리화(?)가 반복되면서 위반사항이 우선순위에 밀려 수정되지 않은 채로 남습니다. 이렇게 넘어간 위반사항은 누적됩니다.
둘째, 신규 팀원은 팀 규칙을 지키기 어렵습니다. 팀 규칙에 익숙하지 않은 신규 팀원은 아키텍처를 충분히 익히기 전까지는 무엇이 위반인지조차 모르고 코드를 작성하게 됩니다. 결국 리뷰어가 매번 알려줘야 합니다.
그러다 보니 리뷰어는 같은 지적을 반복합니다. "여기는 생성자 주입으로 바꿔주세요", "이 의존성 방향은 규칙에 어긋납니다" 같은 코멘트가 반복되고 이미 문서에 있는 내용을 중복으로 말하게 되어 리뷰 효율성이 떨어집니다.
이와 같은 과정이 반복되면 리뷰를 하는 사람도 점점 피곤해지고, 신규 팀원의 온보딩도 늦어지며, 아키텍처 품질은 낮아집니다. 그렇게 문서에 적어 놓은 팀 규칙 또한 레거시가 됩니다.
사실 제가 그랬습니다. 올해 초에 파트너셀프서비스팀에 합류하면서 팀 컨벤션 규칙을 이해하고 적용하기까지 시간이 걸렸고, 어느 정도 숙지한 이후에도 놓친 부분이 생겨 코드 리뷰에서 지적받기도 했습니다. 또한 제가 리뷰를 하는 입장에서도 매번 규칙이 잘 지켜졌는지 확인하는 작업도 소모적이었습니다.
1-1 MR에서 컨벤션 위반을 지적한 사례

이러한 불편함을 최소화하기 위해 팀원들과 논의 끝에 "규칙은 리뷰가 아닌 코드 레벨에서 지켜야 한다"는 결론을 내리고 아크유닛(ArchUnit)을 적용하기로 했습니다.
2. 어떻게 적용했나
아크유닛(ArchUnit)을 도입하기 전 크게 두 가지 걱정이 앞섰습니다. 첫째는 이미 규칙을 위반한 레거시 코드가 많아 적용할 엄두가 안 났고, 둘째는 실제 독립 모듈과 테스트를 만들어 적용하는데에 시간적 부담이 있었습니다. 무엇보다 이 작업은 메인 프로젝트에 비해 우선순위가 낮았기 때문에 계속 늦춰졌습니다. 하지만 일관성 없는 규칙이 잔존하면 메인 프로젝트 품질에도 악영향을 미치고 있었기 때문에 우선순위를 높여 작업했습니다. 그런데 막상 적용해보니 걱정했던것보다 코드 수정 범위가 크지 않았습니다. 아크유닛(ArchUnit)에서 제공하는 FreezingArchRule 기능을 이용해 기존 레거시 위반 코드는 동결해 예외처리하고, 신규 위반 코드 검증에 집중해서 작업 범위를 줄였습니다. 이 과정에서 AI 에이전트를 적극 사용해 빠르게 적용 후 테스트 함으로써 작업 부담을 줄일 수 있었습니다.
2-1. 독립 테스트 모듈로 격리
아크유닛(ArchUnit) 테스트는 별도 모듈로 분리해서, 기존의 다른 검사 도구와 섞이지 않도록 했습니다.
2-2. 단계적(Phase)으로 도입
한 번에 모든 규칙을 적용하면 수천 건이 규칙 위반으로 걸리는 문제가 있기 때문에 규칙 강제 수준을 단계별로 나눠 진행했습니다.
Phase별 규칙과 적용 방식
| 단계 | 규칙 | 적용 방식 |
|---|---|---|
| Phase 1 | 헥사고날 레이어 의존성(도메인→인프라/웹/외부 금지 등) | 즉시 강제(기존 위반 없음) |
| Phase 2 | 코딩 규칙(System.out/err, @Autowired 필드 주입 금지) | 동결 후 신규 차단 |
| Phase 3 | 네이밍 컨벤션(Controller, Service) | 동결 후 신규 차단 |
| Phase 4 | 웹계층→outport 직접 의존 금지 & Lombok 규칙 | 동결 후 신규 차단 |
핵심은 위반이 없는 규칙은 즉시 강제하고, 위반이 있는 나머지는 동결로 예외 처리하는 것입니다. 기존에 위반사항이 있는 규칙들 중 비교적 간단하게 수정할 수 있는 코딩 스타일 위반사항은 Phase 2, 3에 두고, 대규모 리팩터링(refactoring)이 필요한 아키텍처 의존성 관련 위반사항은 Phase4에 놓고 점진적으로 개선하기로 했습니다.
2-3. FreezingArchRule로 레거시 동결
아크유닛(ArchUnit)은 FreezingArchRule이라는 기능을 제공합니다. 현재 존재하는 위반을 "동결 파일"에 기록해두고 테스트를 통과시키되, 동결 목록에 없는 새로운 위반만 실패시키는 기능입니다.
이 기능이 중요했던 이유는 코드 규모 때문입니다. 저희 코드베이스에는 "웹계층 → outport 직접 의존" 위반만 수천 건이 있었습니다. 처음부터 이 모든 위반사항을 고친 다음에 규칙을 적용하려 했다면 도입할 엄두도 못 냈을 것입니다. 대신 기존 위반을 동결해 신규 위반을 0건으로 유지하고, 레거시(동결한 기존 위반사항)는 별도 티켓을 통해 점진적으로 줄여나가기로 했습니다.
동결 파일은 Git으로 추적합니다. 그래서 MR에서 동결 항목이 줄면 기존 위반을 실제로 고쳤다는 뜻입니다(테스트 실행 시 자동 제거). 반대로 신규 위반이 생기면 동결 목록에 없으므로 CI 테스트가 실패해 머지가 막힙니다. 정상 흐름에서는 동결 항목이 늘어날 수 없기 때문에, 만약 동결 항목이 늘어난 변경이력(diff)이 보인다면 이는 누군가 신규 위반을 동결로 우회하려 한 경우이므로 리뷰에서 바로 확인할 수 있습니다.
① 즉시 강제 vs 동결: 규칙 정의 방식 (HexagonalArchitectureTest / CodingRuleTest)
② 동결 저장소 설정 (archunit.properties)
③ 동결 파일에 기록된 모습 (archunit_store/)
2-4. CI에 연결해 규칙을 위반한 경우 머지되지 않게 강제
CI 파이프라인의 verification 단계에 아크유닛(ArchUnit) 테스트를 추가했습니다. 규칙을 위반하면 빌드가 실패하므로 머지되기 전에 막힙니다. 개발자는 MR을 올리기 전에 로컬에서 동일한 테스트를 돌려 미리 확인할 수 있습니다.
2-5. AI 에이전트로 도입 비용 절감
아크유닛(ArchUnit)이 좋은 건 알아도 실제 적용하기까지 손이 많이 갑니다. 팀 내에서 정한 규칙을 테스트 코드로 옮기고, 수천 건의 규칙 위반사항을 분류해 동결하고, 하나씩 고치는 일이 만만치 않기 때문입니다. 이 부분에서 AI 에이전트를 적극 활용해서 부담을 덜었습니다.
- 규칙을 테스트 코드로 옮기기: 의도를 말로 설명하면 팀 규칙 문서를 ArchUnit DSL에 맞는 테스트로 작성해줍니다.
- 레거시 위반 분류와 동결: 수천 건 규모의 기존 위반사항을 단계별로 묶고 동결 대상으로 정리하는 반복 작업을 대신 처리합니다.
- 틀이 정해진 수정: @Autowired 필드 주입을 생성자 주입으로 바꾸는 것처럼 패턴이 일정한 수정을 빠르게 처리합니다.
예를 들어, 처음 도입할 때는 아래와 같은 프롬프트로 시작했습니다.
selfservice-backend 프로젝트에 ArchUnit을 도입하려고 해.
- .claude/rules/에 정의된 팀 규칙(architecture.mdc, coding-conventions.mdc 등)을 읽고
- 그 규칙을 강제하는 ArchUnit 테스트 코드를 작성해줘.
조건:
- 테스트는 별도 모듈로 만들고 JUnit5를 사용해.
- 각 규칙을 전체 모듈에 실행해서 현재 위반 건수를 먼저 파악해
- FreezingArchRule 사용해서 기존 위반은 동결하고 신규 위반만 실패하게 해.
- 위반 메시지는 한글로, 어떤 규칙을 어디서 어겼는지 바로 알 수 있게 작성해.
- 마지막으로 이 테스트를 CI verification 단계에 추가하는 방법을 알려줘.
- 작성 후 전체 테스트를 실행해 통과하는지 확인하고, 동결된 위반 목록을 규칙별로 요약해줘.
3. 시행착오
하지만 아크유닛(ArchUnit)이 잡지 못한 위반도 있었습니다. @Builder, @AllArgsConstructor 같은 Lombok 어노테이션을 사용하면 안 된다는 규칙을 걸었는데 전혀 감지되지 않았습니다.
아크유닛(ArchUnit)은 컴파일된 바이트코드(.class)를 분석하기 때문에, 바이트코드에 남는 RUNTIME, CLASS 어노테이션은 잘 인식합니다. 하지만 Lombok 어노테이션은 @Retention(SOURCE)라서 컴파일 시점에 폐기됩니다. Lombok이 어노테이션을 읽어 생성자와 빌더 코드를 만들지만, 어노테이션 자체는 .class에 남기지 않습니다. 또한 생성된 결과물(예: 생성자)은 바이트코드상 구분이 불가능합니다. 즉 "Lombok을 썼다"는 사실 자체가 바이트코드에 존재하지 않아 아크유닛(ArchUnit) 테스트에서 규칙 위반을 감지하지 못합니다.
@Retention 정책별 아크유닛(ArchUnit) 탐지 여부
| Retention | 바이트코드에 남음 | 아크유닛(ArchUnit) 탐지 | 예시 |
|---|---|---|---|
| RUNTIME | O | O | @Service, @RestController |
| CLASS(기본) | O | O | 다수 라이브러리 |
| SOURCE | X | X | @Builder 등 Lombok |
이 문제는 Lombok 관련 규칙만 소스파일(.java)을 직접 스캔해서 해결했습니다. src/main/java를 순회하며 해당 어노테이션이 포함된 파일을 모으고, 동결 목록과 비교해 신규 위반만 실패시키며, 해소분은 동결 파일에서 자동으로 제거합니다.
소스 스캐닝 기반 동결 검사 핵심 로직
4. 무엇이 달라졌나
적용 전후로 달라진 점 중 가장 중요한 건 신규 위반이 CI 단계에서 0건으로 유지되고 있다는 점입니다. 동결된 기존 부채도 줄고 있습니다.
@Builder 사용 규칙 위반은 823건에서 10건으로, 약 99% 줄었지만, 웹→outport 직접 의존은 4,447건에서 3,813건으로 14%밖에 줄이지 못했습니다. 신규 코드뿐만 아니라 레거시 코드도 동결된 규칙 위반을 개선 대상으로 잡고 점진적으로 개선하는 중입니다.
5. 주의할 점
아크유닛(ArchUnit)을 적용하면서 느낀 주의할 점은 다음과 같습니다.
- FreezingArchRule을 남발하면 레거시 부채는 그대로 쌓이기 때문에 적용하기 전과 다를 게 없습니다. 동결을 했다면 기술 부채 해소 일정과 지표를 함께 운영해야 의미가 있습니다. 특히 신규 코드에는 동결 기능을 쓰지 않도록 합의해 두는 것이 좋습니다.
- 바이트코드 분석에는 한계가 있습니다. @Retention(SOURCE) 어노테이션은 감지하지 못하므로, 소스 스캔 같은 보조 수단이 필요합니다.
- 규칙이 많아지면 관리 비용도 커집니다. 핵심 규칙부터 시작해 점진적으로 늘려가길 권합니다.
- 소규모, 단일 모듈 프로젝트에는 과할 수 있습니다. 레이어 경계가 단순하면 들이는 품에 비해 효용이 낮습니다.
맺으며
아크유닛(ArchUnit)을 도입한 덕분에 팀내 규칙을 사람의 기억에 의존하지 않고 빌드 단계에서 지킬 수 있었습니다. 그 결과 신규 위반은 0건으로 유지하고 있고, 기존 부채도 별도 개선 프로젝트로 관리되며 감소하고 있습니다. 규칙을 문서와 리뷰로만 지키고 계신 팀이라면, 아크유닛(ArchUnit)을 이용한 규칙 관리를 시작해보시길 권합니다.