[CS300 #200] 기술 부채 — 빌린 시간에는 이자가 붙는다
컴퓨터공학 300 주제 시리즈의 200번째 글이다. 전체 지도는 여기.
한 줄 요약
기술 부채는 지금 빨리 가려고 택한 덜 좋은 설계·코드가, 나중에 변경할 때마다 추가 비용(이자)을 물게 만드는 현상을 빚에 빗댄 말이다. 빚 자체는 나쁘지 않다. 갚을 계획 없이 쌓이는 빚이 문제다.
왜 필요한가
이번 파트에서 다룬 거의 모든 주제는 결국 이 질문으로 모인다. “오늘 빨리 끝내는 것” 과 “내일도 빨리 고칠 수 있는 것” 사이에서 어떻게 균형을 잡을 것인가. 요구사항을 대충 받으면, 테스트를 건너뛰면, 계층 규칙을 한 번 어기면, 그날은 빨라진다. 그 대가는 몇 주, 몇 달 뒤에 청구된다.
문제는 이 대가가 보이지 않는다는 것이다. 기능 목록에는 “로그인 개선 완료” 만 적히고, 그 과정에서 쌓인 복붙 코드와 빠진 테스트는 적히지 않는다. 그러다 어느 날 “버튼 하나 추가하는 데 왜 2주나 걸리느냐” 는 질문이 나온다. 기술 부채라는 비유는 개발자가 이 보이지 않는 비용을 비개발자와 같은 언어로 이야기할 수 있게 해 준다.
핵심 개념
비유의 출처
Ward Cunningham 은 1992년 OOPSLA 경험 보고서 The WyCash Portfolio Management System에서 이렇게 썼다.
처음 작성한 코드를 내보내는 것은 빚을 지는 것과 같다. 약간의 빚은 개발 속도를 높인다. 다만 그 빚을 다시 쓰기(rewrite)로 신속히 갚는다면. 위험은 빚을 갚지 않을 때 생긴다. 딱 맞지 않는 코드에 쓰는 매 순간이 그 빚의 이자로 계산된다.
원래 비유의 핵심은 “엉망인 코드” 가 아니었다. 문제를 아직 다 이해하지 못한 상태에서 그때의 이해로 코드를 내보내고, 이해가 깊어지면 그것을 코드에 반영해 갚는다는 것이었다. 빨리 내보내 배우는 것은 좋은 전략이고, 배운 것을 반영하지 않고 방치하는 것이 위험하다.
원금과 이자
Martin Fowler 는 TechnicalDebt에서 비유를 이렇게 정리한다.
| 비유 | 소프트웨어에서 |
|---|---|
| 원금 | 부채를 없애는 데 드는 비용(리팩터링, 테스트 추가, 재설계) |
| 이자 | 부채 때문에 새 기능을 추가할 때마다 더 드는 노력 |
| 상환 | 원금을 갚는 작업(리팩터링 등) |
| 파산 | 이자만 내느라 새 일을 못 하는 상태 → 전면 재작성 논의 |
이자 개념이 판단 기준을 준다. 거의 건드리지 않는 코드의 부채는 이자가 거의 없다. 몇 년째 아무도 고치지 않는 지저분한 모듈은 그냥 둬도 된다. 반대로 매주 고치는 핵심 모듈의 부채는 작아도 이자가 크다. 상환 우선순위는 “얼마나 더러운가” 가 아니라 “얼마나 자주 그 위를 지나가는가” 로 정한다.
부채의 네 사분면
Fowler 는 TechnicalDebtQuadrant에서 부채를 두 축으로 나눈다. 신중한가 무모한가(prudent/reckless), 의도했는가 모르고 졌는가(deliberate/inadvertent).
| 무모한(reckless) | 신중한(prudent) | |
|---|---|---|
| 의도적 | “설계할 시간 없어” | “지금은 출시하고, 결과는 감당하자” |
| 비의도적 | “레이어링이 뭐야?” | “이제야 어떻게 했어야 했는지 알겠다” |
- 신중·의도적: 출시 일정을 위해 알고 지는 빚. 갚을 계획(티켓, 기한)이 있다면 건강한 사업 판단이다.
- 무모·의도적: 이자를 모르고 지는 빚. 대개 “나중에” 가 오지 않는다.
- 무모·비의도적: 기본기 부족에서 오는 빚. 교육과 리뷰가 처방이다.
- 신중·비의도적: 잘한 팀에도 반드시 생긴다. 만들어 보고 나서야 더 나은 설계를 알게 되는 빚이다. 피할 수 없고, 배움의 증거다.
부채의 종류
| 종류 | 예 |
|---|---|
| 코드 부채 | 중복, 긴 함수, 매직 넘버(클린 코드 글의 냄새들) |
| 설계·아키텍처 부채 | 계층 규칙 위반, 잘못 그은 서비스 경계 |
| 테스트 부채 | 테스트 없음, 가끔 실패하는 테스트 방치 |
| 의존성 부채 | 지원이 끝난 언어·프레임워크·라이브러리 버전 |
| 인프라·운영 부채 | 수동 배포 절차, 문서 없는 서버 설정, 손으로 만든 리소스 |
| 문서·지식 부채 | 한 사람만 아는 시스템 |
의존성 부채는 이자가 갑자기 폭등하는 종류다. 오래된 버전에 보안 취약점이 발표되는 날, 몇 년치 업그레이드를 한꺼번에 해야 한다.
직접 해 보기
부채가 팀의 산출을 어떻게 갉아먹는지 단순한 모형으로 본다. 숫자는 모두 설명용 가정이다. 핵심은 “이자는 부채 잔액에 비례해 다음 스프린트 용량을 잠식한다” 는 구조다.
# 기술 부채의 '이자' 모형 (숫자는 모두 설명용 가정이다)
# - 팀의 한 스프린트 용량은 100.
# - 기능 작업 1 단위마다 부채가 0.15 쌓인다(서두른 코드, 빠진 테스트).
# - 쌓인 부채 1 단위마다 다음 스프린트 용량의 0.5 가 '이자'로 사라진다
# (이해하기 어려운 코드를 해독하고, 깨지기 쉬운 곳을 조심하느라 쓰는 시간). 상한 90.
# - 상환에 쓴 용량 1 은 부채를 1 줄인다.
def simulate(repay_ratio, sprints=12):
debt, shipped, history = 0.0, 0.0, []
for _ in range(sprints):
interest = min(debt * 0.5, 90)
usable = 100 - interest
repay = usable * repay_ratio
feature = usable - repay
debt = max(debt - repay, 0) + feature * 0.15
shipped += feature
history.append(feature)
return shipped, debt, history
print("상환 비율 12스프린트 누적 기능 남은 부채 스프린트별 기능 산출(1, 4, 8, 12번째)")
for r in (0.0, 0.1, 0.2, 0.3):
shipped, debt, h = simulate(r)
print(f"{r:6.0%} {shipped:12.0f} {debt:7.1f} "
f"{h[0]:5.1f} {h[3]:5.1f} {h[7]:5.1f} {h[11]:5.1f}")
print("\n기간을 늘리면 최적 상환 비율이 바뀐다(누적 기능 산출)")
print("상환 비율 6스프린트 12스프린트 24스프린트")
for r in (0.0, 0.1, 0.2, 0.3):
print(f"{r:6.0%} " + " ".join(f"{simulate(r, n)[0]:9.0f}" for n in (6, 12, 24)))
실행 결과:
상환 비율 12스프린트 누적 기능 남은 부채 스프린트별 기능 산출(1, 4, 8, 12번째)
0% 810 121.5 100.0 79.1 57.9 42.4
10% 936 46.4 90.0 81.0 75.5 70.3
20% 910 11.3 80.0 75.5 75.5 75.5
30% 801 10.0 70.0 66.5 66.5 66.5
기간을 늘리면 최적 상환 비율이 바뀐다(누적 기능 산출)
상환 비율 6스프린트 12스프린트 24스프린트
0% 498 810 1128
10% 495 936 1690
20% 457 910 1816
30% 402 801 1600
읽을 거리가 세 가지 있다.
- 상환을 안 하면 처음에는 가장 빠르다. 첫 스프린트의 기능 산출이 100 으로 가장 높고, 6스프린트까지 누적으로도 1등이다. 단기 지표만 보는 조직이 상환을 미루는 이유가 여기 있다.
- 그러나 속도가 계속 떨어진다. 상환 0% 팀의 스프린트별 산출은 100 → 79 → 58 → 42 로 줄어든다. “왜 점점 느려지느냐” 는 질문의 정체다. 반면 20% 를 상환하는 팀은 산출이 75.5 근처에서 안정된다.
- 최적 비율은 기간에 따라 다르다. 6스프린트 안에 끝나는 일회성 프로젝트라면 상환하지 않는 것이 합리적일 수 있다. 24스프린트를 갈 제품이라면 20% 상환 팀이 0% 팀보다 60% 넘게 더 만든다. 그리고 너무 많이 갚아도(30%) 손해다. 부채 관리는 0 을 만드는 일이 아니라 이자를 감당할 수 있는 수준으로 유지하는 일이다.
실제 계수는 팀과 코드베이스마다 다르고 측정하기도 어렵다. 이 모형은 숫자를 예측하려는 것이 아니라, 부채 논의에서 “기간” 과 “이자” 를 빼놓으면 안 된다는 구조를 보여 주려는 것이다.
현업에서는
- 부채를 보이게 만든다. 알고 진 부채는 이슈 트래커에 “기술 부채” 라벨로 남기고, 이유와 상환 조건을 적는다. 보이지 않는 부채는 관리할 수 없다. 코드 안의
TODO는 이슈 번호와 함께 남긴다. - 상환 시간을 정례화한다. 많은 팀이 스프린트 용량의 일정 비율을 부채 상환과 개선에 고정으로 배정한다. 매번 기능과 경쟁시키면 부채는 언제나 진다.
- 자주 지나가는 길부터 갚는다. 변경 빈도(커밋 이력)가 높고 복잡도도 높은 파일이 이자를 가장 많이 내는 곳이다. 앞의 리팩터링 글에서 본 “보이스카우트 규칙” 이 이런 곳에서 효과가 크다.
- 인프라 부채도 같다. 홈랩 클러스터에서 “일단 손으로 띄워 둔” 리소스, 문서 없이 바꾼 노드 설정, 고정해 둔 오래된 이미지 태그는 모두 부채다. 장애가 나는 날 이자가 한꺼번에 청구된다. 매니페스트를 Git 에 두고, 자동 업데이트 알림을 켜 두고, 변경을 기록하는 습관이 상환 계획이다.
- 전면 재작성은 마지막 수단이다. 이자가 원금보다 커 보일 때 “처음부터 다시 짜자” 는 유혹이 생긴다. 하지만 기존 시스템에 녹아 있는 수많은 예외 처리와 업무 지식까지 다시 만들어야 한다. 앞 글의 스트랭글러 무화과 방식처럼 점진적으로 교체하는 편이 대개 안전하다.
파트 10 을 마치며
소프트웨어 공학 파트의 스무 편은 하나의 질문을 여러 각도에서 다뤘다. 생명주기와 애자일은 언제 확인할지를, 요구사항과 UML 은 무엇을 만들지를, SOLID 와 디자인 패턴과 아키텍처는 어떻게 나눌지를, 테스트와 리뷰와 CI/CD 는 어떻게 믿을지를, 그리고 기술 부채는 그 모든 결정의 비용이 언제 청구되는지를 다뤘다. 혼자 짜는 코드와 팀이 몇 년 동안 고치는 코드의 차이가 여기 있다.
확인 문제
- Cunningham 의 원래 비유에서 “빚을 진다” 는 것은 무엇을 뜻했는가?
- 기술 부채의 “이자” 를 한 문장으로 정의하라.
- 거의 수정하지 않는 지저분한 모듈의 부채를 급히 갚지 않아도 되는 이유는?
- Fowler 의 사분면에서 “이제야 어떻게 했어야 했는지 알겠다” 는 어느 칸이며, 왜 피할 수 없는가?
- 위 모형에서 6스프린트 기준으로는 상환 0% 가 유리하지만 24스프린트 기준으로는 20% 가 유리한 이유는?
풀이
- 문제를 다 이해하기 전, 그때의 이해로 만든 코드를 먼저 내보내는 것. 이해가 깊어지면 그것을 코드에 반영(다시 쓰기)해 빚을 갚아야 한다.
- 부채 때문에 새 기능을 추가하거나 변경할 때마다 추가로 드는 노력이다.
- 이자는 그 코드를 건드릴 때 발생하므로, 거의 건드리지 않는 코드의 부채는 이자가 거의 없다. 상환 비용이 이자보다 크다.
- 신중하고 비의도적인 부채. 실제로 만들어 보고 사용해 봐야 더 나은 설계를 알게 되는 경우가 많아서, 아무리 잘하는 팀도 사전에 모든 것을 알 수는 없다.
- 상환에 쓴 용량은 당장의 기능 산출을 줄이지만, 부채 잔액을 낮춰 이후 모든 스프린트의 이자를 줄인다. 짧은 기간에는 상환 비용이 이자 절감보다 크고, 긴 기간에는 누적된 이자 절감이 상환 비용을 넘어선다.
더 읽을거리 (References)
- Ward Cunningham, The WyCash Portfolio Management System, OOPSLA ‘92 Experience Report
- Martin Fowler, TechnicalDebt
- Martin Fowler, TechnicalDebtQuadrant
- Philippe Kruchten, Robert Nord, Ipek Ozkaya, Managing Technical Debt: Reducing Friction in Software Development, Addison-Wesley, 2019 (서지 정보)