4.1 코드 복잡도 메트릭
개요 및 동기
1976년 Thomas J. McCabe가 도입한 사이클로매틱 복잡도는 코드 한 조각의 제어 흐름을 통한 독립적인 경로의 수를 센다: 모든 if, 반복문, 분기는 그 수를 늘린다. 그것은 거의 50년이 지난 지금도 여전히 가장 널리 사용되는 코드 복잡도 메트릭으로 남아 있으며, 인지 복잡도(McCabe의 원래 선형 개수보다 중첩되고 따라가기 어려운 제어 흐름에 더 무거운 가중치를 두는 것)와 중첩 깊이 같은 친척들과 함께한다. 이 메트릭들은 실재하고 검증된 통찰을 공유한다: 더 많은 독립적인 경로를 가진 코드는 완전히 테스트하기 더 어렵고, 추론하기 더 어려우며, 수십 년의 실증 연구에서, 측정 가능하게 결함을 포함할 가능성이 더 높다.
이 주제는 그 통찰을 진정으로 존중하면서 동시에 그 한계도 동등하게 진지하게 다룬다. 복잡도 메트릭은 코드의 한 가지 특정한 속성을 측정하며, 코드베이스는 어떤 분기 세기 알고리즘도 감지할 수 없는 방식으로 설계가 나쁘거나, 이름이 잘못 지어졌거나, 개념적으로 일관성 없으면서도 모든 복잡도 메트릭에서 단순할 수 있다. 반대로, 일부 환원 불가능하게 복잡한 문제는 올바르게 해결하기 위해 진정으로 복잡한 코드가 필요하며, 복잡도 점수를 최소화하라는 압박을 받는 팀은 실제로는 본질적인 복잡도를 줄이는 대신 더 많은 파일과 간접 계층에 그것을 퍼뜨림으로써 점수는 좋지만 실제로는 이해하기 더 어려운 코드를 만들어 낼 수 있다.
대규모 팀에게 복잡도 메트릭은 코드 품질에 대한 독립형 평결로서가 아니라 트리아지 도구로서 제 몫을 한다: 수천 개의 파일 중에서 더 자세히 살펴볼 가치가 가장 큰 작은 부분집합을 찾는 방법이다. 어느 개인도 전체를 다 읽을 수 없을 만큼 큰 코드베이스를 유지하는 엔터프라이즈와 정부 조직은 희소한 리팩토링과 리뷰 노력을 가장 많은 도움이 될 곳으로 이끌기 위해 이 트리아지 기능에 의존한다.
핵심 원칙
- 복잡도 메트릭은 테스트와 결함의 어려움을 예측하며, 품질을 직접 측정하지 않는다. 그것들을 평결이 아니라 하나의 입력으로 취급하라.
- 복잡도 점수는 진정한 단순화뿐 아니라 난독화를 통한 게임에도 노출된다. 복잡도를 더 많은 파일에 분산시키는 것은 코드를 실제로 이해하기 쉽게 만들지 않으면서도 점수를 낮출 수 있다.
- 일부 복잡도는 우발적이 아니라 본질적이다. 진정으로 어려운 문제는 진정으로 복잡한 코드가 필요할 수 있다. 목표는 무분별하게 모든 복잡도를 없애는 것이 아니라 우발적 복잡도를 최소화하는 것이다.
- 복잡도 메트릭을 트리아지를 위해 사용하고, 개인이나 팀 성적표로 사용하지 마라. 그것들은 어디를 봐야 할지 가리키는 것이지 누구를 비난할지 가리키는 것이 아니다.
- 추세와 이상치가 어떤 절대적인 임계값보다 더 중요하다. 상승하는 추세나 극단적인 이상치는 단일한 팀 전체 평균보다 더 실행 가능하다.
권장 사항
복잡도 메트릭을 사용해 리뷰와 리팩토링 노력을 트리아지하라
코드베이스 전체에 걸쳐 복잡도 분석을 실행하고 그 결과를 사용해 더 자세한 사람의 리뷰나 리팩토링 투자가 가장 큰 보상을 줄 곳의 우선순위를 정하라: 코드베이스 자체의 전형적인 범위를 훨씬 넘어 점수를 받는 함수나 파일이 먼저 살펴볼 가장 가치 높은 곳이다. 이 트리아지 사용, 즉 어디를 봐야 할지 찾는 것은 복잡도 메트릭의 가장 방어 가능하고 가치 있는 적용이며, 그것들을 절대적인 합격/불합격 게이트로 사용하는 것보다 훨씬 그렇다.
보편적인 수치가 아니라 당신 자신의 코드베이스에 상대적으로 임계값을 설정하라
업계 관습에서 무비판적으로 빌려온 절대적인 복잡도 임계값(복잡도 점수 10이 흔히 인용되는 경험 법칙이다)은 당신의 영역에 따라 너무 관대하거나 너무 엄격할 수 있다: 파서나 규칙 엔진은 전형적인 CRUD 서비스보다 정당하게 더 높은 기준 복잡도를 가질 수 있다. 당신 자신의 임계값을 코드베이스의 실제 분포에 비추어 보정하고, 당신의 팀이 그 트레이드오프를 완전히 인식하고 의도적으로 더 엄격한 정책을 선택하지 않은 한, 임계값 위반을 자동 빌드 실패가 아니라 더 자세히 살펴보라는 신호로 취급하라.
진정한 단순화 없는 분해를 통한 게임을 경계하라
복잡도 점수가 게임당하는 가장 흔한 방법은 이 특정 메트릭에 적용된 주제 1.2의 대체 패턴이다: 진정으로 복잡한 하나의 함수를 개별적으로는 좋은 점수를 받는 여러 개의 더 작은 함수로 쪼개는 것이며, 전체 시스템은 똑같이 이해하기 어렵게 남거나, 때로는 더 어려워지는데, 논리가 이제 더 많은 파일에 걸쳐 더 많은 간접 참조와 함께 흩어져 있기 때문이다. 복잡도 메트릭을 분해가 진정으로 코드를 명확하게 했는지, 아니면 단지 메트릭이 더 이상 볼 수 없는 곳으로 복잡도를 옮겼을 뿐인지에 대한 정성적인 리뷰와 짝지어라.
반응하기 전에 본질적 복잡도를 우발적 복잡도로부터 구별하라
높은 복잡도 점수를 고쳐야 할 문제로 취급하기 전에, 근본적인 문제가 진정으로 그만큼 많은 독립적인 경로를 필요로 하는지 물어보라, 예를 들어 세법 계산 논리는 정당하게 많은 분기를 가진다, 아니면 복잡도가 피할 수 있는 원인, 즉 평평하게 만들 수 있는 깊이 중첩된 조건문, 통합할 수 있는 중복된 논리, 혹은 다시 그릴 수 있는 불명확한 책임 경계에서 오는지. 오직 두 번째 범주만이 이 메트릭이 당신을 고치도록 이끌어야 할 진정한 품질 문제다.
스냅샷 평균뿐 아니라 추세와 이상치를 추적하라
약간 움직이는 코드베이스 전체 평균 복잡도 점수는 그 자체로는 좀처럼 실행 가능하지 않다. 여러 변경에 걸쳐 급격히 상승하는 특정 파일의 복잡도, 혹은 그렇지 않으면 잘 작동하는 코드베이스에서 소수의 극단적인 이상치가 훨씬 더 유용한 신호다. 시간에 따른 추세와 이상치 꼬리 둘 다를 추적하고, 그것들을 사용해 광범위하고 초점 없는 복잡도 감소 이니셔티브가 아니라 구체적이고 목표에 맞춘 조사를 촉발하라.
트레이드오프: 장단점
| 접근법 | 장점 | 단점 |
|---|---|---|
| 절대적인 보편적 임계값 | 단순하고, 일관되며, 자동화하기 쉬움 | 정당한 영역 차이를 무시함; 분해를 통해 게임당할 수 있음 |
| 코드베이스에 상대적인 임계값 | 실제 맥락에 더 잘 보정됨 | 더 많은 설정과 주기적인 재보정이 필요함 |
| 자동화된 빌드 게이트로서의 복잡도 | 사람의 리뷰 오버헤드 없이 일관성을 강제함 | 정당하게 복잡하지만 잘 설계된 코드를 막거나 난독화된 분해를 보상할 수 있음 |
| 사람의 리뷰를 위한 트리아지 신호로서의 복잡도 | 분해 홀로는 놓칠 진정한 품질 문제를 잡아냄 | 완전히 자동화된 게이트보다 더 많은 사람의 리뷰 시간이 필요함 |
핵심 긴장은 자동화 대 판단이다. 완전히 자동화된 복잡도 게이트는 시행하기 저렴하고 일관되지만, 정당하게 복잡하고 잘 설계된 코드를 막을 수도 있고 아무것도 진정으로 단순화하지 않으면서 점수를 게임하는 피상적인 분해를 보상할 수도 있다. 자동화된 복잡도 분석을 사용해 리뷰 후보를 드러내고, 실제 판단, 즉 이 복잡도는 본질적인가 우발적인가, 이 리팩토링은 진정으로 명확하게 했는가 아니면 단지 복잡도를 옮겼을 뿐인가는 엄격한 자동화된 게이트 홀로가 아니라 사람의 리뷰어를 위해 남겨 둠으로써 이 긴장을 해결하라.
팀과 논의할 질문
우리의 복잡도 임계값은 우리 자신의 코드베이스의 실제 분포에 보정되어 있는가, 아니면 일반적인 업계 관습에서 무비판적으로 빌려온 것인가? 당신의 코드베이스의 실제 복잡도 분포를 가져와, 흔히 인용되는 수치가 당신의 영역에 보편적으로 적용된다고 가정하는 대신 당신의 현재 임계값이 그것에 비추어 말이 되는지 확인하라.
결과 코드가 실제로 더 이해하기 쉬워지지 않은 채 함수가 여러 개의 더 작은 것으로 쪼개진 적이 있는가? 이것이 이 주제가 경고하는 분해 게임 패턴의 가장 명확한 신호다. 주로 복잡도 점수에 동기 부여된 최근의 리팩토링을 살펴보고 그것이 진정한 이해 가능성을 개선했는지 정직하게 평가하라.
우리 코드베이스에서 복잡도가 문제에 본질적인 곳은 어디이고, 우발적이고 고칠 수 있는 곳은 어디인가? 당신의 가장 높은 복잡도 이상치를 살펴보고 그것들을 이 두 범주로 명시적으로 분류하라. 오직 두 번째 범주만이 진정하고 실행 가능한 품질 문제를 나타내기 때문이다.
우리는 리뷰 노력을 트리아지하기 위해 복잡도 메트릭을 사용하는가, 아니면 사람의 판단이 전혀 관여하지 않는 엄격한 자동화된 게이트로 사용하는가? 당신의 현재 시행 접근법이 이 주제가 권장하는 본질적-대-우발적 구분을 위한 여지를 남기는지, 아니면 맥락과 무관하게 모든 위반을 동일하게 취급하는지 논의하라.
복잡도 점수가 비공식적으로라도 개별 엔지니어의 작업 품질을 판단하는 데 사용된 적이 있는가? 이는 코드 메트릭에 적용된, 주제 3.4가 활동 메트릭에 대해 경고하는 것과 같은 개인 평가 함정의 위험을 무릅쓰며, 같은 게임 반응을 초래한다.
가장 중요하고 가장 자주 변경되는 파일에 대해 지난 1년 동안 우리의 복잡도 추세는 어떻게 보이는가? 이것을 주제 4.3의 처닝과 핫스팟 분석과 결합하라. 매우 복잡하면서 자주 변경되는 파일은 복잡하지만 거의 손대지 않는 파일보다 훨씬 더 먼저 관심을 받을 자격이 있기 때문이다.
분야별 관점
스타트업. 이 규모에서는 복잡도 메트릭이 보통 덜 긴급하다. 코드베이스 크기는 비공식적인 친숙함이 흔히 공식적인 측정을 대체할 만큼 충분히 작다. 일찍 채택할 가치가 있는 습관은 단순히 팀이 비공식적으로 알아채기에는 너무 커지기 전에 특정 파일이 조용히 관리할 수 없게 되는 것을 잡아내기 위해 이따금 복잡도 스캔을 실행하는 것이다.
중소기업. 대부분의 현대적인 정적 분석 도구는 더 넓고, 무료이거나 저렴한 린팅 설정의 일부로 복잡도 메트릭을 보고한다. 전용 도구에 투자하는 대신 그 산출물을 주기적인 트리아지 신호로 사용하라. 가장 자주 수정되는 파일에 먼저 관심을 집중하라.
엔터프라이즈. 규모에서 복잡도 메트릭은 어느 개인도 수동으로 조사할 수 없을 만큼 큰 코드베이스에 걸쳐 리팩토링 투자의 우선순위를 정하기 위해 처닝 데이터(주제 4.3)와 결합될 때 가장 가치가 있다. 하나의 조직 전체 수치를 적용하는 대신 서비스나 영역별로 임계값을 보정하라. 정당한 복잡도는 다른 종류의 시스템에 걸쳐 상당히 다르기 때문이다.
정부. 장수명 정부 시스템은 흔히 수년이나 수십 년에 걸친 점진적인 요구 사항 변경을 통해 점진적으로 복잡도를 축적하며, 복잡도 감사는 그렇지 않으면 시스템을 단순히 “작동하고 있으니” 투자할 가치가 없다고 볼 수 있는 이해관계자에게 현대화나 리팩토링 투자를 정당화하기 위한 설득력 있고 구체적인 도구가 될 수 있다.
사례
엔터프라이즈. 한 결제 처리 회사는 처음으로 코드베이스 전체 복잡도 감사를 실행했고 코드베이스 중앙값의 열 배가 넘는 사이클로매틱 복잡도 점수를 가진 단일한 거래 검증 함수를 발견했다. 조사 결과 그 복잡도는 거의 전적으로 우발적이었다: 특정 결제 제공업체를 위해 수년에 걸쳐 점진적으로 추가된 특수 사례 처리가 제공업체별 논리를 분리하는 더 깔끔한 전략 패턴으로 재구성될 수 있는 깊이 중첩된 조건문으로 축적되어 있었다. 복잡도 감사가 코드베이스에서 단일한 가장 가치 높은 목표로 식별했기 때문에 직접 우선순위가 매겨진 그 리팩토링은 그 함수의 복잡도 점수를 80% 넘게 줄였고, 더 중요하게는, 이후 두 분기에 걸쳐 그 특정 코드 경로의 결함률을 측정 가능하게 줄였다.
정부. 한 세무 당국의 수십 년 된 복지 계산 엔진은 거의 모든 함수에서 복잡도 메트릭에서 극도로 높은 점수를 받아, 전체 시스템이 처음부터 다시 작성되어야 한다는 초기 가정을 촉발했다. 본질적 복잡도와 우발적 복잡도를 구별하는 더 자세한 함수별 리뷰는 복잡도 대부분이 진정으로 근본적인 법적 규칙을 반영했다는 것을 발견했으며, 그 규칙은 실제로 법령에 의해 명령된 그만큼 많은 정당한 분기와 특수 사례를 가지고 있었다. 반면 더 작은 부분집합은 비슷한 계산 경로에 걸친 피할 수 있는 중복에서 왔다. 그 팀은 오직 우발적 복잡도 부분집합만을 리팩토링 대상으로 삼아, 비싸고 위험한 완전한 재작성을 피하면서도 시스템에서 진정으로 문제가 있는 영역을 의미 있게 개선했다.
비즈니스 사례: 동기, ROI, TCO
복잡도 메트릭을 잘 사용하는 것의 수익은 목표를 겨냥한 고가치 리팩토링 투자다: 위의 결제 회사 사례는 복잡도 분석을 통해 식별된, 광범위하고 목표가 없는 리팩토링 이니셔티브가 요구했을 비용의 극히 일부로 정확히 가장 위험한 코드 경로에서 측정 가능하게 결함을 줄인 단일하고 잘 목표가 맞춰진 수정을 보여 준다.
총 소유 비용은 낮다: 대부분의 현대적인 개발 도구 체인은 정적 분석(주제 4.4)의 일부로 자동으로 복잡도 메트릭을 계산하며, 실제 투자는 상당한 새로운 도구 비용이 아니라 결과를 올바르게 해석하는 사람의 판단 시간, 즉 본질적 복잡도를 우발적 복잡도로부터 구별하고 분해 게임을 잡아내는 것이다.
안티패턴과 함정
- 복잡도 점수를 직접적인 품질 평결로 취급하기: 그것은 전체 코드 품질이 아니라 하나의 특정한 속성을 측정한다.
- 진정한 단순화 없이 점수를 게임하기 위해 함수를 쪼개기: 이 주제가 구체적으로 이름 붙이는 분해 게임 패턴.
- 당신 자신의 코드베이스에 보정하지 않고 보편적인 임계값 적용하기: 영역에 따라 너무 관대하거나 너무 엄격한 시행을 만들어 낸다.
- 복잡도 메트릭을 사용해 엔지니어를 개별적으로 평가하기: 게임을 초래하고 판단이 아니라 트리아지를 위한 메트릭을 잘못 적용한다.
- 모든 복잡도를 똑같이 고칠 수 있는 것으로 취급하기: 진정으로 어려운 문제에서 오는 본질적 복잡도는 제거해야 할 결함이 아니다.
- 평평하고 코드베이스 전체의 평균을 선호해 추세와 이상치를 무시하기: 이 메트릭 패밀리가 제공하는 가장 실행 가능한 신호를 놓친다.
성숙도 모델
- 1단계, 시작: 복잡도가 측정되지 않거나, 검토되지 않고 무비판적으로 적용된 일반적인 보편적 임계값으로 측정된다.
- 2단계, 개발: 복잡도 메트릭이 수집되지만 좀처럼 행동으로 이어지지 않으며, 본질적 복잡도와 우발적 복잡도 사이에 구분이 만들어지지 않는다.
- 3단계, 표준화: 임계값이 코드베이스 자체의 분포에 보정되며, 복잡도 메트릭이 조직 전체에서 일관되게 리뷰 및 리팩토링 트리아지를 이끈다.
- 4단계, 관리: 복잡도 추세와 이상치가 능동적으로 모니터링되고 처닝 데이터(주제 4.3)와 결합되어 리팩토링 투자의 우선순위를 정하며, 분해 게임이 능동적으로 경계된다.
- 5단계, 조율: 조직은 복잡도 정보에 기반한 리팩토링 투자로 직접 거슬러 올라가는 구체적이고 측정 가능한 결함률 개선을 제시할 수 있으며, 복잡도 데이터는 엔지니어링 투자 결정에 대한 일상적이고 신뢰받는 입력이다.
논의를 위한 아이디어
- 우리의 단 하나의 가장 복잡한 함수나 파일은 무엇이며, 그 복잡도는 본질적인가 우발적인가?
- 진정한 단순화 없이 분해를 통해 복잡도 점수를 게임한 적이 있는가?
- 우리의 임계값은 우리 자신의 코드베이스에 보정되어 있는가, 아니면 무비판적으로 빌려온 것인가?
- 우리 코드베이스에서 지금 높은 복잡도가 높은 처닝과 겹치는 곳은 어디인가?
- 복잡도 데이터가 리팩토링 투자 결정을 알려준 적이 있는가, 아니면 사용되지 않은 채 남아 있는가?
핵심 요약
- 사이클로매틱 복잡도 같은 복잡도 메트릭은 테스트와 결함의 어려움을 예측하며, 전체 코드 품질을 직접 측정하지 않는다.
- 높은 점수에 반응하기 전에 본질적 복잡도(진정으로 어려운 문제에서 오는)를 우발적 복잡도(더 나은 설계로 피할 수 있는)로부터 구별하라.
- 분해 게임을 경계하라: 아무것도 진정으로 단순화하지 않으면서 점수를 낮추기 위해 코드를 쪼개는 것.
- 개인 성적표나 엄격한 자동화된 게이트가 아니라 사람의 리뷰와 리팩토링 노력을 이끄는 트리아지를 위해 복잡도 메트릭을 사용하라.
- 임계값을 당신 자신의 코드베이스의 분포에 보정하고, 단순한 평균이 아니라 추세와 이상치를 추적하라.
참고 문헌 및 추가 자료
- McCabe, Thomas J. “A Complexity Measure.” IEEE Transactions on Software Engineering, 1976.
- McConnell, Steve. Code Complete: A Practical Handbook of Software Construction. Microsoft Press, 2004.
- Feathers, Michael. Working Effectively with Legacy Code. Prentice Hall, 2004.
- Campbell, G. Ann. “Cognitive Complexity: A New Way of Measuring Understandability.” SonarSource, 2018.