순환 복잡도의 기초

순환 복잡도의 기본과 모든 프로그래머가 알아야 하는 이유

작성하는 모든 함수에는 복잡도 점수가 있습니다. 함수에 판단 지점이 없으면 복잡도 점수가 없습니다. if, 아니 else루프 없음, 없음 switch순환 복잡도는 1입니다. 즉, 코드를 통과하는 경로가 정확히 하나뿐입니다. if 조건문을 하나 추가하면 2가 되고, 하나 더 추가하면 3이 됩니다. 함수가 여러 중첩 레벨에 걸쳐 12개 이상의 조건 ​​분기를 갖게 되면 순환 복잡도는 15 또는 20이 될 수 있으며, 이를 완벽하게 테스트하려면 각각 다른 실행 경로를 다루는 테스트 케이스의 수가 그에 상응하게 많아야 합니다.

순환 복잡도(CC)는 1976년 토마스 J. 맥케이브가 프로그램 제어 흐름의 논리적 복잡도를 정량적으로 측정하는 지표로 도입했습니다. 복잡도 점수는 구체적이고 실질적인 결과를 제공하기 때문에 여전히 가장 유용한 코드 품질 지표 중 하나로 꼽힙니다. 복잡도 점수를 통해 전체 경로 커버리지를 달성하는 데 필요한 최소 테스트 케이스 수를 알 수 있고, 코드를 이해하고 수정하는 데 얼마나 어려울지 예측할 수 있으며, 발견되지 않은 결함이 있을 가능성이 가장 높은 함수를 식별할 수 있습니다. 이 가이드에서는 복잡도 계산 공식, 임계값, 언어별 예시, 복잡도를 실제로 줄이는 리팩토링 기법, 그리고 복잡도를 자동으로 측정하고 추적하는 방법을 다룹니다.

SMART TS XL

순환 복잡성을 마스터하고, 성능을 최적화하고, 숨겨진 버그를 방지하는 데 도움이 됩니다.

자세히 알아보세요…

순환 복잡도란 무엇인가?

순환 복잡도는 프로그램 소스 코드에서 선형적으로 독립적인 경로의 수를 측정합니다. 토마스 J. 맥케이브는 그래프 이론에서 이를 도출했는데, 모든 프로그램은 노드가 명령문이고 간선이 명령문 사이의 가능한 흐름인 제어 흐름 그래프로 표현될 수 있습니다. 공식은 다음과 같습니다.

CC = E - N + 2P

어디에:

  • E = 제어 흐름 그래프의 에지 수
  • N = 노드 수
  • P = 연결된 구성 요소의 수 (일반적으로 단일 기능의 경우 1개)

실제 계산을 위해서는 다음과 같은 더 간단한 방법이 있습니다. CC = 결정 지점 수 + 1. 모든 if, else if, while, for, case, catch, &&예산 및 || 결정 지점이 하나 추가됩니다. 모든 함수의 초기 CC는 1입니다.

자바

// CC = 1: no decision points
public String greet(String name) {
    return "Hello, " + name;
}

// CC = 3: two decision points (two if statements)
public String classify(int score) {
    if (score >= 90) return "Excellent";
    if (score >= 70) return "Satisfactory";
    return "Needs improvement";
}

// CC = 5: four decision points (three conditions + one loop)
public double calculateTotal(List<Item> items, boolean isMember, boolean isHoliday) {
    double total = 0;
    for (Item item : items) {          // +1
        total += item.getPrice();
    }
    if (isMember) total *= 0.9;        // +1
    if (isHoliday) total *= 0.95;      // +1
    if (total > 100) total -= 5;       // +1
    return total;
}

기준점: 어떤 점수가 허용 가능한 점수일까요?

맥케이브의 최초 지침은 여전히 ​​가장 널리 인용되는 지침으로, 네 가지 위험 수준을 정의합니다.

CC 점수위험 수준해석
1 – 10높음간단하고, 체계적이며, 테스트하기 쉽습니다.
11 – 20보통더 복잡해짐; 테스트 노력 증가 필요
21 – 50 높음 복잡하고 테스트하기 어렵습니다. 리팩토링을 권장합니다.
> 50 매우 높음 실제 적용 시 검증 불가; 심각한 품질 위험

CI/CD 품질 게이트에서 가장 일반적으로 적용되는 제한은 10이라는 임계값 입니다. SonarQube의 기본 인지 복잡성 임계값은 15입니다(관련은 있지만 다른 지표). 안전 필수 시스템에 대한 NIST 지침에서는 모듈당 최대 10을 권장합니다.

한 가지 중요한 점은 CC는 구조적 복잡성을 측정하는 것이지 의미적 복잡성을 측정하는 것이 아니라는 것입니다. 복잡한 재무 계산을 수행하는 CC=8인 함수는 간단한 방어 검사로 구성된 CC=15인 함수보다 이해하기 어려울 수 있습니다. CC는 최종 판단이 아닌 추가 조사가 필요한 신호로 활용해야 합니다.

다양한 언어에서의 순환 복잡도

Python

CC에 기여하는 Python의 결정 구조: if, elif, else (계산되지 않음, 조건이 없음) for, while, try/except (각 except 개수), with (포함되지 않음) 및 불리언 연산자 and/or 조건에서.

파이썬

# CC = 1
def format_name(first: str, last: str) -> str:
    return f"{first} {last}"

# CC = 4: three decision points
def calculate_discount(price: float, is_member: bool, is_holiday: bool) -> float:
    discount = 0.0
    if is_member:        # +1
        discount += 0.10
    if is_holiday:       # +1
        discount += 0.05
    if price > 100:      # +1
        discount += 0.02
    return price * (1 - discount)

# CC = 6: five decision points (list comprehension counts as a loop)
def process_orders(orders: list[dict]) -> list[dict]:
    return [
        {**order, "total": order["qty"] * order["price"]}  # +1 (comprehension)
        for order in orders
        if order["qty"] > 0                                 # +1 (filter condition)
        if order["price"] > 0                              # +1 (second filter)
    ]

Python CC 측정 도구: radon (radon cc src/ -s), flake8-cognitive-complexity, pylint 복잡도 플러그인을 사용한 SonarQube Python 분석.

자바

자바

// CC = 7: complex authentication with multiple conditions
public AuthResult authenticate(String userId, String password, boolean isMfa) {
    if (userId == null || password == null) return AuthResult.INVALID;  // +2 (||)
    User user = userRepository.findById(userId);
    if (user == null) return AuthResult.NOT_FOUND;                      // +1
    if (!user.checkPassword(password)) return AuthResult.WRONG_PASSWORD;// +1
    if (isMfa && !user.hasMfaEnabled()) return AuthResult.MFA_REQUIRED; // +2 (&&)
    return AuthResult.SUCCESS;
}
// CC = 1 + 2 + 1 + 1 + 2 = 7

줄이는 것:

자바

// After refactoring: CC = 3 (main method) + small helpers with CC = 2 each
public AuthResult authenticate(String userId, String password, boolean isMfa) {
    if (hasInvalidInputs(userId, password)) return AuthResult.INVALID;
    User user = findVerifiedUser(userId, password);
    if (user == null) return AuthResult.WRONG_PASSWORD;
    if (requiresMfa(user, isMfa)) return AuthResult.MFA_REQUIRED;
    return AuthResult.SUCCESS;
}

private boolean hasInvalidInputs(String userId, String password) {
    return userId == null || password == null;  // CC = 2
}

private boolean requiresMfa(User user, boolean isMfa) {
    return isMfa && !user.hasMfaEnabled();      // CC = 2
}

Java CC용 도구: Checkstyle, PMD, SonarQube, IntelliJ IDEA 내장 SMART TS XL.

C# 및 TypeScript

C#과 TypeScript는 Java와 동일한 규칙을 따릅니다. 중요한 추가 사항은 C#의 LINQ 표현식 절과 TypeScript의 삼항 연산자 체인에 각각 조건 결정 지점이 추가된다는 점입니다.

날카로운

// CC = 5: switch with four cases
public decimal GetShippingCost(string zone) => zone switch {
    "domestic"      => 5.99m,    // +1
    "eu"            => 15.99m,   // +1
    "international" => 29.99m,   // +1
    "express"       => 49.99m,   // +1
    _               => throw new ArgumentException($"Unknown zone: {zone}")
};

코볼

COBOL의 CC에 기여하는 결정 구조: IF/ELSE, EVALUATE WHEN (각 WHEN 절) PERFORM UNTIL, PERFORM VARYING ... WITH TEST BEFORE/AFTER, AT END, ON EXCEPTION, NOT ON EXCEPTION, ON SIZE ERROR.

코볼

       CALCULATE-DISCOUNT.
           IF WS-CUSTOMER-TYPE = 'GOLD'               *> +1
               IF WS-PURCHASE-AMT > 1000              *> +1
                   COMPUTE WS-DISCOUNT = 0.20
               ELSE                                    *> (no increment)
                   COMPUTE WS-DISCOUNT = 0.15
           ELSE IF WS-CUSTOMER-TYPE = 'SILVER'        *> +1
               COMPUTE WS-DISCOUNT = 0.10
           ELSE                                        *> (no increment)
               COMPUTE WS-DISCOUNT = 0.05
           END-IF
           EVALUATE TRUE
               WHEN WS-REGION = 'NORTH' PERFORM APPLY-REGIONAL-RATE  *> +1
               WHEN WS-REGION = 'SOUTH' PERFORM APPLY-SOUTHERN-RATE  *> +1
           END-EVALUATE.
           *> Total CC = 1 + 5 = 6

COBOL의 장황한 구문 때문에 문단이 현대 언어의 동등한 함수보다 일반적으로 더 깁니다. 문단당 CC가 50을 넘는 COBOL 프로그램은 레거시 코드베이스에서 흔히 볼 수 있으며, 리팩토링 및 현대화 계획에서 최우선 순위 대상입니다.

순환 복잡도 계산 방법: 세 가지

방법 1: 결정점 수 + 1 가장 빠른 수동 방법. 모든 것을 세어보세요. if, else if, while, for, case, catch, &&, || 함수 내에서. 함수 자체에 1을 더합니다.

방법 2: 제어 흐름도 함수를 그래프로 그리세요. 각 문장 또는 블록마다 노드를 하나씩, 제어 흐름마다 엣지를 만드세요. CC = E - N + 2.

방법 3: 자동화 도구. 아주 간단한 함수 이상의 모든 경우에 적용 가능한 유일한 실용적인 방법입니다. 대부분의 정적 분석 도구는 CC를 자동으로 계산하고 이를 CI/CD 파이프라인에 통합합니다.

실제로 복잡성을 줄이는 리팩토링 기법

조기 반환 보호 조항

가드 절은 전제 조건이 충족되지 않으면 함수를 조기에 종료하여 else 분기를 없애고 중첩 깊이를 줄입니다.

파이썬

# Before: deeply nested, CC = 5
def process_order(order):
    if order is not None:
        if order.is_valid():
            if order.has_stock():
                if order.payment_cleared():
                    return fulfill_order(order)
                else:
                    return "Payment failed"
            else:
                return "Out of stock"
        else:
            return "Invalid order"
    else:
        return "No order"

# After: flat, CC = 5 (same complexity, dramatically better readability)
def process_order(order):
    if order is None: return "No order"
    if not order.is_valid(): return "Invalid order"
    if not order.has_stock(): return "Out of stock"
    if not order.payment_cleared(): return "Payment failed"
    return fulfill_order(order)

CC(배포 계수)는 줄어들지 않습니다. 동일한 결정 지점이 존재하지만 코드를 훨씬 읽기 쉽고 테스트하기 쉬워집니다. 진정한 CC 감소는 결정 지점을 재배열하는 것이 아니라 아예 없애는 것을 의미합니다.

추출 방법

논리적인 의사 결정 그룹들을 명명된 메서드로 분리하면 호출 함수의 공통 코드(CC)가 줄어들고, 복잡성이 더 작고 테스트 가능한 단위로 분산됩니다.

자바

// Before: one method doing everything, CC = 9
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
    double subtotal = 0;
    for (LineItem item : invoice.getItems()) {
        subtotal += item.getQuantity() * item.getUnitPrice();
        if (item.isTaxable()) subtotal += item.getPrice() * 0.1;
    }
    if (customer.isMember()) subtotal *= 0.9;
    if (customer.hasVoucher()) subtotal -= customer.getVoucherValue();
    if (subtotal < 0) subtotal = 0;
    return subtotal;
}

// After: main method CC = 4, helpers have CC = 2-3 each
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
    double subtotal = computeLineItemTotal(invoice.getItems());
    subtotal = applyCustomerDiscounts(subtotal, customer);
    return Math.max(0, subtotal);
}

조건문을 다형성으로 대체하기

함수가 타입이나 상태에 따라 분기하는 경우, 다형성은 분기를 완전히 없애줍니다.

자바

// Before: switch on payment type, CC grows with each new type
public void processPayment(String type, double amount) {
    switch (type) {
        case "CREDIT":  processCreditCard(amount); break;
        case "PAYPAL":  processPayPal(amount);     break;
        case "CRYPTO":  processCrypto(amount);     break;
        default: throw new IllegalArgumentException("Unknown type: " + type);
    }
}

// After: new payment types require no changes to this method, CC = 1
public interface PaymentProcessor {
    void process(double amount);
}

public void processPayment(PaymentProcessor processor, double amount) {
    processor.process(amount);  // no branching
}

복잡한 조건문 분해하기

복잡한 부울 표현식을 의도를 명확히 드러내는 명명된 메서드로 추출합니다.

파이썬

# Before: dense boolean logic, hard to understand, easy to mis-test
if user.age >= 18 and user.country in ALLOWED_COUNTRIES and not user.is_banned and user.verified:
    grant_access()

# After: named predicate, self-documenting, unit-testable independently
def is_eligible_for_access(user: User) -> bool:
    return (
        user.age >= 18
        and user.country in ALLOWED_COUNTRIES
        and not user.is_banned
        and user.verified
    )

if is_eligible_for_access(user):
    grant_access()

CI/CD 파이프라인의 순환 복잡도

CI/CD 파이프라인에서 자동화된 CC(Common Code) 적용을 통해 코드 검토 사이에 복잡성이 눈에 띄지 않게 누적되는 것을 방지할 수 있습니다.

얌

# GitHub Actions: fail PR if any function exceeds CC threshold
name: Code Quality

on: [pull_request]

jobs:
  complexity-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install radon (Python CC tool)
        run: pip install radon

      - name: Check cyclomatic complexity
        run: |
          radon cc src/ --min C --show-complexity
          # Fails if any function has CC grade C (11-15) or worse
          radon cc src/ --min C --total-average | grep -q "Average complexity" \
            && echo "Complexity check passed" \
            || (echo "Functions with high complexity found" && exit 1)

SonarQube를 사용한 Java 개발:

얌

# SonarQube quality gate blocks merge if CC exceeds threshold
- name: SonarCloud Scan
  uses: SonarSource/sonarcloud-github-action@master
  env:
    SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
  with:
    args: |
      -Dsonar.qualitygate.wait=true
      -Dsonar.java.complexity.Function.threshold=10

품질 검증은 복잡도가 높은 새로운 코드에 대해서만 차단해야 하며, 이미 복잡한 코드가 많아 별도로 처리되고 있는 기존 코드베이스 전체에 대해서는 차단해서는 안 됩니다.

COBOL 및 레거시 코드베이스의 순환 복잡도

순환 복잡도(CC)가 50 또는 100을 넘는 문단이 있는 COBOL 프로그램이 포함된 엔터프라이즈 시스템의 경우, 순환 복잡도 분석은 최신 코드베이스에서와는 다른 주요 목적을 가집니다. 여기서 중요한 질문은 "이 함수를 리팩토링해야 할까요?"가 아니라 "이 프로그램의 마이그레이션 위험은 무엇이며, 어떤 순서로 현대화해야 할까요?"입니다.

메인 단락의 CC 값이 80인 COBOL 프로그램은 80개의 독립적인 실행 경로를 가지며, 각 경로의 유효성을 검증하기 위한 테스트 케이스가 필요합니다. 대부분의 기존 COBOL 프로그램처럼 테스트 케이스가 부족한 경우, CC 값은 변환 작업이 안전하다고 간주되기 전에 구축해야 하는 검증 시나리오의 수를 예측하는 주요 지표가 됩니다.

SMART TS XL의 정적 코드 분석 COBOL, JCL, RPG, PL/I 및 모든 최신 언어에 대한 순환 복잡도를 동시에 계산하여 포트폴리오 수준의 순환 복잡도 분포를 생성함으로써 현대화 순서 결정을 근거 기반으로 내릴 수 있도록 지원합니다. 순환 복잡도 점수가 가장 높고 호출자가 가장 많은 프로그램(높은 팬인)은 마이그레이션 위험이 가장 높은 대상입니다. COBOL 애플리케이션의 유지보수성 지표 설정CC는 Halstead Volume 및 코드 라인 수와 같은 다른 요소들과 함께 더 광범위한 품질 지표를 구성하는 한 가지 요소입니다.

영향 분석 기능은 CC 기반 복잡성 분류를 사용하여 복잡성이 높은 프로그램 변경 시 검증해야 할 사항의 범위를 지정합니다. CC가 높을수록 실행 경로가 많아지고, 이는 수정 전후의 동작 동등성을 확인하기 위해 검증해야 하는 테스트 시나리오가 더 많아짐을 의미합니다.

레거시 시스템 현대화 프로그램을 계획하는 팀의 경우 , 포트폴리오 전체의 통화량 분포가 마이그레이션 단계 순서 결정의 핵심 요소입니다. 통화량이 적고 문의자가 적은 프로그램은 초기에 마이그레이션을 진행하고, 통화량이 많고 문의자가 많은 프로그램은 팀이 비교적 간단한 구성 요소에 대한 전문성을 확보하고 복잡한 구성 요소를 검증할 수 있는 테스트 인프라가 구축된 후에 마지막으로 마이그레이션을 진행합니다.

복잡성 자체가 적이 아니라, 보이지 않는 복잡성이 적이다.

순환 복잡도는 테스트 용이성과 직접적으로 연결되는 몇 안 되는 코드 품질 지표 중 하나입니다. 전체 경로 커버리지를 위해 필요한 테스트 케이스 수는 정의상 순환 복잡도 점수와 같거나 그 이상입니다. 이러한 연결성 덕분에 순환 복잡도는 다른 많은 품질 지표와 달리 실질적인 조치가 가능한 지표입니다.

CC 관리라는 분야는 복잡성이 점진적으로 축적된다는 점을 이해하는 것을 요구합니다. 모든 if 점점 커지는 함수에 추가되는 조건문은 개별적으로는 합리적일 수 있습니다. 하지만 3년 동안 개별적으로 합리적인 결정을 내린 결과, CC(컴파일 계수)가 40에 달하는 함수가 생성될 수 있으며, 이는 너무 복잡해서 안전하게 분석하기 어렵기 때문에 아무도 건드리고 싶어 하지 않습니다. 이 가이드에서 소개하는 가드 절, 메서드 추출, 다형성, 조건부 분해, CI/CD 품질 게이트와 같은 도구와 기법은 이러한 복잡성 누적이 눈에 띄지 않게 발생하는 것을 방지하고, 이미 발생했을 때는 체계적으로 해결하기 위해 존재합니다.