본문 바로가기

유지보수와 기술 부채

수정, 적응, 완전화, 예방이라는 유지보수의 네 목적을 구별하고, 리팩터링과 기술 부채를 정의합니다. 고치기 전의 동작을 특성화 테스트로 기록한 뒤, 그 기록으로 새 구현이 동작을 지켰는지 판단하는 과정을 실행합니다.

운영 중에 들어온 "같은 참가자가 명단에 두 번 보입니다"라는 결함 신고와 "새 Node.js 버전에서도 실행되게 해 주세요"라는 요청은 모두 소프트웨어를 바꾸는 일이지만, 바꾸는 이유가 다릅니다. 소프트웨어를 전달한 뒤에도 계속 쓸모 있게 고치고 다듬는 활동을 유지보수(maintenance)라고 합니다. 이 글에서는 유지보수의 목적을 나누어 보고, 이미 동작하는 코드를 바꿀 때 무엇이 그대로 남아야 하는지 기록하고 확인하는 방법을 다룹니다.

유지보수의 네 가지 목적

수정 유지보수(corrective maintenance)는 발견된 결함을 고치고, 적응 유지보수(adaptive maintenance)는 운영체제나 실행 환경, 바깥 인터페이스의 변화에 맞춥니다. 완전화 유지보수(perfective maintenance)는 사용성, 기능, 성능을 더 낫게 만들고, 예방 유지보수(preventive maintenance)는 앞으로 생길 결함이나 변경의 위험을 미리 줄입니다. 변경 하나가 여러 목적을 함께 가질 수도 있습니다.

모임 앱으로 예를 들면, 중복 검사의 순서를 바로잡는 일은 결함의 수정이고, 저장 모듈을 새 실행 환경에 맞추는 일은 적응입니다. 오류 안내를 이해하기 쉽게 바꾸는 일은 완전화이고, 저장 자료의 검사를 분명하게 나누어 두는 일은 예방에 가깝습니다. 다만 목적에 이름을 붙이는 것으로 끝내지 말고, 실제로 어떤 동작이 바뀌는지, 어떤 위험이 있는지, 무엇으로 확인할지를 따로 분석해야 합니다.

소프트웨어는 쓰이는 동안 계속 바뀌고, 바뀔수록 구조가 복잡해지는 경향이 있습니다. 레만(Lehman)은 실제 환경에서 쓰이는 소프트웨어의 이런 성질을 소프트웨어 진화의 법칙으로 정리했으며, 그래서 구조를 다듬는 일에 따로 노력을 들이지 않으면 시간이 지날수록 고치기가 어려워집니다.

리팩터링과 기술 부채

리팩터링(refactoring)은 바깥에서 보이는 동작을 그대로 두고 안쪽의 구조를 고치는 변경입니다. 결과 코드의 우선순위를 바꾸는 일은 요구의 변경이나 결함의 수정이지 리팩터링이 아니며, 지켜야 할 동작에는 성공했을 때의 결과뿐 아니라 거절했을 때의 상태와 오류를 알리는 방식까지 들어갑니다.

기술 부채(technical debt)는 당장의 편의를 위해 고른 구조나 미루어 둔 작업이, 나중의 변경에 비용과 위험을 더하는 상태를 빚에 비유한 말입니다. 빚에 이자가 붙듯 미룬 작업은 시간이 지날수록 더 큰 비용이 됩니다. 다만 오래된 코드나 마음에 들지 않는 이름을 모두 부채라고 부르지는 않으며, 왜 그렇게 남았는지, 어떤 위험이 있는지, 언제 갚을지를 기록할 때 비로소 관리할 수 있습니다.

예를 들어 "지금의 저장은 프로세스 하나만 지원한다"는 것은 이 교재가 의도해서 정한 범위의 제약입니다. 하지만 서버를 여러 대로 늘리면서 이 제약을 그대로 두면, 동시 저장과 복구에서 본 갱신 손실의 위험과 그것을 해결할 작업이 생깁니다. 이런 제약을 숨기지 않고 적어 두는 것이 이후 계획의 출발점입니다.

변경 목적을 먼저 확인하고 동작 보존을 검증한다

취소 표현을 바꾸되 외부 계약은 보존한다. 이전 상태도 유지되어야 한다. 각 단계의 값과 전환 이유를 비교합니다.

SOFTWARE ENGINEERING · READING CLUB

변경 목적을 먼저 확인하고 동작 보존을 검증한다

같은 취소 함수라도 오류 수정과 리팩터링의 기대 변화는 다릅니다.

변경 목적을 먼저 확인하고 동작 보존을 검증한다변경 이유: 정책 결함? 환경 변화?, 구조 개선? 미래 위험? → 기대 동작 범위: 오류 교정: 의도한 변화, 리팩터링: 외부 계약 유지 → 회귀와 개선 증거: 취소 결과·이전 상태, 변경·검토 비용의 변화변경 이유정책 결함? 환경 변화?구조 개선? 미래 위험?선택기대 동작 범위오류 교정: 의도한 변화리팩터링: 외부 계약 유지확인회귀와 개선 증거취소 결과·이전 상태변경·검토 비용의 변화
변경 목적을 먼저 확인하고 동작 보존을 검증한다변경 이유: 정책 결함? 환경 변화?, 구조 개선? 미래 위험? → 기대 동작 범위: 오류 교정: 의도한 변화, 리팩터링: 외부 계약 유지 → 회귀와 개선 증거: 취소 결과·이전 상태, 변경·검토 비용의 변화
변경 이유정책 결함? 환경 변화?구조 개선? 미래 위험?
선택선택변경 이유 · 정책 결함? 환경 변화? · 구조 개선? 미래 위험? → 기대 동작 범위 · 오류 교정: 의도한 변화 · 리팩터링: 외부 계약 유지
기대 동작 범위오류 교정: 의도한 변화리팩터링: 외부 계약 유지
확인확인기대 동작 범위 · 오류 교정: 의도한 변화 · 리팩터링: 외부 계약 유지 → 회귀와 개선 증거 · 취소 결과·이전 상태 · 변경·검토 비용의 변화
회귀와 개선 증거취소 결과·이전 상태변경·검토 비용의 변화

줄 수가 줄었다는 사실만으로 개선을 확정하지 않습니다. 요구 변화와 동작 보존 변경을 구분하여 기록합니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 변경 목적부터 회귀 확인까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
변경 목적

취소 표현을 바꾸되 외부 계약은 보존한다.

  • 표현·관측 결과 1: before [min,hana]
  • 표현·관측 결과 2: 취소 대상 min
  • 보존할 기대값 확정
새 표현 실행

내부 표현의 차이보다 결과·상태를 비교한다.

  • 표현·관측 결과 1: CANCELLED
  • 표현·관측 결과 2: after [hana]
  • min만 제거
회귀 확인

이전 상태도 유지되어야 한다.

  • 표현·관측 결과 1: before [min,hana] 유지
  • 표현·관측 결과 2: after [hana]
  • 관찰 동작 보존 단언; 전체 경계의 완전한 검증으로 확대 금지

변경 목적을 먼저 확인하고 동작 보존을 검증한다

취소 표현을 바꾸되 외부 계약은 보존한다. 이전 상태도 유지되어야 한다. 각 단계의 값과 전환 이유를 비교합니다.

SOFTWARE ENGINEERING · READING CLUB

변경 목적을 먼저 확인하고 동작 보존을 검증한다

같은 취소 함수라도 오류 수정과 리팩터링의 기대 변화는 다릅니다.

변경 목적을 먼저 확인하고 동작 보존을 검증한다변경 이유: 정책 결함? 환경 변화?, 구조 개선? 미래 위험? → 기대 동작 범위: 오류 교정: 의도한 변화, 리팩터링: 외부 계약 유지 → 회귀와 개선 증거: 취소 결과·이전 상태, 변경·검토 비용의 변화변경 이유정책 결함? 환경 변화?구조 개선? 미래 위험?선택기대 동작 범위오류 교정: 의도한 변화리팩터링: 외부 계약 유지확인회귀와 개선 증거취소 결과·이전 상태변경·검토 비용의 변화
변경 목적을 먼저 확인하고 동작 보존을 검증한다변경 이유: 정책 결함? 환경 변화?, 구조 개선? 미래 위험? → 기대 동작 범위: 오류 교정: 의도한 변화, 리팩터링: 외부 계약 유지 → 회귀와 개선 증거: 취소 결과·이전 상태, 변경·검토 비용의 변화
변경 이유정책 결함? 환경 변화?구조 개선? 미래 위험?
선택선택변경 이유 · 정책 결함? 환경 변화? · 구조 개선? 미래 위험? → 기대 동작 범위 · 오류 교정: 의도한 변화 · 리팩터링: 외부 계약 유지
기대 동작 범위오류 교정: 의도한 변화리팩터링: 외부 계약 유지
확인확인기대 동작 범위 · 오류 교정: 의도한 변화 · 리팩터링: 외부 계약 유지 → 회귀와 개선 증거 · 취소 결과·이전 상태 · 변경·검토 비용의 변화
회귀와 개선 증거취소 결과·이전 상태변경·검토 비용의 변화

줄 수가 줄었다는 사실만으로 개선을 확정하지 않습니다. 요구 변화와 동작 보존 변경을 구분하여 기록합니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 변경 목적부터 회귀 확인까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
변경 목적

취소 표현을 바꾸되 외부 계약은 보존한다.

  • 표현·관측 결과 1: before [min,hana]
  • 표현·관측 결과 2: 취소 대상 min
  • 보존할 기대값 확정
새 표현 실행

내부 표현의 차이보다 결과·상태를 비교한다.

  • 표현·관측 결과 1: CANCELLED
  • 표현·관측 결과 2: after [hana]
  • min만 제거
회귀 확인

이전 상태도 유지되어야 한다.

  • 표현·관측 결과 1: before [min,hana] 유지
  • 표현·관측 결과 2: after [hana]
  • 관찰 동작 보존 단언; 전체 경계의 완전한 검증으로 확대 금지

바꾸기 전의 동작을 기록한다

그림처럼 내부 구현을 바꾸려면 먼저 지켜야 할 결과와 상태를 정해 두어야 합니다. 요구 문서가 부족한 옛 코드라면, 지금 코드가 실제로 어떻게 동작하는지를 테스트로 기록해 두는데, 이를 특성화 테스트(characterization test)라고 합니다. 다음 코드는 cancel을 바꾸기 전에 네 가지 상황의 결과를 기록합니다. reading-lab/examples/ch9-2.mjs로 저장해 실행합니다.

import assert from "node:assert/strict";
import { createSession, register, cancel, closeSession } from "../core.mjs";

const base = register(register(createSession({ id: "book-1", title: "첫 책", capacity: 2 }), "min").state, "hana").state;
const cases = [
  ["열린 모임에서 min 취소", base, "min"],
  ["명단에 없는 jun 취소", base, "jun"],
  ["형식이 틀린 Min 취소", base, "Min"],
  ["마감 뒤 hana 취소", closeSession(base), "hana"],
];
for (const [label, state, memberId] of cases) {
  const result = cancel(state, memberId);
  console.log(`${label} → ${result.code} ${result.state.status} [${result.state.participants}]`);
}
assert.deepEqual(base.participants, ["min", "hana"]);
console.log("처음 명단은 그대로:", base.participants.join(","));
열린 모임에서 min 취소 → CANCELLED OPEN [hana]
명단에 없는 jun 취소 → NOT_REGISTERED OPEN [min,hana]
형식이 틀린 Min 취소 → INVALID_MEMBER OPEN [min,hana]
마감 뒤 hana 취소 → CANCELLED CLOSED [min]
처음 명단은 그대로: min,hana

네 줄은 정상적인 취소, 명단에 없는 아이디, 형식이 틀린 아이디, 마감 뒤의 취소에서 지금의 cancel이 돌려주는 결과 코드와 상태입니다. 마지막 줄은 취소를 여러 번 했어도 처음 상태의 명단이 그대로라는 것을 보여 줍니다. 이제 cancel의 안쪽을 다른 방식으로 바꾸더라도, 같은 사례에서 이 출력이 그대로 나오는지 비교하면 동작을 지켰는지 판단할 수 있습니다.

다만 이 네 사례가 cancel이 지켜야 할 모든 동작을 담고 있지는 않습니다. 특성화 테스트는 지금의 동작을 기록할 뿐 그 동작이 옳은지는 알려 주지 않으므로, 기록한 결과 가운데 요구와 어긋나는 것이 있다면 리팩터링과 따로 결함으로 다루어야 합니다.

바꾸는 순서

바꿀 때는 먼저 문제를 다시 일으키는 조건과 지켜야 할 동작을 확인하고, 동작을 지키는 가장 작은 변경을 만든 뒤, 관련된 회귀 테스트를 실행합니다. 그리고 새 구조가 실제로 읽고 고치고 확인하기 쉬워졌는지 검토합니다. 큰 이름 바꾸기와 정책의 변경을 한꺼번에 묶어 처리하면, 무언가 깨졌을 때 어느 쪽이 원인인지 찾기 어렵습니다.

더 깊이: 기술 부채라는 말이 처음 가리킨 것

기술 부채라는 비유는 Ward Cunningham이 1992년 OOPSLA 학회에 낸 경험 보고 "The WyCash Portfolio Management System"에서 나왔습니다. 그는 처음 쓴 코드를 내보내는 일을 빚을 지는 것에 견주면서, 약간의 빚은 곧바로 다시 써서 갚기만 하면 개발을 빠르게 해 준다고 보았습니다. 위험은 빚을 갚지 않을 때 생기며, 꼭 맞지 않는 코드 위에서 보내는 시간은 모두 그 빚의 이자로 쌓인다고 했습니다.

보고서가 이 비유로 경고한 것은 통합(consolidation)을 미루는 실패입니다. 덜 다듬어진 코드도 지금은 잘 동작하고 고객이 받아들일 수 있지만, 그런 코드가 지나치게 쌓이면 누구도 프로그램 전체를 다루지 못하게 되고, 프로그래머는 자기 부분에만 매이게 되며, 제품은 바꾸기 어려워진다는 것입니다. 보고서는 이런 빚이 쌓이면 개발 조직 전체가 멈출 수도 있다고 덧붙입니다. 그래서 이 비유의 핵심은 엉성한 코드를 허락하는 데 있지 않고, 빌린 것을 알고 제때 갚는 데 있습니다. 이 글에서 범위의 제약을 기록해 두고 범위가 바뀔 때 다시 보라고 한 것도, 빚이 있다는 사실과 갚을 시점을 드러내 두는 일입니다.

정리

  • 유지보수는 전달한 뒤에도 소프트웨어를 쓸모 있게 고치고 다듬는 활동이며, 수정, 적응, 완전화, 예방의 목적으로 나뉩니다.
  • 리팩터링은 바깥에서 보이는 동작을 지키며 안쪽 구조를 고치는 변경이고, 거절했을 때의 상태와 오류를 알리는 방식도 지켜야 할 동작입니다.
  • 기술 부채는 미루어 둔 작업이 나중의 변경에 비용과 위험을 더하는 상태이며, 이유와 위험과 갚을 시점을 기록해 관리합니다.
  • 특성화 테스트는 바꾸기 전의 동작을 기록해 두어, 새 구현이 같은 결과를 내는지 비교할 기준이 됩니다.
  • 큰 이름 변경과 정책 변경을 한꺼번에 하지 않고, 작은 변경과 회귀 확인을 차례로 이어 갑니다.

연습 문제

1. splice로 바꾼 취소

누군가 cancel을 다음처럼 짧게 바꾸었습니다. 본문의 특성화 사례로 비교하면 어떤 차이가 드러날지 예상하세요.

function cancelBySplice(session, memberId) {
  const index = session.participants.indexOf(memberId);
  if (index === -1) return { ok: false, code: "NOT_REGISTERED", state: session };
  session.participants.splice(index, 1);
  return { ok: true, code: "CANCELLED", state: session };
}
해설 보기

먼저 형식 검사가 빠져 Min의 취소가 INVALID_MEMBER가 아니라 NOT_REGISTERED를 돌려주므로, 셋째 사례의 결과가 달라집니다. 또 splice는 넘겨받은 배열을 직접 고치는데, core.mjs가 만든 상태는 고정되어 있어 min이나 hana를 취소하려 하면 TypeError가 납니다. 고정되지 않은 상태에 썼더라도 처음 상태의 명단까지 바뀌므로, "처음 명단은 그대로"라는 단언이 실패합니다. 코드가 짧아졌다고 동작을 지킨 것은 아니므로, 이 변경은 리팩터링이 아닙니다.

2. 모두 다시 쓰자는 제안

팀원이 "옛 코드니까 전부 다시 쓰자"고 제안했습니다. 지금 명단을 되살리는 기능은 안정적으로 동작하지만, 요구 문서는 부족합니다. 결정하기 전에 확인할 것과, 안전하게 시작할 수 있는 작은 작업을 제시하세요.

해설 보기

실제로 결함이 얼마나 나오는지, 얼마나 자주 바꾸는지, 읽기 어려운 곳이 어디인지, 지원해야 할 환경과 복구 방식이 무엇인지를 먼저 확인합니다. 그다음 정상적인 저장과 복원, 중복과 정원 거절을 특성화 테스트로 기록해 두고, 저장 부분을 따로 떼어 내는 작은 구조 개선부터 시작해 위험을 줄일 수 있습니다. 요구를 모르는 상태에서 전부 다시 쓰면 문서에 없던 동작을 잃어버리기 쉬우므로, 다시 쓰는 이익과 비용이 분명해졌을 때 따로 계획합니다.

3. 유지보수의 목적 나누기

다음 변경을 유지보수의 네 목적으로 나누세요. ㉠ Node.js의 새 주 버전에서 사라진 함수를 다른 함수로 바꾼다. ㉡ 정원이 1일 때 두 번째 신청을 받아 주던 결함을 고친다. ㉢ 아직 문제는 없지만 저장 자료의 검사를 별도 함수로 떼어 테스트를 붙인다. ㉣ 거절 안내에 다른 모임을 추천하는 버튼을 더한다.

해설 보기

㉠은 실행 환경의 변화에 맞추는 적응 유지보수이고, ㉡은 발견된 결함을 고치는 수정 유지보수입니다. ㉢은 앞으로의 결함과 변경의 위험을 줄이는 예방 유지보수이며, ㉣은 사용성과 기능을 더 낫게 만드는 완전화 유지보수입니다. 다만 ㉣처럼 새 기능이 들어가는 변경은 요구 문서와 회귀 사례도 함께 고쳐야 하므로, 목적의 이름과 별개로 영향 범위를 분석합니다.

"코드가 더 깔끔해 보인다"는 인상만으로 변경을 승인해서는 안 됩니다. 다음 글 측정·검토·품질 개선에서는 측정과 검토를 개선의 근거로 쓰는 방법을 다룹니다.