14.2 품질과 유지보수성 — 지표를 목표가 아니라 증거로 쓴다
테스트는 실행해서 관찰할 수 있는 위험만 덮는다. 설계의 퇴화, 이해의 손실, 변경 비용의 누적은 실행 결과에 나타나지 않는 위험이고, 이를 다루는 증거 수단이 코드 리뷰, 정적 지표, 그리고 변경 이력이다. 이 문서는 리뷰가 실제로 잡는 것의 연구 증거, 지표가 목표가 될 때의 왜곡, git 로그로 기술 부채를 계측해 상환을 이자 계산으로 판단하는 모델을 다룬다.
학습 목표
- 코드 리뷰가 실제로 잡는 결함과 잡지 못하는 결함을 연구 증거로 구분하고 리뷰의 기대치를 재설정한다.
- 정적 지표(복잡도·중복·커버리지)가 대리하는 것과 지표가 목표가 될 때의 왜곡을 설명한다.
- 변경 빈도와 복잡도의 곱으로 핫스팟을 계측해 기술 부채의 위치를 식별한다.
- 부채 상환 여부를 이자(변경 비용 증가)와 예상 변경 빈도로 판단한다.
배경: 실행되지 않는 위험
주문 확정 서비스에 14.1의 테스트 하네스를 갖췄다고 하자. 회귀는 잡히고 배포는 안전해졌다. 그런데 6개월 뒤, 같은 서비스의 기능 하나를 추가하는 데 걸리는 시간이 두 배가 되어 있다. 테스트는 전부 통과한다. 어떤 테스트도 "이 코드는 점점 바꾸기 어려워지고 있다"를 실패로 보고하지 않기 때문이다.
이것이 유지보수성(maintainability)의 위험이 가진 특징이다. ISO/IEC 25010이 신뢰성·보안과 나란히 품질 특성으로 정의하는 유지보수성은 실행 시점이 아니라 변경 시점에 청구되는 비용이어서, 실행 기반 증거(테스트, SLI)로는 관찰되지 않는다. 대신 세 가지 다른 원천에서 증거를 얻는다. 사람의 판단(코드 리뷰), 코드의 구조적 성질(정적 지표), 그리고 실제 변경의 기록(git 로그)이다. 세 원천 모두 측정하는 것과 알고 싶은 것 사이에 간극이 있는 대리 증거라는 공통 한계를 갖고, 이 문서의 판단 모델은 그 간극을 다루는 방법이다.
핵심 개념
코드 리뷰: 기대와 실제의 간극
코드 리뷰에 대한 가장 널리 인용되는 실증 연구는 Bacchelli와 Bird가 Microsoft의 개발자·관리자·테스터를 대상으로 수행한 조사(ICSE 2013)다. 결과의 구조가 흥미롭다. 리뷰를 하는 최우선 기대로는 결함 발견이 꼽혔지만, 실제 리뷰 코멘트를 분석하자 기능 결함에 대한 지적은 소수였고 대부분은 코드 개선(가독성, 구조, 관례), 대안 제시, 지식 전달에 관한 것이었다. 기대와 산출이 어긋나 있는 것이다.
이 간극의 원인으로 연구가 지목한 것은 이해(understanding) 다. 리뷰어가 변경의 맥락 — 왜 이 변경인가, 주변 코드는 어떻게 동작하는가 — 을 충분히 이해하지 못하면, 깊은 결함 대신 표면적으로 확인 가능한 것(네이밍, 스타일, 명백한 실수)만 지적하게 된다. 결함 검출력은 리뷰라는 형식이 아니라 리뷰어의 맥락 이해에서 나온다.
여기서 두 가지 판단이 나온다.
첫째, 리뷰와 테스트의 분업을 명시한다. 실행해야 드러나는 결함(경계값, 동시성, 성능)은 리뷰어의 눈보다 테스트가 싸고 확실하게 잡는다. 리뷰가 비교 우위를 갖는 것은 실행으로 관찰되지 않는 것들이다. 설계가 기존 구조와 정합하는가, 이 추상화가 다음 변경을 쉽게 하는가, 코드가 의도를 드러내는가, 놓친 요구사항은 없는가. "리뷰어가 버그를 잡아줄 것"이라는 기대로 테스트 없는 변경을 리뷰에 넘기는 것은, 연구가 보여준 리뷰의 실제 검출 분포에 비추면 증거 없는 배포와 크게 다르지 않다.
둘째, 리뷰어의 이해 비용을 낮추는 것이 리뷰 품질에 대한 투자다. 변경을 작게 나누고, 왜 이 변경인지를 설명에 담고, 테스트를 함께 제출해 기대 동작을 보여주는 것은 예의가 아니라 검출력을 사는 비용이다. 수백 파일짜리 변경에 붙은 "LGTM"은 리뷰가 아니라 리뷰의 형식만 남은 것이다.
리뷰의 산출 중 결함 검출만 세면 리뷰는 비싸 보인다. 그러나 연구가 확인한 실제 산출 — 지식 전달, 코드베이스에 대한 집단 이해, 설계 일관성 — 은 아래에서 다룰 유지보수성 위험을 줄이는 증거 활동이다. 리뷰의 가치를 "잡은 버그 수"로 평가하는 팀은 리뷰가 실제로 사고 있는 증거를 계산에서 빠뜨리고 있는 것이다.
정적 지표: 대리 증거의 의미와 한계
정적 지표는 코드를 실행하지 않고 구조에서 계산한다. 각 지표가 실제로 재는 것과 그것이 대리하는 것을 구분해야 한다.
| 지표 | 실제로 재는 것 | 대리하려는 것 | 간극이 벌어지는 경우 |
|---|---|---|---|
| 순환 복잡도 | 제어 흐름 그래프의 선형 독립 경로 수 | 이해 비용, 필요한 테스트 케이스 수 | 경로는 적지만 상태·데이터 흐름이 복잡한 코드 |
| 중복도 | 유사 코드 블록의 비율 | 변경 시 수정 누락 위험 | 우연히 닮았을 뿐 함께 변하지 않는 코드 |
| 커버리지 | 테스트 중 실행된 코드 비율 | 검증 강도 | 단언 없는 실행(14.1) |
| 함수·파일 크기 | 줄 수 | 책임의 응집도 | 길지만 단순한 테이블성 코드 |
순환 복잡도(cyclomatic complexity)는 McCabe(1976)가 제어 흐름 그래프에서 정의한 값으로, 모든 분기를 한 번씩 지나는 데 필요한 테스트 케이스 수의 하한이라는 정확한 의미가 있다. 복잡도 15인 함수를 테스트 3개로 덮었다면 최소 12개의 경로 조합이 미검증이라는 계산은 유효한 증거다. 그러나 "복잡도가 높으니 나쁜 코드"로 건너뛰면 대리 관계를 실측으로 오인한 것이다. 거대한 switch로 짜인 상태 기계는 복잡도 수치가 높아도 구조가 균질해 이해 비용이 낮을 수 있다. 정적 분석이 이런 값을 어떻게 계산하고 원리적으로 무엇을 놓치는지는 챕터 5의 정적 분석이 다루며, 여기서 중요한 것은 운영적 판단이다. 지표는 조사할 곳을 가리키는 증거이지, 그 자체로 판결이 아니다.
지표를 판결로 승격하는 순간 Goodhart의 법칙이 작동한다. 경제학자 Goodhart의 관찰을 Strathern이 일반화한 형태로 널리 알려진 명제 — 측정치가 목표가 되면 좋은 측정치이기를 멈춘다 — 는 지표와 대리 대상의 간극이 있는 모든 곳에 적용된다. 최적화 압력은 정확히 그 간극으로 흘러 들어간다.
- "함수 복잡도 10 이하" 게이트 → 분기를 그대로 둔 채 함수만 쪼개 수치를 분산시킨다. 이해 비용은 오히려 늘 수 있다.
- "커버리지 90%" 게이트 → 단언 없는 테스트, getter 테스트가 늘어난다. 검증 강도는 그대로다.
- "중복 0%" 목표 → 우연히 닮은 코드까지 공통 추상화로 묶어, 함께 변하지 않는 것들을 결합시킨다.
세 사례의 공통 구조에 주목해야 한다. 지표는 처음에는 실제 문제(복잡한 함수, 미검증 코드, 위험한 중복)와 상관관계가 있었다. 목표가 되는 순간 사람들은 문제가 아니라 지표를 직접 조작하는 최단 경로를 찾고, 상관관계가 끊어진다. 그래서 지표의 안전한 용법은 게이트가 아니라 추세와 이상 감지다. 같은 코드베이스에서 복잡도 상위 목록이 어떻게 변하는지, 새 코드의 커버리지가 급락한 변경이 무엇인지를 보고 조사를 시작하는 신호로 쓴다.
기술 부채: 은유의 원래 의미
기술 부채(technical debt)는 Cunningham이 OOPSLA 1992 경험 보고에서 만든 은유다. 원문의 맥락은 자주 오해되는 것보다 정교하다. 그가 말한 부채는 "대충 짠 나쁜 코드"가 아니라, 아직 불완전한 이해를 담은 코드를 출시하는 것이다. 첫 출시 코드는 빚을 지는 것과 같고, 빚은 더 빠른 출시라는 가치를 준다. 문제는 원금이 아니라 이자다. 이해와 코드의 불일치가 남아 있는 동안 그 위에서 이뤄지는 모든 변경은 추가 시간을 지불하며, 갚지 않은 부채 위에 쌓인 코드는 이자를 복리로 만든다.
이 은유가 판단 모델로 유용한 이유는 부채를 도덕이 아니라 경제로 다루게 하기 때문이다.
- 부채는 죄가 아니라 거래다. 마감 전에 빠른 경로를 선택하는 것은, 이자를 알고 갚을 계획이 있다면 합리적 차입이다. 문제가 되는 것은 무엇을 차입했는지 기록하지 않는 무자각 부채다.
- 이자율은 위치에 따라 다르다. 이자는 변경할 때만 청구된다. 아무도 건드리지 않는 모듈의 흉한 코드는 이자율 0%의 부채이므로 갚을 필요가 없다. 매주 변경되는 모듈의 얽힌 코드는 고이자 부채다.
- 상환에도 비용이 있다. 리팩터링은 시간이 들고 그 자체가 회귀 위험이다. 상환 가치 = 절감될 미래 이자 − 상환 비용이며, 이 값이 음수인 부채는 안고 가는 것이 옳다.
핫스팟: 이자율을 git 로그로 계측한다
"이자율이 높은 부채"를 감으로 찾으면 목소리 큰 사람의 불만이 우선순위가 된다. 계측 가능한 근사가 있다. 이자는 변경 시 청구되므로 변경 빈도가 청구 횟수의 실측이고, 복잡도가 회당 이자의 대리다. 두 값의 곱이 큰 파일 — 핫스팟(hotspot) — 이 상환 우선순위의 후보다. 변경 빈도는 git 로그에서 직접 나온다.
# 최근 12개월 동안 파일별 변경 횟수 상위 10개 (macOS/Linux 셸 기준)
git log --since="12 months ago" --pretty=format: --name-only \
| sort | uniq -c | sort -rn | head -10 87 src/order/confirm-order.ts ← 변경 빈도 1위
54 src/order/pricing.ts
41 src/inventory/stock.ts
12 src/report/monthly.ts
9 src/legacy/tax-rules.ts ← 복잡하기로 악명 높지만 변경은 드묾
...복잡도 축은 순환 복잡도 도구를 쓸 수도 있고, 줄 수나 최대 들여쓰기 깊이 같은 값싼 대리도 추세 비교에는 충분하다. 두 축을 곱해 보면 판단이 사분면으로 정리된다.
복잡도 높음
│
[안고 간다] │ [상환 후보 1순위]
변경이 없으니 │ 청구가 잦고 회당
이자 청구 없음 │ 이자가 비싸다
──────────────────┼────────────────── 변경 빈도 높음
[무시한다] │ [건강한 활성 코드]
│ 복잡해지는지 추세만 감시
│이 계측이 바꾸는 것은 대화의 형태다. "이 코드 너무 지저분해요, 리팩터링 스프린트가 필요해요"는 취향과 일정의 협상이지만, "이 파일은 12개월간 87번 변경됐고 복잡도 상위 3%입니다. 변경당 추가 소요를 보수적으로 잡아도 연간 이 정도 이자를 내고 있습니다"는 비용 계산이다. 흥미로운 부수 효과도 있다. 악명 높은 코드가 사실 거의 변경되지 않는다는 것(위 예시의 tax-rules.ts)이 데이터로 드러나, 감정적으로 가장 불만스러운 코드와 경제적으로 갚을 가치가 있는 코드가 다르다는 것을 팀이 확인하게 된다.
상환을 결정했다면 실행 순서는 14.1이 제공한다. 핫스팟은 정의상 자주 변경되는 코드이므로 특성화 테스트로 현재 동작을 고정하는 투자의 회수가 빠르고, seam 도입 → 구조 개선의 각 단계가 테스트라는 안전망 위에서 진행된다. 부채 상환과 테스트 하네스 구축은 별개 작업이 아니라 같은 작업의 두 단면이다.
실무 관점
리뷰 대기 시간은 품질 비용이다
리뷰가 병목이 되면 두 가지 퇴행이 온다. 변경이 리뷰를 기다리며 커지고(큰 변경은 리뷰 품질을 떨어뜨린다), 개발자들이 리뷰를 통과 의례로 취급하기 시작한다. 리뷰 SLA(예: 영업일 1일 내 첫 응답)를 두는 것은 관료제가 아니라, "리뷰어의 이해가 검출력"이라는 연구 결과를 운영에 반영하는 것이다. 작은 변경이 빨리 돌수록 회당 이해 비용이 낮아지고 검출력이 유지된다.
기계가 잡을 것을 사람에게 시키지 않는다
들여쓰기, 임포트 순서, 네이밍 관례에 대한 리뷰 코멘트는 리뷰어의 이해 예산을 가장 낮은 가치의 지적에 소비한다. 포매터와 린터가 잡을 수 있는 것은 전부 자동화로 옮기고, 사람의 리뷰는 기계가 볼 수 없는 것 — 설계 정합성, 요구사항 해석, 다음 변경에 대한 영향 — 에 집중시킨다. 리뷰 논쟁이 스타일에서 반복된다면 그것은 리뷰 문화 문제가 아니라 자동화 공백이다.
"리팩터링 스프린트"보다 변경에 얹는 상환
부채 상환을 별도 프로젝트로 만들면 두 가지가 어긋난다. 기능 개발과 제로섬 협상이 되어 늘 밀리고, 변경 빈도와 무관하게 "눈에 띄는" 코드부터 고치게 된다. 핫스팟 모델과 정합하는 방식은 변경에 상환을 얹는 것이다. 어차피 건드리는 파일이 핫스팟이라면 그 변경에 상환 비용을 포함해 견적한다. 자주 변경되는 곳일수록 이 기회가 자주 오므로, 이자율이 높은 부채일수록 자연히 먼저 갚아진다. 다만 구조 전체를 바꿔야 하는 큰 상환은 이 방식으로 쪼개기 어려우므로, 그때는 핫스팟 데이터를 근거로 별도 항목으로 계획한다.
지표 대시보드는 결정에 연결될 때만 가치가 있다
복잡도·중복·커버리지 대시보드를 만들어 놓고 아무 결정도 바뀌지 않는 팀이 많다. 지표 수집 전에 물어야 할 것은 "이 수치가 어떤 값이면 우리는 무엇을 다르게 하는가"다. 답이 없는 지표는 수집 비용만 있는 장식이고, 답이 "게이트로 막는다"라면 Goodhart 왜곡을 예약한 것이다. 좋은 답의 형태는 "신규 코드 커버리지가 급락한 PR은 리뷰에서 테스트 계획을 묻는다", "핫스팟 상위 10개 목록을 분기마다 갱신해 상환 계획에 반영한다"처럼 지표가 사람의 조사와 판단을 트리거하는 것이다.
더 깊이: 부채 은유의 오용과 재작성 판단
Cunningham은 훗날 이 은유가 "나쁜 코드를 짜도 된다는 면허"로 오용된다고 지적했다. 원래 은유에서 차입이 정당한 이유는 출시로 얻는 학습 때문이다. 시장에 먼저 도달해 얻은 이해로 코드를 현실에 맞게 고쳐 갚는 것까지가 거래의 전체다. 학습을 코드에 반영하는 상환 단계를 생략하면 은유는 성립하지 않는다. 무모한 코드와 신중한 차입을 가르는 기준은 코드의 상태가 아니라 팀이 무엇을 차입했는지 알고 있는가다.
부채가 한계를 넘으면 재작성이 거론된다. 재작성 판단에도 같은 경제 모델이 적용된다. 재작성은 원금 전액을 한 번에 갚는 대신, 작성 기간 동안 기능이 동결되고, 기존 코드에 축적된 문서화되지 않은 동작 — 그것이 의존자들의 실제 계약이다 — 을 잃을 위험을 진다. 핫스팟이 코드베이스 전반이 아니라 소수 모듈에 집중되어 있다면(경험적으로 흔한 분포다), 전면 재작성보다 핫스팟 단위의 점진 교체가 같은 이자 절감을 훨씬 낮은 위험으로 산다. 전면 재작성이 정당화되는 것은 부채의 분포가 아니라 전제(플랫폼, 언어, 아키텍처 스타일)가 바뀌어 점진 경로 자체가 없을 때다.
정리
- 유지보수성 위험은 실행이 아니라 변경 시점에 청구되므로 테스트로 관찰되지 않으며, 리뷰·정적 지표·변경 이력이라는 대리 증거로 다룬다.
- 연구 증거에 따르면 코드 리뷰의 실제 산출은 결함 검출보다 코드 개선·지식 전달에 있고, 검출력은 리뷰어의 맥락 이해에서 나온다. 테스트가 잡을 것은 테스트에 맡기고 리뷰는 실행으로 안 보이는 것에 집중시킨다.
- 정적 지표는 알고 싶은 것의 대리이며, 목표로 승격되면 최적화 압력이 지표와 실체의 간극으로 흘러 상관관계가 끊어진다(Goodhart). 지표는 게이트가 아니라 조사 트리거로 쓴다.
- 기술 부채의 비용은 원금이 아니라 이자(변경 시 추가 비용)다. 변경 빈도(git 로그)×복잡도의 핫스팟으로 이자율을 계측하면 상환 우선순위가 취향이 아니라 계산이 된다.
- 변경이 없는 부채는 갚지 않고, 상환은 가능한 한 변경에 얹으며, 상환 작업은 특성화 테스트의 안전망 위에서 진행한다.
확인 문제
- 팀이 "리뷰어 2인 승인 필수" 정책을 도입한 뒤에도 프로덕션 결함율이 줄지 않자, 승인 인원을 3인으로 늘리자는 제안이 나왔다. Bacchelli & Bird의 연구 결과에 비추어 이 제안의 전제를 비판하고, 결함율에 실제로 영향을 줄 가능성이 높은 대안 두 가지를 제시하라.
- 경영진이 "전사 순환 복잡도 평균을 내년까지 20% 낮춘다"를 팀 목표(KPI)로 설정했다. 1년 뒤 지표는 달성됐지만 변경 소요 시간은 그대로였다. 무슨 일이 일어났을 가능성이 높은지 설명하고, 같은 의도(변경 비용 절감)를 왜곡 없이 추구하는 목표 설계를 제안하라.
- 핫스팟 분석 결과 A 파일(연 90회 변경, 복잡도 상위 1%)과 B 파일(연 2회 변경, 복잡도 최상위 0.1%)이 나왔다. 팀원들은 "가장 끔찍한 코드"인 B부터 고치자고 한다. 어느 쪽을 먼저 상환해야 하며, B를 방치할 때 감수하는 위험과 그 위험을 싸게 줄이는 방법은 무엇인가?
정답과 해설
- 제안의 전제는 "리뷰어 수 × 리뷰 = 결함 검출량"이지만, 연구가 보여준 것은 리뷰의 결함 검출이 리뷰어의 맥락 이해에 의해 제한되며 실제 코멘트의 대부분이 결함이 아니라 코드 개선에 관한 것이라는 사실이다. 이해 없는 승인자를 늘리면 "다른 사람이 봤겠지"라는 책임 분산으로 오히려 리뷰당 주의가 줄 수 있다. 결함율에 영향을 줄 대안: (1) 실행해야 드러나는 결함은 리뷰가 아니라 테스트의 영역이므로, 변경에 테스트 동반을 요구하고 회귀가 잦은 영역의 검증 강도(뮤테이션 감사 등)를 높인다. (2) 리뷰어의 이해 비용을 낮춘다 — 변경 크기 제한, 변경 의도 설명 의무화, 해당 영역 맥락을 가진 리뷰어 배정. 결함이 특정 영역에 몰린다면 핫스팟 데이터로 그 영역의 구조 개선을 계획하는 것이 리뷰 인원 증가보다 검출 이전 단계에서 결함을 줄인다.
- Goodhart 왜곡의 전형적 경로다. 복잡도 평균은 함수를 기계적으로 쪼개면 분기 구조를 전혀 바꾸지 않고도 내려간다. 호출 계층만 깊어져 이해 비용이 오히려 늘 수 있고, 평균이라는 집계는 신규 단순 코드 추가로도 희석된다. 지표가 목표가 되자 최적화가 "변경하기 쉬운 코드"가 아니라 "낮은 수치"를 직접 겨냥한 것이다. 왜곡을 줄이는 설계: 목표를 대리 지표가 아니라 알고 싶은 결과에 가깝게 둔다 — 예를 들어 핫스팟 상위 파일들의 변경당 소요 시간·변경당 결함율 추세, 또는 "분기마다 핫스팟 상위 N개에 대한 상환 계획 수립·실행"처럼 판단 행위를 목표로 삼는다. 복잡도는 그 과정에서 조사 트리거로만 쓰고 KPI에서 제외한다.
- 이자 모델로는 A가 명백한 우선이다. A는 연 90회 이자가 청구되는 고이자 부채이고, B는 원금이 아무리 커도 연 2회만 청구되는 저이자 부채다. 상환 가치 = 절감될 미래 이자 − 상환 비용에서 B는 절감할 이자 자체가 작고, 최상위 복잡도라 상환 비용과 회귀 위험은 가장 크다. 다만 B의 방치에는 두 가지 잔여 위험이 있다. 드물지만 변경이 필요해졌을 때 아무도 이해하지 못하는 코드가 되어 있을 위험(지식 소실)과, 복잡도가 높은 만큼 잠재 결함이 실행 경로에 숨어 있을 위험이다. 이 위험은 전면 리팩터링 없이 싸게 줄일 수 있다. 현재 동작을 고정하는 특성화 테스트를 씌워 두면 미래 변경의 안전망과 동작 문서가 되고, 입출력 경계와 알려진 특이 동작을 기록해 두면 지식 소실을 완충한다. "가장 끔찍한 코드"라는 감정과 "가장 비싼 부채"라는 계산이 다르다는 것이 이 문제의 핵심이다.
참고 자료
- Alberto Bacchelli, Christian Bird, Expectations, Outcomes, and Challenges of Modern Code Review (ICSE 2013) — 리뷰의 기대(결함 발견)와 실제 산출(코드 개선·지식 전달)의 간극, 이해가 검출력을 결정한다는 본문 주장의 실증 근거다.
- Ward Cunningham, The WyCash Portfolio Management System (OOPSLA 1992) — 기술 부채 은유의 원문으로, 부채가 "나쁜 코드"가 아니라 "이해와 코드의 불일치"라는 원래 의미를 확인한다.
- Thomas J. McCabe, A Complexity Measure (IEEE TSE, 1976) — 순환 복잡도의 원 정의와 "필요한 테스트 경로 수의 하한"이라는 해석의 출처다.
- ISO/IEC 25010:2023 — 유지보수성을 신뢰성·보안과 나란한 품질 특성으로 정의하는 표준 용어를 개요 수준에서 참고한다.
- Marilyn Strathern, "Improving Ratings": Audit in the British University System (European Review, 1997) — "측정치가 목표가 되면 좋은 측정치이기를 멈춘다"는 Goodhart 법칙의 일반화 표현의 출처다.