Governance-as-Architecture: ArchUnit·OPA로 아키텍처 위반을 매 커밋마다 자동 감지해 분기별 리뷰를 없앤 경험
"이 팀은 왜 레이어드 아키텍처 규칙을 계속 어기는 거지?" 리뷰어 입장에서 PR을 열었다가 한숨부터 나왔던 경험, 다들 한 번쯤 있지 않으신가요. 저는 2년 전 이 문제를 해결하려고 직접 Governance-as-Architecture를 도입해봤는데, 지금 그 경험을 꺼내려고 합니다. Confluence 어딘가에 잘 정리된 아키텍처 문서가 있는데 실제 코드는 그 문서를 전혀 모르는 것처럼 흘러가는 상황 — 솔직히 이건 개발자를 탓할 문제가 아닙니다. 규칙이 "읽어야 할 문서" 안에만 존재하는 한, 지켜지지 않는 게 당연한 결과입니다.
Governance-as-Architecture는 아키텍처 규칙을 문서가 아닌 코드와 플랫폼에 심어서, 위반이 발생하는 순간 자동으로 감지되도록 만드는 패러다임 전환입니다. 분기별 아키텍처 리뷰 미팅이 매 커밋마다 실행되는 자동화된 검사로 대체되는 구조입니다. 처음 이 개념을 접했을 때 저도 "이런 거 대기업이나 하는 거 아니야?" 싶었는데, 막상 도입해보니 팀 규모와 관계없이 충분히 적용할 수 있었습니다.
이 글에서는 Governance-as-Architecture를 이루는 핵심 패러다임 세 가지와, ArchUnit·OPA·Backstage를 이용한 실제 구현 방법, 그리고 도입 과정에서 제가 몸소 겪은 시행착오를 공유해 보겠습니다. "어디서부터 시작해야 하나?" 하는 분들께 실마리가 되면 좋겠습니다.
핵심 개념
거버넌스가 "느린 루프"에 갇히는 이유
전통적인 아키텍처 거버넌스 모델을 그려보면 대략 이렇습니다. 아키텍트가 규칙을 문서화하고 → 개발자가 코드를 작성하고 → 분기 또는 월 단위로 리뷰 미팅을 열어 → 위반 사항을 찾아내고 → 수정을 요청합니다. 문제는 위반이 발견되는 시점입니다. 이미 그 코드가 수십 개의 다른 코드와 뒤엉킨 이후죠.
피드백 루프(Feedback Loop): 시스템이 자신의 출력을 다시 입력으로 받아 조정하는 구조. 루프가 느릴수록 오류가 누적된 뒤에야 감지됩니다.
저희 팀도 이 문제를 직접 겪었습니다. Controller가 Repository를 직접 참조하는 코드가 한 PR에서 시작해 두 달 만에 7개 모듈로 퍼져 있었고, 결국 리팩토링에 스프린트 하나를 통째로 썼습니다. Governance-as-Architecture는 이 루프를 가능한 한 짧게 만드는 게 목표입니다. 이상적으로는 개발자가 코드를 커밋하는 순간, 혹은 IDE에서 저장하는 순간 피드백이 도착합니다.
세 가지 하위 패러다임
Governance-as-Architecture는 크게 세 가지 접근법이 서로를 보완하는 구조로 이루어져 있습니다.
① Governance as Code (GaC)
아키텍처 규칙 자체를 코드로 작성하고, CI/CD 파이프라인에서 자동 실행하는 방식입니다. 여행 플랫폼 Agoda가 대표적인 사례인데, 수천 개의 엔지니어링 표준을 OPA(Open Policy Agent)의 Rego 언어로 재작성해서 모든 커밋마다 자동 검증을 돌리는 구조를 구축했습니다. Confluence에 잠들어 있던 문서가 "살아있는 정책"으로 전환된 거죠.
② Fitness Functions (적합성 함수)
Building Evolutionary Architectures (O'Reilly)에서 제시한 개념으로, 아키텍처의 특정 속성—모듈성, 의존성, 레이턴시 등—을 측정하는 자동화된 테스트입니다. 처음 이 개념을 접했을 때 솔직히 "그게 그냥 통합 테스트 아니야?"라고 생각했는데, 핵심 차이는 무엇을 검증하는가입니다. 일반 기능 테스트가 "이 기능이 올바르게 동작하는가?"를 묻는다면, 적합성 함수는 "이 시스템이 우리가 의도한 아키텍처 속성을 여전히 만족하는가?"를 묻습니다.
Fitness Function: 진화 알고리즘에서 빌려온 개념으로, 아키텍처가 특정 기준에 얼마나 "적합"한지를 측정하는 함수입니다. 예를 들어 "서비스 간 순환 의존성(Circular Dependency)이 존재하는가?"를 매 빌드마다 검사하는 테스트가 대표적입니다.
③ Platform-embedded Governance
Backstage 같은 내부 개발자 포털(IDP)을 통해, 규정을 준수하는 경로만 기본으로 제공되도록 플랫폼 자체를 설계하는 방식입니다. 개발자가 새 서비스를 생성할 때 골든 패스(Golden Path) 템플릿을 사용하면 자연스럽게 표준을 따르게 됩니다. 정책을 외울 필요가 없어지죠.
왜 지금인가: 2025~2026년 흐름
세 가지 패러다임이 이제 중소 규모 팀도 충분히 실용적으로 도입할 수 있을 만큼 도구 생태계가 성숙해졌습니다. 최근 흐름을 보면 이 방향이 선택이 아닌 기본값이 되어가고 있다는 인상을 받습니다.
| 트렌드 | 내용 |
|---|---|
| AI 기반 거버넌스 | 에이전틱 AI가 아키텍처 위반 탐지와 수정 제안을 자동화하는 방향으로 진화 중 |
| 분산 거버넌스 | 중앙 집중식 리뷰 보드 → 사전 승인된 블루프린트 + 분산 의사결정 |
| Policy-as-Code 주류화 | 기술 의사결정자 대다수가 Policy-as-Code를 핵심 역량으로 인식하기 시작 |
| 플랫폼 엔지니어링 통합 | IDP의 핵심 구성요소 중 하나로 "거버넌스 자동화"가 명시 |
한 가지 주의할 점이 있습니다. OPA 관련 통계를 인용할 때는 출처를 잘 살펴볼 필요가 있습니다. OPA의 상업화를 담당하는 Styra가 발표한 수치들은 자사 제품을 지지하는 방향으로 편향될 가능성이 있습니다. 실제로 2025년에 OPA 창시자들이 Apple로 이직하고 Styra 엔터프라이즈 구독 서비스가 종료되는 상황이 발생했습니다. OPA는 여전히 좋은 도구지만, 특정 벤더에 과도하게 의존하기보다 Kyverno나 Cedar 같은 대안도 함께 살펴보시는 것을 권장합니다.
마이크로서비스가 늘어날수록 수동 거버넌스는 스케일이 안 됩니다. 팀이 10배 늘어도 아키텍트가 10배로 늘어나지는 않으니까요.
실전 적용
세 예시는 각각 애플리케이션 레이어 → 인프라 레이어 → 플랫폼 레이어 순으로 거버넌스를 확장하는 흐름입니다. 규모나 상황에 따라 하나씩 선택하거나, 세 가지를 조합해서 사용할 수 있습니다.
예시 1: ArchUnit으로 Java 레이어드 아키텍처 강제하기
이 예시는 Java + Gradle(또는 Maven) 프로젝트를 기준으로 합니다.
가장 쉽게 시작할 수 있는 방법 중 하나입니다. ArchUnit은 JUnit 테스트로 아키텍처 규칙을 작성할 수 있게 해줍니다. CI에서 테스트가 실패하면 빌드가 깨지는 방식이라 추가 툴 없이도 바로 적용 가능합니다.
// ArchitectureTest.java
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
@AnalyzeClasses(packages = "com.example.myapp")
public class ArchitectureTest {
@ArchTest
static final ArchRule layerDependencyRule =
layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
@ArchTest
static final ArchRule noDirectDbAccessFromController =
noClasses()
.that().resideInAPackage("..controller..")
.should().dependOnClassesThat()
.resideInAPackage("..repository..");
}Controller에서 Repository를 직접 참조하면 빌드 시 이런 오류가 출력됩니다:
Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package
'..controller..' should depend on classes that reside in a package '..repository..''
was violated (1 times):
Field <com.example.myapp.controller.OrderController.orderRepository>
has type <com.example.myapp.repository.OrderRepository>
in (OrderController.java:18)어느 파일 몇 번째 줄에서 규칙이 깨졌는지 정확히 알려줍니다. 코드 리뷰에서 잡아낼 필요 없이 CI가 해결해 주는 거죠.
| 코드 요소 | 역할 |
|---|---|
@AnalyzeClasses |
분석 대상 패키지 지정 |
layeredArchitecture() |
레이어 간 허용 의존성 방향 정의 |
@ArchTest |
JUnit 5와 통합, CI에서 자동 실행 |
noDirectDbAccessFromController |
Controller → Repository 직접 접근 금지 규칙 |
예시 2: OPA/Gatekeeper로 Kubernetes 정책 강제하기
이 예시는 Kubernetes 클러스터와 OPA Gatekeeper가 설치된 환경을 기준으로 합니다.
애플리케이션 레이어만으로는 부족한 경우가 있습니다. 인프라 레이어까지 거버넌스를 확장하고 싶다면 OPA가 좋은 선택입니다. 아래 예시에서는 모든 Kubernetes Deployment에 team 레이블이 반드시 있어야 한다는 정책을 Gatekeeper ConstraintTemplate으로 작성합니다.
# required-labels.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: requiredlabels
spec:
crd:
spec:
names:
kind: RequiredLabels
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package kubernetes.admission
violation[{"msg": msg}] {
input.review.kind.kind == "Deployment"
not input.review.object.metadata.labels.team
msg := sprintf(
"Deployment '%v'에 'team' 레이블이 없습니다. 소유팀을 명시해 주세요.",
[input.review.object.metadata.name]
)
}
violation[{"msg": msg}] {
input.review.kind.kind == "Deployment"
container := input.review.object.spec.template.spec.containers[_]
not container.resources.limits
msg := sprintf(
"컨테이너 '%v'에 resources.limits가 없습니다. 리소스 상한을 명시해 주세요.",
[container.name]
)
}# required-labels-constraint.yaml (실제 정책 적용)
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: RequiredLabels
metadata:
name: deployment-must-have-team-label
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]이렇게 하면 kubectl apply를 실행하는 순간, 정책을 위반한 Deployment는 클러스터에 아예 배포되지 않습니다. 운영 환경에 잘못된 리소스가 올라가는 걸 원천 차단하는 거죠.
| 구성 요소 | 역할 |
|---|---|
ConstraintTemplate |
정책 로직(Rego)을 CRD로 등록 |
violation |
정책 위반 조건과 오류 메시지 정의 |
| Constraint 리소스 | 어떤 리소스에 정책을 적용할지 지정 |
예시 3: Backstage 스코어카드로 플랫폼 수준 거버넌스 구현
서비스가 10개 이상 운영 중이거나, 소유팀·온콜·문서 관리가 혼란스러워진 시점부터 효과를 체감할 수 있습니다.
인프라 레이어까지 확장했다면, 이제 플랫폼 레이어로 올라가볼 차례입니다. 서비스가 많아지면 "이 서비스는 모니터링이 있나?", "온콜 담당자가 등록되어 있나?" 같은 걸 일일이 확인하기 힘들어집니다. Backstage의 Scorecards 기능을 쓰면 각 서비스가 표준을 얼마나 준수하는지 점수로 표시할 수 있습니다.
먼저 각 서비스 레포 루트에 카탈로그 파일을 추가합니다:
# catalog-info.yaml (각 서비스 레포 루트에 위치)
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payment-service
annotations:
backstage.io/techdocs-ref: dir:.
pagerduty.com/service-id: P1234567
github.com/project-slug: myorg/payment-service
spec:
type: service
lifecycle: production
owner: team-payments
system: payment-platform
dependsOn:
- component:order-service
- resource:postgres-payments그런 다음 Backstage 백엔드 플러그인(plugins/tech-insights-backend/src/checks.ts)에 스코어카드 기준을 정의합니다:
// plugins/tech-insights-backend/src/checks.ts
import { Entity } from '@backstage/catalog-model';
export const productionReadinessChecks = [
{
id: 'has-owner',
name: '소유팀 명시',
description: 'catalog-info.yaml에 owner가 정의되어 있어야 합니다',
check: (entity: Entity): boolean => Boolean(entity.spec?.owner),
},
{
id: 'has-pagerduty',
name: 'PagerDuty 연동',
description: '온콜 알림을 위한 PagerDuty 서비스 ID가 등록되어 있어야 합니다',
check: (entity: Entity): boolean =>
Boolean(entity.metadata?.annotations?.['pagerduty.com/service-id']),
},
{
id: 'has-techdocs',
name: '문서화 완료',
description: 'TechDocs 참조가 설정되어 있어야 합니다',
check: (entity: Entity): boolean =>
Boolean(entity.metadata?.annotations?.['backstage.io/techdocs-ref']),
},
];개발자 포털에서 각 서비스의 스코어를 한눈에 볼 수 있고, 점수가 낮은 서비스는 "무엇을 개선해야 하는지" 바로 안내받을 수 있습니다. 아키텍트가 직접 쫓아다니며 확인하지 않아도 됩니다.
장단점 분석
직접 도입해보면서 느낀 장단점을 솔직하게 정리해봤습니다.
장점
| 항목 | 내용 |
|---|---|
| 빠른 피드백 루프 | 분기별 리뷰 → 매 커밋마다 자동 검증으로 전환. 위반이 즉시 감지됩니다 |
| 일관성 보장 | 팀 규모나 경험에 관계없이 동일한 기준이 적용됩니다 |
| 문서-실제 간극 해소 | 규칙이 코드로 존재하므로 문서와 구현 사이의 괴리가 생기지 않습니다 |
| 인지 부하 감소 | 플랫폼이 올바른 방향을 제시하므로 개발자가 정책을 외울 필요가 없습니다 |
| 확장성 | 팀이 늘어나도 거버넌스 품질이 함께 무너지지 않습니다 |
단점 및 주의사항
초기에 기대감이 너무 커서 "일단 다 자동화해보자"고 달려들었다가 한 달이 지나도 배포를 못 했던 기억이 납니다. 아래 주의사항은 그 경험에서 나온 것들입니다.
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 초기 투자 비용 | 아키텍처 규칙 전체를 코드로 옮기는 데 상당한 시간이 필요합니다 | 가장 자주 위반되는 규칙 2~3개부터 시작하는 것을 권장합니다 |
| 유연성 저하 위험 | 지나치게 엄격한 규칙이 실험과 혁신을 막을 수 있습니다 | 예외 처리 경로(escape hatch)를 명시적으로 설계해두는 것이 좋습니다 |
| 학습 곡선 | OPA/Rego, ArchUnit 등 도구 학습과 조직 문화 변화가 필요합니다 | 팀 내 챔피언을 먼저 육성하고 점진적으로 확산하는 방식을 권장합니다 |
| 거버넌스 코드 유지보수 | 시스템이 진화하면 거버넌스 코드도 함께 업데이트해야 합니다 | 거버넌스 규칙도 일반 코드처럼 PR 리뷰와 테스트 대상으로 관리하는 것이 좋습니다 |
| 도구 생태계 변동 | OPA 창시자 이탈 등 핵심 도구의 불확실성이 존재합니다 (2025년 기준) | 특정 도구에 과도하게 의존하지 않도록 추상화 레이어를 두거나, Kyverno·Cedar 같은 대안도 함께 검토해볼 수 있습니다 |
OPA (Open Policy Agent): 마이크로서비스, Kubernetes, API 게이트웨이 등 다양한 환경에 통합 가능한 범용 정책 엔진. Rego라는 선언형 언어로 정책을 작성합니다. Rego: OPA에서 사용하는 정책 언어로, 데이터와 쿼리를 분리하여 "허용 여부"를 선언적으로 표현합니다.
실무에서 가장 흔한 실수
-
모든 규칙을 한 번에 자동화하려는 시도: 저도 처음에 이 실수를 했습니다. "어차피 할 거면 다 하자"는 마음으로 규칙 수십 개를 한꺼번에 코드로 옮기려다 과부하가 왔습니다. 가장 많이 위반되는 규칙 하나를 먼저 자동화해보는 것에서 시작하면 훨씬 지속 가능합니다.
-
예외 경로를 설계하지 않는 것: 저희 팀도 레거시 코드 때문에 일부 규칙을 일시 예외 처리해야 할 상황이 생겼는데, 예외 메커니즘이 없었더니 개발자들이 거버넌스 코드 자체를 우회하는 방법을 찾기 시작했습니다. 의도된 예외는 코드 리뷰를 거쳐 허용하는 프로세스를 함께 설계해두는 것이 좋습니다.
-
거버넌스를 감시 도구로 포지셔닝하는 것: "이제 니들 코드 다 감시한다"는 분위기로 도입하면 팀의 반발을 삽니다. 저도 이걸 뒤늦게 깨달아서 한번 삐걱였던 기억이 있습니다. "우리 팀이 더 빠르고 안전하게 배포할 수 있도록 돕는 가드레일"이라는 관점으로 소통하는 게 훨씬 효과적입니다.
마치며
PR을 열 때마다 한숨 쉬던 그 순간으로 돌아가서 생각해보면 — 문제는 개발자도 아키텍트도 아니었습니다. 문제는 규칙이 "지켜지지 않아도 즉각 아무것도 일어나지 않는" 환경이었습니다. 거버넌스는 문서가 아닌 시스템으로 강제될 때 비로소 살아납니다.
지금 바로 시작해볼 수 있는 3단계가 있습니다:
-
현재 가장 자주 위반되는 아키텍처 규칙 하나를 골라 ArchUnit 또는 OPA로 작성해보시면 좋습니다. Java 프로젝트라면
build.gradle에testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'을 추가하고, 가장 간단한 레이어 의존성 규칙 하나로 시작해볼 수 있습니다. 이 단계에서 "경고만 출력"으로 시작했다가 팀이 익숙해지면 "빌드 실패"로 전환하는 방식도 좋습니다. -
새로 작성한 거버넌스 테스트를 CI 파이프라인에 포함시켜 주시면 됩니다. GitHub Actions를 사용하신다면 기존 테스트 스텝에 함께 실행되도록 구성할 수 있습니다. 처음엔 별도 잡(job)으로 분리해두면 실패해도 메인 빌드에 영향을 주지 않아 팀이 부담 없이 익숙해질 수 있습니다.
-
Backstage나 비슷한 IDP 도구를 통해 소프트웨어 카탈로그를 구성하고, 스코어카드 항목을 한두 가지만 먼저 정의해보시는 것을 권장합니다. 각 서비스의 소유팀 명시 여부, 문서 존재 여부 정도만 시작점으로 삼아도 팀 전체가 현황을 한눈에 파악할 수 있게 됩니다.
참고 자료
- Governance as Code: An Innovative Approach to Software Architecture Verification | Agoda Engineering
- How Agentic AI Empowers Architecture Governance | O'Reilly Radar
- Automating Architectural Governance | Building Evolutionary Architectures, 2nd Ed.
- Empower your teams with modern architecture governance | Noise/GetOto
- Architectural Fitness Functions: Automating Modern Architecture Governance in .NET | DevelopersVoice
- How Policy-as-Code Enhances Infrastructure Governance with OPA | env0
- Introducing architecture governance to tackle microservices sprawl | vFunction
- Fitness Functions for Your Architecture | InfoQ
- What is Spotify Backstage and how does it work in 2025? | GetDX
- C4 Model 공식 사이트