
기술 부채: 코드로 빚을 지다
빠르게 개발하기 위해 저지른 더러운 코드는 '빚'입니다. 나중에 이자(수정 비용)까지 쳐서 갚아야 합니다. 파산하지 않으려면 관리하세요.

빠르게 개발하기 위해 저지른 더러운 코드는 '빚'입니다. 나중에 이자(수정 비용)까지 쳐서 갚아야 합니다. 파산하지 않으려면 관리하세요.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

첫 스타트업을 시작했을 때, 나는 매일 밤 같은 선택의 기로에 섰다. 경쟁사가 다음 주에 출시한다는 소식을 들었고, 우리 제품은 아직 반도 안 됐다. 코드를 보니 이 부분은 제대로 설계하면 3일은 걸릴 것 같았다.
"이거... 일단 하드코딩으로 박고, 나중에 고치자."
키보드를 두드리며 나는 중얼거렸다. 그렇게 복사-붙여넣기를 10번 반복하고, if문을 20개 중첩시키고, 변수명을 temp1, temp2, final_final_v2로 짓고 나니, 기능이 작동했다. 데모는 성공이었다. 투자도 받았다. 하지만 3개월 뒤, 나는 그때의 나를 저주했다.
한 줄 고치려고 하면 세 곳이 터졌고, 새 기능 하나 추가하는데 일주일이 걸렸다. 내가 지은 빚이 이자를 달고 돌아온 것이다. 이것이 바로 기술 부채(Technical Debt)다.
기술 부채라는 개념은 1992년 워드 커닝햄(Ward Cunningham)이 처음 제시했다. 그는 금융의 '부채'를 코드에 비유했다. 집을 사려고 대출받듯이, 빠른 출시를 위해 코드의 품질을 일부 포기하는 것이다.
핵심은 이거다. 부채 그 자체는 나쁜 게 아니다. 부채로 집을 사고, 그 집에서 살며 월급을 받아 부채를 갚으면 결국 순이익이다. 하지만 이자를 안 갚고 방치하면 복리로 불어나 파산한다. 코드도 똑같다.
처음에는 빨리 개발할 수 있었지만, 나중에 그 코드를 수정하려면 10배의 시간이 든다. 이게 이자다. 그리고 방치하면 어느 순간 "차라리 새로 짜는 게 빠르겠어요"라는 말이 나온다. 이게 파산이다.
마틴 파울러(Martin Fowler)는 기술 부채를 4가지 유형으로 나눴다. 내 경험을 대입해보니 딱 맞아떨어졌다.
1. 무모하고 의도적인 부채 (Reckless & Deliberate) "설계가 뭔지 모르겠고, 일단 돌아가면 되지 뭐." 개발 초기의 나다. 디자인 패턴이 뭔지도 모르고, 그냥 막 짰다. 이건 빚이 아니라 도박이다.
2. 신중하고 의도적인 부채 (Prudent & Deliberate) "이번 주까지 출시해야 해. 일단 하드코딩으로 가고, 다음 스프린트에 리팩토링하자." 이게 바로 전략적 부채다. 경영진에게 "2주 후면 완벽하게 만들 수 있는데, 1주 후에 더러운 코드로 출시할 수도 있습니다. 단, 한 달 후에 빚을 갚는 시간이 필요합니다"라고 말할 수 있다면, 이건 좋은 부채다.
3. 무모하고 무의식적인 부채 (Reckless & Inadvertent)
"SOLID 원칙? 들어는 봤는데 뭔지 모르고 짰어요."
실력 부족으로 인한 빚이다. 처음 React를 배울 때 나는 모든 상태를 전역에 때려박았다. 나중에 useContext 지옥을 경험했다.
4. 신중하고 무의식적인 부채 (Prudent & Inadvertent) "아, 이렇게 짜면 되는 거였구나. 이제야 알았다." 처음엔 최선을 다했지만, 나중에 더 나은 방법을 알게 됐을 때다. 이건 어쩔 수 없다. 학습의 부산물이다.
코드가 썩어가고 있다는 신호를 코드 스멜(Code Smell)이라고 한다. 내가 겪은 대표적인 스멜들이다.
1. 중복 코드 (Duplicated Code) 같은 로직이 5곳에 복붙되어 있다. 버그 수정하려면 5곳을 다 고쳐야 한다. 이자율 500%다.
2. 긴 함수 (Long Method) 200줄짜리 함수. 스크롤을 3번 내려야 끝이 보인다. 주석 없으면 뭐 하는 함수인지 모른다.
3. 거대한 클래스 (Large Class) 한 클래스에 메서드가 50개. 이건 클래스가 아니라 백화점이다.
4. 산탄총 수술 (Shotgun Surgery) 하나 고치려면 10개 파일을 열어야 한다. 의존성이 스파게티처럼 얽혀있다.
5. 매직 넘버 (Magic Number)
if (status === 3) 3이 뭔지 아무도 모른다. 6개월 후의 나도 모른다.
금융에서 복리가 무서운 이유는 '이자에 이자가 붙기' 때문이다. 코드도 똑같다.
처음엔 "이 함수 고치는 데 10분 더 걸리네, 뭐 괜찮아." 수준이다. 한 달 뒤엔 "이 부분 건드리면 테스트 3개가 깨지네, 그냥 복붙하자." 수준이 된다. 3개월 뒤엔 "이 모듈은 아무도 손 못 대요. 레거시입니다." 수준이 된다. 1년 뒤엔 "새로 짜는 게 빠릅니다." 수준이 된다.
실제로 겪은 일이다. 초기에 3시간만 투자해서 리팩토링했으면 끝날 일을, 미루고 미뤄서 결국 2주짜리 리라이트 프로젝트로 변했다.
부채를 갚는 방법은 리팩토링(Refactoring)이다. 외부 동작은 그대로 두고, 내부 구조를 개선하는 것이다.
예를 들어, 이런 코드가 있었다고 해보자.
function processOrder(order) {
let total = 0;
for (let i = 0; i < order.items.length; i++) {
if (order.items[i].type === 'book') {
if (order.items[i].price > 50) {
total += order.items[i].price * 0.9;
} else {
total += order.items[i].price * 0.95;
}
} else if (order.items[i].type === 'electronics') {
if (order.items[i].price > 100) {
total += order.items[i].price * 0.85;
} else {
total += order.items[i].price * 0.92;
}
} else {
total += order.items[i].price;
}
}
if (order.customer.isPremium) {
total = total * 0.95;
}
if (total > 200) {
total = total - 20;
}
return total;
}
이 코드는 작동한다. 하지만 새로운 상품 유형을 추가하거나, 할인 정책을 변경하려면 지옥이다. 리팩토링하면 이렇게 된다.
function processOrder(order) {
const subtotal = calculateSubtotal(order.items);
const discounted = applyCustomerDiscount(subtotal, order.customer);
const final = applyBulkDiscount(discounted);
return final;
}
function calculateSubtotal(items) {
return items.reduce((sum, item) => sum + getPriceWithDiscount(item), 0);
}
function getPriceWithDiscount(item) {
const discountRate = getDiscountRate(item);
return item.price * (1 - discountRate);
}
function getDiscountRate(item) {
const rates = {
book: { highPrice: 0.1, lowPrice: 0.05, threshold: 50 },
electronics: { highPrice: 0.15, lowPrice: 0.08, threshold: 100 },
default: { highPrice: 0, lowPrice: 0, threshold: 0 }
};
const config = rates[item.type] || rates.default;
return item.price > config.threshold ? config.highPrice : config.lowPrice;
}
function applyCustomerDiscount(amount, customer) {
return customer.isPremium ? amount * 0.95 : amount;
}
function applyBulkDiscount(amount) {
return amount > 200 ? amount - 20 : amount;
}
코드가 길어졌다고? 맞다. 하지만 이제 새로운 상품 유형을 추가하려면 rates 객체에 한 줄만 추가하면 된다. 할인 정책을 변경하려면 해당 함수만 수정하면 된다. 이게 빚을 갚은 상태다.
"캠핑장을 떠날 때는, 왔을 때보다 깨끗하게 만들어라."
코드도 마찬가지다. 보이스카우트 규칙(Boy Scout Rule)이라고 한다. 파일을 열었다면, 떠나기 전에 조금이라도 개선하고 가라. 변수명 하나, 주석 하나, 함수 분리 하나라도 좋다.
나는 이걸 실천하려고 노력한다. 버그 픽스하러 들어갔다가 변수명 3개 바꾸고, 매직 넘버 상수로 빼고, 중복 코드 함수로 추출하고 나온다. 하루에 5분씩만 투자해도, 1년이면 엄청난 차이가 난다.
빚이 있다는 걸 알아야 갚을 수 있다. 나는 이렇게 추적한다.
1. TODO 주석// TODO: 이 로직 나중에 전략 패턴으로 리팩토링
// FIXME: N+1 쿼리 문제, 나중에 배치로 변경
// HACK: 임시 방편, 다음 스프린트에 제대로 구현
하지만 솔직히 TODO 주석은 무덤이다. 쓰고 나면 영원히 안 본다.
2. 이슈 트래커 Jira나 GitHub Issues에 "Tech Debt" 라벨을 붙여서 관리한다. 스프린트 계획할 때 20%는 무조건 기술 부채 해결에 할당한다.
3. SonarQube 같은 도구 자동으로 코드 품질을 측정해준다. "이 파일은 복잡도가 너무 높습니다", "중복 코드가 30%입니다" 같은 리포트를 준다. 숫자로 보니까 설득력이 있다.
나는 20% 규칙을 따른다. 스프린트 시간의 80%는 새 기능, 20%는 리팩토링과 기술 부채 해결. 이게 안 지켜지면 3개월 후에 속도가 반으로 떨어진다. 실제로 경험했다.
빚이 너무 쌓이면 "차라리 새로 짜자"는 유혹이 온다. 나도 그랬다. 하지만 대부분의 경우, 리라이트는 실패한다.
왜냐하면:
대신 점진적 리팩토링을 해라. 기능 하나씩, 모듈 하나씩 갈아치운다. 마치 달리는 기차의 바퀴를 하나씩 교체하듯이. 이걸 Strangler Fig Pattern이라고 한다. 큰 나무를 감아 천천히 대체하는 무화과 식물 비유다.
가장 어려운 부분이다. CEO나 PM에게 "이번 스프린트는 기능 개발 없고, 리팩토링만 할게요"라고 말하면 돌아오는 대답은 뻔하다.
"그게 사용자한테 보여요? 매출에 도움 돼요?"
나는 이렇게 설명한다.
자동차 비유: "지금 우리 코드는 3년 동안 엔진 오일을 안 갈아서 시커멓게 됐어요. 지금 당장은 달리지만, 다음 달에 엔진이 터질 수 있어요. 오일 교환에 하루만 주시면, 앞으로 6개월은 안전합니다."
집 비유: "벽에 금이 가는데 페인트로 덧칠만 하면, 언젠가 무너져요. 지금 2주 투자해서 기초를 보강하면, 2층을 올릴 수 있어요."
이자 비유: "이 코드는 연이율 50% 대출이에요. 지금 기능 하나 추가하는데 1주일 걸리는데, 3개월 뒤엔 3주일 걸려요. 지금 1주일 투자해서 빚을 갚으면, 앞으로 매번 1주일이면 돼요."
숫자로 보여주는 게 가장 강력하다. "지난 분기엔 기능 10개 출시했는데, 이번 분기엔 5개밖에 못 했어요. 속도가 반으로 떨어졌어요. 기술 부채 때문이에요."
기술 부채는 피할 수 없다. 완벽한 코드를 짤 시간은 없고, 요구사항은 계속 바뀌고, 우리의 실력도 성장한다. 중요한 건 부채를 인정하고, 관리하고, 계획적으로 갚는 것이다.
나는 이제 새벽 2시에 하드코딩을 할 때, 악마와 거래하는 게 아니라 미래의 나와 계약을 한다. "이번 주에 빠르게 출시하는 대신, 다음 주에 이틀을 리팩토링에 쓰겠다." 그리고 실제로 갚는다.
파산하지 마라. 이자를 갚아라. 그러면 부채는 성장의 도구가 된다.