추상화가 부패하지 않음을 증명하는 방법 — 대규모 코드베이스에서 존재론적 무결성 (Ontological Integrity) 강제하기
요약
대규모 코드베이스에서 발생하는 '추상화 부패' 현상을 분석하고, 이를 방지하기 위해 아키텍처를 실행 가능한 테스트 스위트로 관리하는 방법을 제안합니다. 의존성 그래프와 레이어 규칙을 활용하여 설계된 구조적 불변량을 CI 단계에서 검증하는 전략을 다룹니다.
핵심 포인트
- 추상화 부패: 작은 의존성 위반이 축적되어 아키텍처 경계가 해체되는 현상
- 아키텍처 검증의 필요성: 코드 구문이나 로직뿐만 아니라 모듈 간 의존성 관계를 검증해야 함
- 실행 가능한 단언: 아키텍처 규칙을 코드로 정의하여 CI에서 빌드 실패를 유도
- 핵심 요소: 기계 판독 가능한 의존성 그래프와 레이어별 허용 규칙 구축
"우리는 200개의 모듈을 가지고 있었습니다. 아키텍처 다이어그램은 그것들이 계층화되어 있다고 말했습니다. 하지만 git log는 그것들이 스파게티(spaghetti)라고 말했습니다."
클린 아키텍처 (Clean Architecture)의 서서히 진행되는 죽음
모든 코드베이스는 깨끗하게 시작됩니다.
첫째 날: 아름다운 계층형 아키텍처 (layered architecture). 바닥에는 코어 모듈 (Core modules). 중간에는 비즈니스 로직 (Business logic). 상단에는 API 핸들러 (API handlers). 표면에는 UI. 아래를 향하는 깔끔한 화살표들. 설계자의 다이어그램이 현실과 일치합니다. 모두가 자랑스러워합니다.
365일째: 한 개발자가 UI 레이어에서 데이터베이스 유틸리티 (database utility)로 직접 연결되는 "빠른" 임포트 (import)를 추가합니다. 서비스 레이어 (service layer)를 거치는 것보다 빠릅니다. 코드 리뷰 (code review)는 가볍게 지나갑니다 — 기능 구현이 시급하기 때문입니다. 아무도 눈치채지 못합니다.
730일째: 세 개의 레이어 위반 (layer violations)이 더 스며들었습니다. 코어 모듈은 이제 "단지 이 하나의 설정 항목 때문에" 서비스 레이어 헬퍼 (service-layer helper)를 임포트합니다. API 핸들러는 "느린" 서비스 호출을 피하기 위해 데이터베이스를 직접 쿼리합니다. UI 컴포넌트는 그 자체로 코어 모듈을 임포트하는 공유 유틸리티 (shared utility)를 임포트하며 — 4개 레이어를 건너뛰는 상황을 만듭니다.
1095일째: Confluence에 있는 아키텍처 다이어그램은 실제 코드베이스와 전혀 닮지 않았습니다. 신입 엔지니어들은 다이어그램을 보고, 코드를 보고, 어깨를 으쓱하며, 그저 편리하다고 느껴지는 대로 임포트를 추가합니다. 계층형 아키텍처는 이제 장식용 허구에 불과합니다.
이것이 바로 추상화 부패 (abstraction rot) 입니다 — 축적된 작은 위반들을 통해 의도된 아키텍처 경계가 점진적으로 해체되는 현상입니다. 그리고 이것은 보편적입니다. 마이크로서비스 시스템 (microservice systems), 모놀리스 (monoliths), Terraform 모듈, 데이터베이스 스키마 (database schemas), 패키지 매니저 (package managers), 그리고 API 게이트웨이 (API gateways)에서 모두 발생합니다.
근본 원인은 간단합니다: 아무도 아키텍처 자체를 검증하지 않습니다. 우리는 코드 구문 (syntax)을 린트 (lint) 합니다. 비즈니스 로직을 테스트합니다. 코드 스타일을 강제합니다. 하지만 우리는 모듈 간의 의존성 관계 (dependency relationships between modules) 가 우리가 주장하는 구조적 불변량 (structural invariants)을 충족하는지 거의 검증하지 않습니다.
만약 당신의 아키텍처를 린트 할 수 있다면 어떨까요?
실행 가능한 단언 (Executable Assertions)으로서의 아키텍처
핵심 아이디어는 아키텍처 다이어그램을 단순한 문서가 아니라, 하나의 **테스트 스위트 (test suite)**로 취급하는 것입니다. 레이어 규칙을 코드로 정의하십시오. 이를 CI(지속적 통합)에서 실행하십시오. 만약 어떤 모듈이라도 의존성 계약 (dependency contract)을 위반한다면 빌드를 실패시키십시오.
이를 위해서는 두 가지가 필요합니다:
- 의존성 그래프 (dependency graph) — 어떤 모듈이 어떤 모듈에 의존하는지를 나타내는 기계 판독 가능한 매니페스트 (manifest)
- 레이어 규칙 (layer rules) — 각 레이어에 대해 허용되는 의존성에 관한 일련의 단언 (assertions)
두 가지 모두 구축해 보겠습니다.
의존성 그래프 (The Dependency Graph)
먼저, 모듈 의존성에 대한 신뢰할 수 있는 단일 원천 (source of truth)이 필요합니다. 가장 간단한 버전은 저장소에 존재하는 JSON 매니페스트입니다:
{
"auth-core": {
"layer": "core",
...
이 방식은 버전 관리가 가능하고, PR (Pull Request)에서 검토할 수 있으며, 검증 스크립트로 파싱할 수 있습니다. 누군가 새로운 의존성을 추가할 때마다 반드시 여기에 선언해야 하며, 해당 선언은 검증 과정을 거칩니다.
검증기 (The Validator)
다음은 검증 스크립트입니다. 이 스크립트는 네 가지 범주의 규칙을 강제합니다:
- 끊어진 참조 (Dangling references): 모든 의존성은 실제로 존재하는 모듈을 가리켜야 합니다.
- 코어 무결성 (Core integrity): 코어 모듈은 의존성이 전혀 없어야 합니다 (이들은 토대이기 때문입니다).
- 레이어 접지 (Layer grounding): 각 레이어는 반드시 바로 아래 레이어에 의존해야 하며, 레이어를 건너뛰어서는 안 됩니다.
- 데드 코드 방지 (No dead code): 코어 모듈은 반드시 무언가에 의해 사용되어야 하며, 그렇지 않으면 불필요한 짐(dead weight)이 됩니다.
import json
import sys
...
위반 사항이 포함된 그래프를 대상으로 실행했을 때 어떤 일이 발생하는지 살펴보겠습니다.
위반 사항 포착 (Catching Violations in Action)
다음은 세 가지 일반적인 위반 사항이 섞여 있는 그래프입니다:
{
"auth-core": { "layer": "core", "depends_on": [] },
"dead-util": { "layer": "core", "depends_on": [] },
...
검증기를 실행하면:
❌ ARCHITECTURE VALIDATION FAILED:
• DEAD CODE: Core module 'dead-util' is not used by any other module. Delete it or wire it in.
...
네 건의 위반 사항이 발견되었습니다. 코드 리뷰(Code review) 단계에서는 이러한 개별 의존성들이 각각 합리적으로 보였을 것입니다. 하지만 검증기(Validator)는 구조적 전체상을 봅니다. 즉, 아무도 사용하지 않는 데드 모듈(dead module), 근거가 없는 서비스(ungrounded service), 그리고 스파게티 경로(spaghetti pathways)가 될 두 개의 레이어 건너뛰기(layer skips)를 포착해냅니다.
CI 통합 (CI Integration)
이를 빌드 게이트(build gate)로 만드세요. 아키텍처 위반이 발생하면 PR(Pull Request)을 병합할 수 없도록 설정합니다:
# .github/workflows/architecture-lint.yml
name: Architecture Lint
on: [pull_request]
...
두 번째 단계인 Detect Uncommitted Graph Changes는 미묘한 실패 모드(failure mode)를 잡아냅니다. 누군가 새로운 의존성을 추가하기 위해 아키텍처 그래프를 업데이트했지만, 검증기 스크립트가 (메타데이터 정리와 같은) 변경 사항을 함께 생성했을 경우입니다. 만약 이러한 변경 사항이 커밋(commit)되지 않는다면, PR은 동기화되지 않은 상태가 됩니다.
레이어 규칙 설계하기 (Designing Your Layer Rules)
4계층 모델(core → service → api → ui)은 단지 하나의 예시일 뿐입니다. 여러분의 아키텍처는 다른 레이어를 가질 수 있습니다. 이 패턴은 무엇에든 적용 가능합니다:
마이크로서비스 아키텍처 (Microservice Architectures)
infrastructure → data-stores → business-services → api-gateways → clients
규칙: 클라이언트(clients)는 데이터 저장소(data stores)에 절대 직접 접근할 수 없습니다. API 게이트웨이(API gateways)는 비즈니스 서비스(business services)를 우회할 수 없습니다. 인프라스트럭처(infrastructure) 모듈은 의존성이 전혀 없어야 합니다.
패키지/라이브러리 아키텍처 (Package/Library Architectures)
primitives → data-structures → algorithms → public-api
규칙: 프리미티브(primitives)는 아무것에도 의존하지 않습니다. 각 레이어는 바로 아래에 있는 레이어로부터만 임포트(import)합니다. 퍼블릭 API(public API)는 외부 소비자(external consumers)가 볼 수 있는 유일한 레이어입니다.
Terraform / 코드형 인프라 (Infrastructure-as-Code)
providers → modules → environments → stacks
규칙: 프로바이더(providers)는 독립적입니다. 모듈(modules)은 프로바이더와 다른 모듈에만 의존합니다. 환경(environments)은 모듈을 구성합니다. 스택(stacks)은 환경을 구성합니다.
데이터베이스 스키마 설계 (Database Schema Design)
base-tables → junction-tables → views → materialized-views
규칙: 기본 테이블(base tables)은 뷰(views)에 대한 외래 키(foreign keys)를 가지지 않습니다. 뷰는 테이블 및 다른 뷰에 의존합니다. 구체화된 뷰(materialized views)는 뷰에 의존합니다.
이 패턴의 강력함은 레이어가 무엇을 나타내든 관계없이 _검증 로직 (validation logic)_이 동일하다는 점에 있습니다. 오직 레이어의 이름과 규칙만 변경될 뿐입니다.
심화: 순환 탐지(Cycle Detection) 및 영향 범위(Blast Radius)와의 결합
레이어 검증기(layer validator)는 구조적 위반을 잡아냅니다. 하지만 완전한 아키텍처 안전망을 구축하려면, 다음 두 가지 체크를 추가로 결합하십시오.
1. 순환 탐지 (Cycle Detection)
유효한 레이어 구조 내에서도 순환 의존성(circular dependencies)이 형성될 수 있습니다 (Service A → Service B → Service A). 레이어 검증과 함께 실행되는 별도의 체크 항목으로 순환 탐지를 추가하십시오. (순환 의존성에 관한 동반 기사에서 DFS 기반 접근 방식을 참조하십시오.)
2. 영향 범위 계산 (Blast Radius Computation)
PR(Pull Request)이 모듈을 수정할 때, 역의존성 그래프(reverse dependency graph)에 대한 BFS(너비 우선 탐색)를 사용하여 전이적으로 영향을 받는 전체 모듈 세트를 계산하십시오. 이를 PR 코멘트에 표시하여 리뷰어가 변경 사항의 범위를 이해할 수 있도록 합니다:
📊 이 PR의 영향 범위 (Blast Radius):
수정됨: auth-core
직접적인 영향: user-service, billing-service
...
이를 통해 코드 리뷰는 "이 파일이 올바르게 보이나요?"에서 "우리가 이 변경 사항의 전체적인 아키텍처적 영향을 이해하고 있는가?"로 전환됩니다.
아키텍처 다이어그램은 CI 체크이거나, 혹은 희망 사항일 뿐입니다
여기 불편한 진실이 있습니다. 만약 아키텍처 제약 조건이 CI(지속적 통합)에서 강제되지 않는다면, 그것은 제약 조건이 아닙니다. 그것은 제안일 뿐입니다. 그리고 제안은 부패합니다.
모든 대규모 코드베이스는 결국 선택의 기로에 서게 됩니다:
-
아키텍처를 코드로 강제하기 (Enforce architecture as code). 규칙은 명시적이고, 기계에 의해 확인되며, 규칙 자체를 의도적으로 수정하고 리뷰하지 않는 한 위반하는 것이 불가능합니다.
-
아키텍처를 문화로 강제하기 (Enforce architecture as culture). 규칙은 위키(wiki)에 존재하고, 암묵적 지식(tribal knowledge)으로 존재하며, 초기에 합류한 시니어 엔지니어들의 기억 속에 존재합니다. 이 규칙들은 해당 엔지니어들이 떠나거나, 마감 기한 때문에 누군가가 "이번 한 번만"이라며 편법을 쓰기 전까지만 작동합니다.
옵션 1은 몇 백 줄의 Python 코드와 CI 워크플로우 비용이 듭니다. 옵션 2는 당신의 아키텍처를 비용으로 지불하게 합니다 — 느리게, 보이지 않게, 그리고 되돌릴 수 없게 말입니다.
검증하지 않는 의존성 그래프(dependency graph)는 부패하기 마련입니다. 아키텍처 다이어그램을 실행 가능한 단언(executable assertion)으로 만드세요. 규칙이 위반되면 빌드를 실패시키십시오. 레이어 규칙(layer rules)을 타입 시스템(type system)만큼이나 엄격하게 다루십시오.
당신의 아키텍처 다이어그램은 CI 체크이거나, 혹은 단순한 바람일 뿐입니다.
중간 지대는 없습니다.
이 글의 모든 코드는 Python 표준 라이브러리(json, sys)만을 사용합니다. 외부 패키지는 필요하지 않습니다. 아키텍처 그래프는 일반적인 JSON 파일입니다. 여러분의 모듈 구조에 맞게 스키마를 조정하여 오늘 바로 검증을 시작하십시오.
대기 명단 등록: https://metareignity.com/
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기