9.8.1. 순환 복잡도(Cyclomatic Complexity) 측정 및 임계값 제한
소프트웨어 공학의 역사에서 1970년대 빙하기부터 현대의 클라우드 시대까지, 인간이 작성한 코드의 가독성과 복잡성을 정량적으로 가장 확실하게 뜯어보고 측정하는 고전적이자 강력한 지표는 단연 **순환 복잡도(Cyclomatic Complexity, McCabe’s Complexity)**다.
순환 복잡도는 물리적인 소스 코드의 줄(Line) 수가 아니라, 하나의 함수(Function)나 메서드(Method) 내에 존재하는 **‘상호 독립적인 실행 경로(Execution Path)의 총 개수’**를 대수학의 그래프 이론으로 추적하여 의미한다.
코드가 위에서 아래로 단일하게 흐르는 선형적(Linear) 구조라면 복잡도는 기본값인 1이지만, 코드 블록 내부에 if, else, while, for, switch/case, catch 같은 논리적 분기점(Branch)이나 반복문 루프가 하나 추가될 때마다 복잡도 점수가 가차 없이 1씩 누적 증가한다.
자율형 AI 코더(Agentic Coder) 모델은 요구사항 프롬프트가 복잡하게 주어지면, 객체 지향 프로그래밍(OOP)의 원칙에 따라 이를 여러 개의 작은 명목상의 함수나 클래스로 분할하여 우아하게 추상화(Abstraction)하려는 생각 자체를 극도로 귀찮아한다. 대신 자신이 이미 컨텍스트 윈도우(Context Window) 리콜(Recall)을 쥐고 있는 하나의 거대하고 뚱뚱한 함수(God Function / Monolithic Function) 내부 빈 공간에, if-else 분기문을 수십 개씩 스파게티처럼 덕지덕지 이어 붙여서 당장의 컴파일 에러만을 뭉개버리려는 아주 강력하고 악랄한 편향(Monolithic Skew Bias)을 태생적으로 무조건적으로 보인다.
따라서 정적 스캐닝 오라클(Static Scanning Oracle)이 이 순환 복잡도를 CI/CD 단에서 강제로 통제하지 않으면, AI는 불과 몇 번의 리팩토링 프롬프트 왕복만으로도 순환 복잡도 50이 우습게 넘어가 버리는, 인간으로서는 도저히 읽고 유지보수할 수 없는(Unmaintainable) 혐오스러운 몬스터 코드를 당신의 main 배포 파이프라인 브랜치로 무자비하게 밀어넣게 된다.
1. 정적 분석기(Static Analyzer) 플러그인을 통한 복잡도 자동 산출 아키텍처
코드 유지보수성을 감시하는 오라클은 굳이 LLM을 또 호출하여 “이 코드가 복잡하니?“라고 허술하게 묻지 않는다. 컴파일 직전 또는 빌드 파이프라인의 AST(Abstract Syntax Tree) 파싱 초기 단계에서, 현존하는 가장 빠르고 기계적인 린터(Linter) 도구를 CLI(Command Line Interface)로 연동하여 각 함수의 복잡도를 0.1초 만에 1차원적으로 산출해 낸다.
- [Python 계열]:
radon,mccabe플러그인 또는flake8 --max-complexity옵션을 CI 셸 스크립트에 탑재한다. - [JavaScript/TypeScript 계열]:
ESLint설정 파일에complexity: ["error", 10]룰을 하드코딩하여 적용한다. - [C/C++/Java 등 컴파일 언어]: 엔터프라이즈급
SonarQube스캐너 미들웨어를 붙이거나, 가벼운 오픈소스인Lizard정적 분석기를 이용한다.
가드레일 오라클은 해당 도구들을 격리된 도커(Docker) 샌드박스에서 백그라운드로 실행하여, AI 에이전트가 방금 막 토해낸 파일 내부의 수십 개 함수 코드들의 각 함수별 복잡도 점수 벡터 V(G)를 배열로 하나하나 스캐닝하여 추출해 낸다.
2. 엄격한 하드 리미트(Hard Limit) 설정과 강제 분할 지시 피드백
엔터프라이즈의 장기적인 코드베이스(Codebase) 수명을 방어하기 위해, 오라클 CI 파이프라인에는 CTO조차 ‘절대 타협할 수 없는’ 물리적인 복잡도 임계값(Threshold)이 세팅되어야 한다.
- 소프트웨어 공학의 일반론적인 잣대에 따라, 순환 복잡도 **
10 미만**을 인간이 한눈에 파악할 수 있는 안전망으로 분류하고, **15 이상**을 버그가 서식하기 좋고 유지보수가 매우 막막한 임계 한계치로 평가하여 설정한다. - 만약 오라클 파이프라인의 정책(Policy) 파일에서
MAX_CYCLOMATIC_COMPLEXITY = 10으로 맞춰두었다면, AI가 짠 코드에서if문과 중첩for루프가 10개 이상 징그럽게 중첩된 단일 함수가 스캐닝 도중 발견되는 즉시, 해당 깃 커밋 빌드는 붉은색 실패(Fail) 처리된다.
이때 재미있는 점은, 오라클이 실패의 대가로 AI 에이전트의 프롬프트 창에 되돌려주는 피드백이 단순히 버그나 기능적 오류 수정 패치 요구가 아니라, 순수하게 개발 철학적인 **‘구조적 리팩토링(Structural Refactoring)’**을 강압적으로 요구하는 명령어라는 것이다.
“[Maintainability Oracle Error] 당신이 작성한
processPaymentTransaction()함수의 측정된 순환 복잡도(Cyclomatic Complexity)가 18점으로, 시스템이 허용하는 최대 임계값(10점)을 크게 초과했습니다. 이 뚱뚱한 괴물 함수를 역할에 맞게 직렬화 파싱(Parsing), PG사 토큰 검증(Validation), 데이터베이스 영속성 저장(Save) 등 단일 책임 원칙(SRP, Single Responsibility Principle)을 엄격히 준수하는 3~4개의 독립적인 헬퍼(Helper) 서브 함수 모듈로 조각조각 분할(Extract Method)하여 다시 제출하십시오.”
3. 테스트 파이프라인의 폭발(Combinatorial Explosion) 사전 차단 효과
순환 복잡도 임계값을 10 이하로 강압적으로 제어하는 행위는, 단지 깃허브 코드 리뷰 위에서 코드가 예쁘고 간결하게 보이도록 치장하는 사치스러운 스타일(Style) 수정 가이드가 절대 아니다. 이는 바로 이다음 파이프라인 스테이지에서 맞닥뜨리게 될 가혹한 ‘동적 단위 유닛 테스트(Unit Test) 자동 생성’ 생태계의 붕괴를 물리적으로 보호하기 위한 가장 필수적이고 수학적인 방어선이다.
순환 복잡도가 20인 거대한 스파게티 함수 덩어리가 하나 배포 파이프라인에 도달했다는 것은 무엇을 의미하는가? 그것은 그 함수 내부의 모든 논리적 분기 트리를 100% 검증(Branch Coverage 100%)하기 위해, 테스트 엔지니어(혹은 AI 테스트 제너레이터)가 최소 20개의 완전히 서로 다른 엣지 케이스 유닛 테스트 시나리오 입력값을 만들어내야 한다는 수학적 고통을 의미한다.
만약 이 20개의 복잡도를 오라클이 코드 생성 단계에서 통제하지 않은 채로, 테스트 에이전트 AI에게 *“이 함수에 대한 완벽한 JUnit 단위 테스트 코드 커버리지를 작성해 줘”*라고 미룬 채 요청을 던져버리면 어떻게 될까?
아무리 뛰어난 GPT-4 모델이라도, 함수 내부의 수천 가지 상태 변이가 교차하는 천문학적인 경우의 수 폭발(Combinatorial Explosion) 앞에서 컨텍스트 메모리가 터져나가며 정신을 잃고 엉뚱한 Mock 객체 할루시네이션(Hallucination)을 심각하게 일으키거나, 테스트 코드 생성을 모듈 중간에 조용히 포기하고 끊어버린다.
따라서 애초에 소스 코드 생성 단계 정적 오라클에서 **‘함수당 순환 복잡도 10 이하 강제’**라는 확고한 칼날 기준을 휘두름으로써, 우리는 아키텍처 모듈 단위를 인간 시니어 갭급 수준으로 잘게 쪼개어 가독성을 확보하는 부수 효과(Side-effect)를 거둔다. 뿐만 아니라, 다음 스테이지(동적 검증)에서 테스트 AI 에이전트 스스로가 인지 부하 오버플로우 없이 기능 단위별로 완벽하게 검증 가능한 수준의 “작고 확실한 테스트 코드 장벽(Sandbox)“을 무사히 세울 수 있도록, 필수적인 **인지적 여유 공간(Cognitive Breathing Room)**을 파이프라인 전체에 숨겨 놓는 거대한 설계의 묘수다.