Architecture & Design/Software Architecture

Foundations - 05. 기술 부채와 의사결정

대출을 받아 출하한다 — 기술 부채의 본질

사업자가 대출을 받아 지금 투자하면, 지금 가치를 얻는 대신 이자를 치른다. 상환 계획이 있으면 현명한 선택이고, 없으면 파산으로 이어진다. 소프트웨어에서도 비슷한 일이 벌어진다. 출시일을 맞추기 위해 결제 모듈의 카드 번호 길이 검증을 건너뛰고 코드 옆에 // TODO: 출시 후 추가를 달아둔다. "출시 후"는 오지 않는다. 6개월 뒤 한 사용자가 잘못된 카드 번호로 결제를 시도하고, 검증 없는 시스템은 그걸 승인 처리해 버린다. 환불 비용과 고객 신뢰 하락이 그 TODO 주석의 이자였다.

이것이 기술 부채(technical debt)다. "나중에 고치지"로 미뤄둔 결정이 이자를 치는 현상. 이 은유는 널리 쓰이면서도 자주 오해된다 — 부채를 "나쁜 코드"와 동일시하거나, "모든 빚을 갚아야 한다"거나 반대로 "빚은 항상 나쁘다"고 단정하는 식이다. 은유가 무엇을, 무엇이 아닌 것을 가리키는지, 그리고 언제 빚을 지고 언제 갚아야 하는지를 따져본다.

커닝엄의 원래 은유 — 의도적 단축

"기술 부채"를 처음 쓴 건 워드 커닝엄(Ward Cunningham)이다. 그의 원래 은유는 오늘날 느슨한 용법과 중요하게 다르다. 커닝엄이 말한 부채는 깨끗하지 않은 코드를 출하(ship)하는 것, 즉 지금 출하해서 가치를 얻고 나중에 정리하겠다는 의식적 선택이다 — 사업자가 대출을 받아 지금 투자하는 것과 같다. 그리고 그 부채에는 이자가 붙는다. 그 코드를 기반으로 무언가를 개발할 때마다 부채 때문에 개발이 느려진다.

두 가지가 핵심이다. 첫째, 부채는 무지나 게으름의 결과가 아니라 출하를 위해 의도적으로 치른 단축이다. 둘째, 이자는 '변경의 마찰'로 나타난다. 부채가 많은 코드는 다음 변경이 더 느리고 위험해진다.

이 원 정의가 시사하는 역설적 사실이 있다. 잘 쓰인 기술 부채는 현명한 결정일 수 있다. 시장 출시 속도가 생존인 상황에서 완벽한 코드를 위해 3개월을 기다리는 것보다, 약간의 부채를 지고 지금 출하해 피드백을 받는 쪽이 이긴다. 빚 자체가 악이 아니다 — 빚을 지는 결정이 의도적이냐, 상환 계획이 있느냐가 문제다. 대출도 상환 계획 없이 받으면 파산이지만, 계획이 있으면 레버리지가 된다.

파울러의 부채 사분면 — 네 종류의 빚

커닝엄의 은유(의도적 부채)를 보면, "기술 부채"가 오늘날 무엇이든 담는 표현으로 느슨해진 이유가 보인다. 사람들은 "부채"라며 실수로 생긴 엉망 코드까지 포함시켜 버린다. 마틴 파울러는 이 혼란을 정리하려 기술 부채 사분면을 제안했다. 두 축으로 네 가지 부채를 나눈다:

의도적(deliberate) 무의식적(inadvertent)
신중함(prudent) (1) "지금 출하하자, 나중에 정리" (2) "그땐 몰랐는데 이젠 더 나은 법을 알겠다"
무모함(reckless) (3) "구조? 시간 없어, 일단 짠다" (4) "뭐가 문제야? 깔끔한데?" (문제 자체를 모름)

(1) 신중·의도적은 커닝엄의 원래 부채다. 출하를 위해 합의된 단축이고 상환 계획이 있다. 건강한 부채다. (2) 신중·무의식적은 당시엔 최선이었으나 더 배운 뒤 "더 나은 구조가 있었다"를 깨닫는 부채다. 무지의 부채지만 부끄러울 것 없다 — 학습의 자연스러운 결과다. (3) 무모·의도적은 "시간 없으니 무시한다"며 논의조차 안 한 채 치르는 단축이다. 위험한 부채다. (4) 무모·무의식적은 부채가 있다는 사실조차 모르는 상태로, 가장 치명적이다 — 고칠 기회조차 인지 못한다.

(1)과 (2)는 건강하고 피할 수 없는 부채다. (3)과 (4)가 진짜 적이며, 특히 (4)는 인지조차 못 하므로 가장 위험하다. "기술 부채"를 막연히 탓하기보다, 우리 부채가 어느 사분면인가를 먼저 가늠하는 것이 출발점이다.

본질적 복잡성과 우발적 복잡성 — 갚을 것과 갚지 못할 것

부채를 논할 때 빠지면 안 되는 구분이 있다. Fred Brooks가 나눈 본질적 복잡성(essential)우발적 복잡성(accidental)이다.

본질적 복잡성은 문제 자체에서 온다. 결제 도메인의 복잡한 규칙, 동시성 요구, 분산 합의 — 이것들은 문제가 원래 어려운 것이다. 줄일 수 없다(문제를 덜 복잡한 것으로 바꾸지 않는 한). 우발적 복잡성은 우리가 선택한 도구·기법·구조에서 온다. 과도한 간접층, 불필요한 추상화, 잘못 고른 프레임워크, 레거시 호환 코드 — 이것은 제거 가능한 부채의 큰 원천이다.

우발적 복잡성을 본질적 복잡성처럼 받아들이는 순간, 갚을 수 있는 부채를 갚지 않게 된다. "이 도메인은 원래 복잡해"라며 실은 도구 때문에 생긴 복잡성까지 합리화하는 우를 범한다. 반대로 본질적 복잡성을 갚으려 드는 것도 낭다 — 그것은 부채가 아니라 문제의 본질이다. 부채 상환 에너지는 우발적 복잡성에 쏟아야 한다.

이자와 상환 — 점진적으로, 가시화해서

부채의 이자는 "변경 마찰"이다. 이자가 치명적이 되면 두 증상이 나타난다. 변경 비용의 비선형 증가 — 원래 1주면 될 변경이 3주가 된다. 팀의 속도가 서서히 떨어지고, 원인을 찾기 어렵다(속도 저하가 한 번에 오지 않고 누적되므로). 그리고 변경 회피 — 위험한 코드를 건드리기 싫어 기능 추가가 우회로로 이루어진다. 이것이 부채를 더 키운다(우회로 자체가 새 부채).

상환은 두 가지로 한다. 하나는 점진적 리팩터링이다. 매 변경마다 지나가는 자리를 조금씩 개선한다. "보이스카웃 규칙(boy scout rule)" — 떠날 때 들어올 때보다 조금 더 깨끗하게. 별도의 '부채 상환 스프린트'를 요구하지 않고 일상에 녹인다. 둘은 정기 상환 시간이다. 부채가 이미 이자를 크게 치르는 수준이면 별도 시간 할애가 필요하다. 이때 파울러가 강조하듯 리팩터링은 거대한 한 번의 작업이 아니라 작고 안전한 단계들의 연속이어야 한다. 각 단계마다 테스트가 행동을 보존한다.

"부채 상환"이란 명목으로 재작성(rewrite)을 하겠다는 유혹이 자주 든다. 재작성은 거의 항상 과소평가된다 — 기존 코드에 박힌 암묵적 규칙을 놓친다. Joel Spolsky가 "절대 하지 말아야 할 일"이라 부른 재작성은 점진적 리팩터링보다 훨씬 위험하다. 부채 상환의 기본은 점진적·테스트 보호된 리팩터링이다.

부채의 교활함은 보이지 않는다는 것이다. 균형 잡힌 재무제표가 없어서, 많은 팀이 부채의 존재를 속도 저하로만 느낄 뿐 어디에 얼마나 있는지 모른다. (4) 사분면(무의식적 부채)이 위험한 이유도 이것이다. 가시화는 거창한 도구가 아니어도 된다. 부채를 ADR이나 전용 목록에 적으면 명시적 결정이 돼 (1) 사분면으로 옮겨진다. // DEBT: 주석이나 코드 분석 도구 경고로 "빚의 위치"에 표식을 남기고, 상환을 기능 작업에 묶어 현실적으로 갚히게 한다. 측정은 완벽할 필요가 없다 — 어디에 빚이 있는지 대략 아는 것이 모르는 것과의 결정적 차이다.

설계 사례 — 점진적 리팩터링으로 부채 상환하기

한 결제 서비스에 카드 번호 검증이 없는 채로 6개월이 지났다. // TODO: 카드 번호 검증 추가 주석만 있고, 실제 검증은 전혀 구현되지 않았다. 이 부채의 이자가 이미 치러졌다 — 잘못된 카드 번호로 결제가 승인되는 사고가 발생했다.

이걸 어떻게 상환하는가. 재작성이 아니라 점진적 리팩터링이다.

flowchart LR
    subgraph Before["부채 상태"]
        B1["CheckoutService<br/>검증 없음, TODO 주석만"]
    end
    subgraph Step1["1단계: 테스트 확보"]
        S1["기존 동작 보존 테스트 작성<br/>(검증 없이 통과하는 현상 기록)"]
    end
    subgraph Step2["2단계: 검증 추가"]
        S2["CardNumber 값 객체 생성<br/>+ Checkout에 검증 호출"]
    end
    Before --> Step1 --> Step2
// Before — 부채: 검증 없음, 잘못된 번호 그대로 통과
class CheckoutService {
    void process(Order order) {
        String cardNumber = order.getCardNumber();
        paymentGateway.charge(cardNumber, order.total());
        // TODO: 카드 번호 검증 추가, 출시 후
        // → 6개월 동안 방치 → 잘못된 번호로 결제 승인 사고
    }
}

// 1단계: 테스트 확보 — 현재 동작(검증 없음)을 기록
@Test
void 현재_카드번호_검증_없이_통과됨() {
    // 이 테스트는 부채의 존재를 문서화 — 상환 후 실패해야 함
    Order order = Order.withCardNumber("invalid-number");
    checkout.process(order);   // 현재는 통과 (검증 없음)
    assertThat(order.isProcessed()).isTrue();
}

// 2단계: CardNumber 값 객체로 검증 추가
public record CardNumber(String value) {
    public CardNumber {
        if (!value.matches("\\d{16}"))
            throw new IllegalArgumentException("카드 번호는 16자리 숫자");
    }
}

class CheckoutService {
    void process(Order order) {
        CardNumber card = new CardNumber(order.getCardNumber());  // 검증 발생
        paymentGateway.charge(card.value(), order.total());
    }
}

// 상환 후 — 부채 테스트가 실패(예상됨) → 새 동작으로 교체
@Test
void 카드번호_검증_실패시_예외() {
    Order order = Order.withCardNumber("invalid-number");
    assertThatThrownBy(() -> checkout.process(order))
        .isInstanceOf(IllegalArgumentException.class);
}

이 상환의 핵심 — 한 번에 검증을 다 넣는 게 아니라, (1) 테스트로 현재 동작을 고정하고, (2) 값 객체로 검증을 추가하고, (3) 테스트를 새 동작으로 교체한다. 각 단계에서 테스트가 보호막이다. 재작성이 아니라 점진적 리팩터링 — 정원에서 잡초를 한 번에 뽑는 게 아니라, 한 포기씩 뽑으면서 흙을 보존하는 것이다.

이자는 매일, 조용히 부풀고 있다

"나중에 고치지"라고 주석을 달기 전에 한 번 더 따져볼 것이 있다. 이것이 커닝엄의 의미에서 부채인가, 아니면 무모한 단축인가. 의도적이고 상환 계획이 있다면 부채로 기록하고, 그렇지 않다면 지금 고치는 편이 싸다. "나중에"는 오지 않는다 — 사업자가 상환 계획 없이 대출을 계속 끌면 이자가 불어나 파산하듯, 코드의 단축도 매일 조용히 이자를 치고 있다. 잘 쓰인 부채는 레버리지가 되지만, 방치된 부채는 팀의 속도를 서서히 갉아먹는다.


참고

  • Cunningham, Ward — "The WyCash Portfolio Management System" (OOPSLA 경험 보고, 1992), 기술 부채 은유 최초 제안 — 접근 2026-07-15
  • Fowler, Martin — "TechnicalDebtQuadrant", https://martinfowler.com/bliki/TechnicalDebtQuadrant.html — 접근 2026-07-15
  • Fowler, Martin — <Refactoring: Improving the Design of Existing Code> (2nd ed, Addison-Wesley, 2018)
  • Brooks, Fred — "No Silver Bullet" (1986) (본질적 vs 우발적 복잡성)
  • Spolsky, Joel — "Things You Should Never Do, Part I" (joelonsoftware.com, 2000) — 접근 2026-07-15