소프트웨어 복잡도 측정과 유지보수성 관리
소프트웨어 복잡도의 의미와 측정 관점을 정리하고, McCabe 순환복잡도와 Halstead 소프트웨어 과학으로 유지보수성과 테스트 위험을 관리하는 방법을 다룬다.
2026-08-14 · 최초 발행 2025-05-23
유지보수 비용은 코드의 복잡한 경로에서 시작된다
소프트웨어 복잡도는 프로그램의 논리적 실행 경로와 무질서성을 수치로 다루는 지표다. 개발에 필요한 노력, 테스트 난이도, 유지보수 비용을 예측하는 근거가 되며, 복잡도가 높아질수록 코드를 이해하고 수정·확장하기 어려워진다. 오류 가능성과 테스트 비용도 함께 커진다.
복잡도를 측정하는 목적은 단순한 점수 매기기가 아니다. 테스트 수준을 정하고, 개발 노력을 가늠하며, 유지보수 비용과 품질 상태를 판단하는 데 쓰인다.
코드의 크기와 구조는 다른 질문을 던진다
복잡도는 코드의 물리적 규모와 내부 구조라는 두 관점에서 볼 수 있다.
규모 기반 측정은 LOC(Lines of Code), SLOC(Source Lines of Code), 함수·메소드 수, 클래스·모듈 수처럼 코드가 얼마나 큰지를 다룬다. LOC는 코드 라인 수이고, SLOC는 실제 실행되는 코드 라인 수를 뜻한다. 함수·메소드와 클래스·모듈의 개수는 구조적 크기를 보여준다.
반면 구조 기반 측정은 코드의 제어 흐름과 구성 요소의 관계를 본다. McCabe의 순환복잡도는 제어 흐름의 복잡성을, Halstead의 소프트웨어 과학은 코드의 어휘적 복잡성을 측정한다. 결합도와 응집도 역시 모듈 사이 관계의 복잡도를 판단하는 기준이다.
분기 경로를 세는 McCabe 순환복잡도
Thomas McCabe가 1976년에 제안한 순환복잡도(Cyclomatic Complexity)는 모듈 안에 존재하는 독립적인 분기 경로의 수를 측정한다. 제어 흐름 그래프를 사용하면 다음 식으로 계산할 수 있다.
- V(G) = E - N + 2
- E: 간선(Edge)의 수
- N: 노드(Node)의 수
- V(G) = P + 1
- P: 조건문의 수
값은 테스트 범위와 리팩토링 우선순위를 판단하는 단서가 된다. 1-10은 단순한 프로그램과 낮은 위험, 11-20은 복잡한 프로그램과 중간 위험, 21-50은 매우 복잡한 프로그램과 높은 위험으로 해석한다. 50 이상은 테스트 불가능한 코드와 극도의 위험에 해당한다.
순환복잡도가 높은 모듈은 테스트 케이스 설계 시 더 많은 분기 경로를 고려해야 한다. 따라서 품질 관리 지표이면서 리팩토링 대상을 찾는 기준이 된다.
// 순환복잡도가 4인 함수 예시
public int calculateGrade(int score) {
if (score >= 90) { // 조건 1
return 'A';
} else if (score >= 80) { // 조건 2
return 'B';
} else if (score >= 70) { // 조건 3
return 'C';
} else {
return 'F';
}
}
// V(G) = 조건문 수 + 1 = 3 + 1 = 4
이 함수에는 조건문이 3개 있으므로 순환복잡도는 4다. 각 조건에서 갈라지는 실행 경로가 테스트 설계의 출발점이 된다.
연산자와 피연산자로 읽는 Halstead 측정법
Maurice Halstead가 1977년에 제안한 소프트웨어 과학(Software Science)은 프로그램을 구성하는 어휘적 요소로 복잡도를 계산한다. 여기서 n1은 고유 연산자 수, n2는 고유 피연산자 수, N1은 전체 연산자 발생 횟수, N2는 전체 피연산자 발생 횟수다.
이 값으로 프로그램 길이, 어휘, 볼륨, 난이도, 노력, 구현 시간을 산출한다.
- 프로그램 길이(N): N = N1 + N2
코드의 실제 길이 - 프로그램 어휘(n): n = n1 + n2
프로그램에 사용된 고유 요소의 수 - 프로그램 볼륨(V): V = N * log2(n)
정보량을 비트 단위로 표현 - 난이도(D): D = (n1/2) * (N2/n2)
프로그램 이해의 어려움 정도 - 노력(E): E = D * V
프로그램 구현에 필요한 노력 지수 - 구현 시간(T): T = E/18
프로그램 구현에 소요되는 시간(초)
프로그램 코드:
sum = 0;
for (i = 1; i <= 10; i++) {
sum = sum + i;
}
연산자: =, for, <=, ++, +
피연산자: sum, 0, i, 1, 10
n1 = 5 (고유 연산자 수)
n2 = 5 (고유 피연산자 수)
N1 = 8 (총 연산자 발생 횟수)
N2 = 9 (총 피연산자 발생 횟수)
프로그램 길이(N) = 8 + 9 = 17
프로그램 어휘(n) = 5 + 5 = 10
프로그램 볼륨(V) = 17 * log2(10) ≈ 56.5
난이도(D) = (5/2) * (9/5) = 4.5
노력(E) = 4.5 * 56.5 ≈ 254.3
구현 시간(T) = 254.3/18 ≈ 14.1초
측정값을 설계와 리팩토링에 연결하는 방법
복잡도 관리는 구현 후에만 시작하는 일이 아니다. 설계 단계에서는 기능을 모듈로 나누고, 불필요한 세부사항은 추상화하며, 계층을 통해 관심사를 분리한다. 검증된 디자인 패턴도 설계 복잡도를 다루는 수단이 된다.
구현 단계에서는 함수나 메소드가 하나의 기능을 맡게 하고, 중첩된 조건문을 줄이는 편이 좋다. 의미 있는 이름은 코드의 가독성을 높이고, 중복 제거는 DRY(Don't Repeat Yourself) 원칙과 연결된다.
운영 중인 코드에는 허용 가능한 복잡도 임계값을 두고, 정기적인 코드 리뷰로 증가 신호를 일찍 찾는다. 자동화된 측정 도구와 점진적 리팩토링을 함께 사용하면 복잡도 관리가 일회성 정리가 아니라 지속적인 개선 과정이 된다.
SonarQube는 코드 품질과 복잡도를 종합 분석하며, PMD는 자바 코드 분석과 복잡도 측정에 사용된다. ESLint는 자바스크립트 코드 분석에, Radon은 파이썬 코드 복잡도 측정에 쓰인다. Visual Studio Code Metrics는 .NET 프로젝트의 복잡도 분석을 제공한다.
복잡도를 낮춘 시스템 개선 사례
대형 은행의 레거시 코어 뱅킹 시스템은 평균 순환복잡도가 25를 초과해 유지보수 비용이 급증했다. 순환복잡도 25 초과 모듈을 작은 단위로 분해하고, 공통 기능과 유틸리티 클래스를 추출했으며, 복잡한 조건문에는 전략 패턴을 적용했다. 그 결과 평균 순환복잡도는 15로 감소했고, 버그 발생률은 45% 감소했으며, 유지보수 비용은 30% 절감됐다.
대형 통신사의 빌링 시스템은 평균 Halstead 볼륨이 3000을 초과하는 복잡한 모듈로 구성돼 있었다. 마이크로서비스 아키텍처를 도입하고 도메인 주도 설계와 명령-쿼리 책임 분리(CQRS) 패턴을 적용한 뒤, Halstead 볼륨 평균은 1200으로 감소했다. 시스템 확장성은 200% 개선됐고 신규 기능 개발 주기는 60% 단축됐다.
복잡도 지표는 개발 리소스 배분, 테스트 전략, 리팩토링 우선순위를 정하는 객관적 기준이 된다. 설계부터 유지보수까지 측정과 개선을 이어가면 품질과 비용뿐 아니라 개발 팀의 생산성, 고객 만족도, 비즈니스 민첩성에도 영향을 준다.