코드 분석을 통한 복잡도(Cyclomatic Complexity) 측정과 유지보수성 예측

📅 March 2, 2026 👤 Floyd Owen
복잡하게 얽히고 빛나는 코드의 거미줄에 달러 표시가 박혀 있으며, 이는 금융과 기술의 융합이 점점 더 밀집되고 혼란스러워지는 현대 디지털 경제의 모습을 상징적으로 표현한 이미지입니다.

증상 확인: 코드가 복잡해지고 유지보수 비용이 증가하는가?

코드 리뷰를 할 때마다 새로운 기능 추가가 두렵습니까? 버그를 수정하면 다른 곳에서 문제가 터지는 ‘휘발성 코드’를 다루고 계신가요? 이는 높은 순환 복잡도(Cyclomatic Complexity)가 초래하는 전형적인 증상입니다. 이 지표는 단순히 코드의 ‘줄 수’가 아닌, 실행 경로의 수를 측정하여 코드의 테스트 및 이해 난이도를 수치화합니다. 지금 당장 프로젝트에서 가장 취약한 모듈을 찾아내는 진단부터 시작하겠습니다.

복잡하게 얽히고 빛나는 코드의 거미줄에 달러 표시가 박혀 있으며, 이는 금융과 기술의 융합이 점점 더 밀집되고 혼란스러워지는 현대 디지털 경제의 모습을 상징적으로 표현한 이미지입니다.

원인 분석: 복잡도는 왜 치명적인 결함으로 이어지는가

순환 복잡도는 1976년 토마스 매케이브가 제안한 개념으로, 코드 내의 결정점(if, while, for, case 등)의 수에 기반합니다, 복잡도가 높을수록 프로그램의 제어 흐름이 복잡해지며, 이는 두 가지 주요 문제를 야기합니다. 첫째, 모든 실행 경로를 테스트하기 위해 필요한 케이스 수가 기하급수적으로 증가하여 테스트 커버리지 달성이 불가능에 가까워집니다. 둘째, 코드의 의도를 파악하고 수정하는 데 걸리는 시간이 늘어나, 유지보수 비용을 폭발적으로 증가시킵니다. 높은 복잡도는 버그의 온상이자 기술 부채의 직접적인 원인입니다.

해결 방법 1: 빠른 진단 – 복잡도 측정 도구 실행

가설이 아닌 데이터로 접근해야 합니다. 인기 있는 프로그래밍 언어에는 대부분 복잡도를 분석하는 정적 분석 도구가 존재합니다. 아래 도구를 사용하여 프로젝트의 ‘핫스팟’을 즉시 확인하십시오.



  1. Java 프로젝트: SonarQube, Checkstyle, PMD를 빌드 도구에 통합. 가장 간단한 방법은 Maven pom.xml에 maven-pmd-plugin을 추가하고 mvn pmd:check를 실행하는 것.


  2. JavaScript/TypeScript 프로젝트: ESLint와 eslint-plugin-complexity 규칙을 사용. .eslintrc.js 파일에 ‘complexity’: [‘error’, { max: 10 }] 규칙을 추가하면 복잡도가 10을 초과하는 함수에서 즉시 린트 에러 발생.


  3. .NET 프로젝트: Visual Studio 내장 코드 메트릭스 분석 도구 사용. ‘분석’ > ‘코드 메트릭스 계산’을 클릭하면 각 메서드의 순환 복잡도, 유지관리 지수 등이 표시됨.


  4. Python 프로젝트: radon 라이브러리 설치 후 터미널에서 radon cc [파일명 또는 디렉토리] -a 명령어 실행. A~F 등급으로 결과 제공.

이 단계의 목표는 ‘측정’입니다. 복잡도가 15를 넘는 함수는 주의가 필요하며, 30을 초과하는 함수는 즉각적인 리팩토링 대상으로 간주해야 합니다.

해결 방법 2: 구조적 리팩토링 – 복잡도를 낮추는 실전 기법

측정 후 높은 복잡도 함수를 발견했다면, 다음 패턴을 적용하여 체계적으로 분해하십시오. 리팩토링 전 반드시 해당 함수에 대한 단위 테스트가 존재하는지 확인하거나, 최소한의 테스트를 먼저 작성하는 것이 안전합니다.

기법 1: 조기 반환(Early Return)과 가드 클러절(Guard Clause)

중첩된 if-else 문을 평평하게 만들어 가독성을 높이는 가장 효과적인 방법입니다.

주의사항: 이 기법은 함수의 주요 로직이 정상 조건 하에서 실행된다는 전제가 있습니다. 모든 반환 지점이 명확한지 확인 필수.

// 리팩토링 전 (복잡도 높음)

function processOrder(order) {

  if (order != null) {

    if (order.isValid()) {

      if (order.items.length > 0) {

        // 실제 처리 로직 (깊은 중첩)

        return calculateTotal(order);

      } else {

        throw new Error(‘No items’);

      }

    } else {

      throw new Error(‘Invalid order’);

    }

  } else {

    throw new Error(‘Order is null’);

  }

}

// 리팩토링 후 (복잡도 낮춤)

function processOrder(order) {

  if (order == null) throw new Error(‘Order is null’);

  if (!order.isValid()) throw new Error(‘Invalid order’);

  if (order.items.length === 0) throw new Error(‘No items’);

// 실제 처리 로직 (단일 레벨)

  return calculateTotal(order);

}

기법 2: 전략 패턴(Strategy Pattern)으로 조건부 로직 캡슐화

복잡한 switch-case나 if-else 체인이 객체의 행위를 결정할 때 적용하십시오. 새로운 조건이 추가되어도 기존 코드를 수정하지 않고 확장 가능합니다.

// 리팩토링 전

function calculateShippingCost(country, weight) {

  if (country === ‘US’) {

    return weight * 5;

  } else if (country === ‘EU’) {

    return weight * 7 + 10;

  } else if (country === ‘ASIA’) {

    return Math.max(weight * 6, 20);

  } else {

    return weight * 10;

  }

}

// 리팩토링 후

const shippingStrategies = {

  US: (weight) => weight * 5,

  EU: (weight) => weight * 7 + 10,

  ASIA: (weight) => Math.max(weight * 6, 20),

  default: (weight) => weight * 10

};

function calculateShippingCost(country, weight) {

  const strategy = shippingStrategies[country] || shippingStrategies.default;

  return strategy(weight);

}

기법 3: 큰 함수를 의미 있는 단위로 분할

한 함수가 50줄을 넘고 여러 가지 일을 한다면, ‘함수 추출(Extract Function)’을 적용하십시오. 함수명은 ‘하는 일’을 서술적으로 지어야 합니다.

// 리팩토링 전: 사용자 데이터 검증, 저장, 알림 전송을 한 함수에서 처리

function handleUserRegistration(rawData) {

  // 검증 로직 20줄… // 데이터 정제 로직 15줄… // DB 저장 로직 10줄… // 환영 이메일 전송 로직 10줄… }

// 리팩토링 후: 책임 분리

function handleUserRegistration(rawData) {

  const validatedData = validateUserInput(rawData);

  const user = transformToUserModel(validatedData);

  const savedUser = persistUserToDatabase(user);

  sendWelcomeNotification(savedUser);

}

// 각 하위 함수는 별도로 정의 및 테스트 가능

복잡하게 얽힌 전선과 기어 시스템에서 하나의 중요한 빨간 선이 끊어지며 전체 정교한 네트워크가 고장 나고 작동이 멈추는 순간을 상징적으로 묘사한 이미지입니다.

해결 방법 3: 예방 및 통합 – 지속 가능한 코드 품질 관리 체계 구축

일회성 리팩토링으로 끝나면 다시 원점으로 돌아갑니다. 복잡도 관리를 개발 프로세스에 지속적으로 통합해야 합니다.



  1. CI/CD 파이프라인에 정적 분석 통합: GitHub Actions, Jenkins, GitLab CI 등에 복잡도 검사 단계를 추가. 설정한 임계값(예: 복잡도 15)을 초과하는 코드가 머지되지 못하도록 차단.


  2. 프리커밋 훅 설정: husky와 lint-staged를 사용하여 커밋 전에 변경된 파일만 대상으로 빠르게 복잡도 검사 실행. 문제가 있는 코드는 커밋 자체가 안 되도록 설정.


  3. 코드 리뷰 체크리스트 항목화: 리뷰 요청 시 ‘함수 순환 복잡도 확인했는가?’를 필수 질문으로 포함. 리뷰어가 주관적으로 느끼는 ‘복잡함’을 객관적 지표로 논의할 수 있는 근거 마련.

주의사항 및 한계점 이해

순환 복잡도는 강력한 도구이지만 만능은 아닙니다. 맹신하면 오히려 생산성을 해칠 수 있습니다.

  • 복잡도 1의 거대 함수 문제: 순환 복잡도는 결정점만 계산합니다, 이로 인해 if문 하나 없이 500줄의 직렬 코드가 있는 함수는 복잡도는 1이지만, 유지보수성은 극히 낮습니다. 반드시 라인 수, 결합도, 응집도 등 다른 메트릭과 함께 종합적으로 판단해야 합니다.
  • 필수적인 복잡성: 복잡한 비즈니스 로직을 표현하는 알고리즘 자체가 본질적으로 복잡할 수 있습니다. 이 경우 복잡도를 무조건 낮추려고 하면 오히려 코드가 분산되어 흐름을 파악하기 더 어려워질 수 있습니다. 도메인의 복잡성을 코드에서 감추지 말고, 잘 구조화하고 문서화하는 데 중점을 두십시오.
  • 임계값은 팀의 합의: 복잡도 10을 임계값으로 삼는 것이 일반적이지만, 이는 팀의 경험 수준, 프로젝트 도메인, 언어 특성에 따라 유연하게 조정되어야 합니다. 팀 내에서 기준을 정하고, 그 이유를 공유하는 과정이 더 중요합니다.

전문가 팁: 유지보수성 지수 예측

순환 복잡도만으로는 부족합니다. Visual Studio의 ‘유지관리 지수’나 SonarQube의 ‘유지보수성 평가’처럼 복잡도, 라인 수, 주석 비율 등을 조합한 종합 지표를 확인하십시오. 이 지수는 코드 섹션이 수정하기 얼마나 위험한지를 A~C 등급으로 예측합니다. 주기적으로(예: 월 1회) 프로젝트 전체의 유지보수성 지수 추이를 모니터링하십시오. 지수가 하락하는 추세라면, 기술 부채가 쌓이고 있다는 명확한 신호입니다. 이를 근거로 관리자에게 리팩토링 시간을 할당받는 객관적 자료로 활용할 수 있습니다. 예방이 최고의 해결책입니다.

관련 레시피