코드스멜·리팩토링·안전망
중복이 많고 함수가 긴 코드를 처음부터 다시 작성하면 구조는 깔끔해질 수 있다. 그러나 기존의 숨은 업무규칙·오류처리·운영계약을 동시에 잃을 수 있고, 기능변경과 구조변경이 한꺼번에 섞이면 무엇이 달라졌는지 검증하기 어렵다.
1. 오래된 코드를 깨끗하게 다시 쓰면 리팩토링일까
중복이 많고 함수가 긴 코드를 처음부터 다시 작성하면 구조는 깔끔해질 수 있다. 그러나 기존의 숨은 업무규칙·오류처리·운영계약을 동시에 잃을 수 있고, 기능변경과 구조변경이 한꺼번에 섞이면 무엇이 달라졌는지 검증하기 어렵다.
이번 절은 리팩토링을 코드스멜 발견 → 행위 안전망 확보 → 작은 구조변경 → 즉시 검증 → 반복으로 다루고, 대규모 재작성과 기능개선을 구분한다.
테스트는 리팩토링의 결과가 아니라 선행 안전망이다
모듈·캡슐화·객체·인터페이스에서는 캡슐화를 내부 상태와 구현을 숨기고 허용된 연산만 노출하여 상태 규칙을 한 책임 경계에서 강제하는 원칙으로 배웠다. 테스트 기획·계획·분석·설계에서는 위험에서 케이스를 설계하고, 시험수준과 유형, 자동화, 결과관리를 통해 변경의 관찰가능한 결과를 검증했다. 리팩토링은 캡슐화된 책임과 공개 경계를 더 명확히 하되 이 안전망으로 외부 행위가 의도치 않게 바뀌지 않았는지 확인한다.
리팩토링
리팩토링은 외부에서 관찰 가능한 행위를 의도적으로 바꾸지 않은 채 내부 코드구조를 작은 단계로 개선하고, 각 단계에서 테스트와 정적검사 같은 안전망으로 행위 보존을 확인하는 변경 활동이다.
새 기능 추가·업무규칙 변경·성능목표 변경은 별도 변경으로 분리해 검증한다.
리팩토링의 기본 루프
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
기존 외부 행동을 확인하고 안전망을 만든 뒤 작은 구조 변경을 검증한다. 실패는 기대값을 새 결과에 맞추어 지우지 않는다. 정책 변경이 승인되면 별도 요구와 오라클로 다룬다.
텍스트 표현으로 다시 확인하기
작은 반복 그림의 현재 행위·안전망·구조변경·검증을 따라간다. 테스트·정적검사 실패는 원인을 확인하고 필요한 경우 되돌린다. 통과한 범위만 기록한 뒤 다음 작은 변경으로 진행한다.
한 단계의 차이를 작게 유지하면 실패원인을 좁히기 쉽다.
2. 코드스멜은 결함 판정이 아니라 조사 신호다
긴 함수, 중복, 큰 클래스, 많은 조건분기, 전역상태, 잦은 동시변경은 구조적 위험을 찾는 단서다. 냄새가 있다고 자동으로 리팩토링하지 않고 변경빈도·결함이력·이해비용·테스트가능성과 함께 우선순위를 정한다.
대표 코드스멜과 질문
| 신호 | 확인할 질문 |
|---|---|
| 중복 | 같은 규칙이 여러 곳에서 독립적으로 변하는가 |
| 긴 함수 | 서로 다른 책임·추상수준이 섞였는가 |
| 큰 클래스 | 변경이유가 여러 개인가 |
| 긴 매개변수 목록 | 하나의 개념을 흩어진 값으로 전달하는가 |
| 전역상태 | 순서·병렬 실행에 숨은 결합이 생기는가 |
| 산탄총 수정 | 한 요구 변경이 너무 많은 파일을 건드리는가 |
이름보다 실제 변경비용과 오류위험을 본다.
RF-1: 세 군데의 동일 할인공식
A·B·C 화면에 amount * 0.9가 각각 복사되어 있고 정책이 15% 할인으로 바뀐다고 하자. 세 곳 중 한 곳만 수정하면 사용자 경로에 따라 결과가 달라질 수 있다. 동일 규칙이 실제로 같은 책임이라면 한 소유경계로 모으는 후보가 된다.
비슷해 보인다는 이유로 다른 규칙을 합치지 않는다
신규고객 10% 할인과 장기회원 10% 할인이 현재 숫자만 같다고 하나의 함수로 합치면 미래 정책변경 때 서로 다른 이유로 변해야 할 규칙이 다시 결합된다. 중복 제거는 의미와 변경이유가 같은지 확인한 뒤 한다.
3. 안전망은 ‘현재 행위’를 명시적으로 고정한다
리팩토링 전에 핵심 입력·출력·상태전이·오류·외부계약에 대한 테스트를 확보한다. 기존 행위가 잘못되었더라도 구조변경과 버그수정을 동시에 섞지 않고, 먼저 현재 행위를 고정한 뒤 별도 요구로 수정할 수 있다.
RF-2: 레거시 함수의 Characterization Test
문서가 부족한 가상 세금계산 함수에 대표 입력 10개를 넣고 현재 출력과 예외를 기록한다. 그 결과가 올바른 세법임을 승인하는 것이 아니라, 구조변경 전후에 무엇이 달라졌는지 감지하는 임시 안전망으로 쓴다.
테스트가 없는데 먼저 대규모 추출을 시작하지 않는다
함수 2,000줄을 여러 클래스로 한 번에 나눈 뒤 끝에서만 시험하면 실패원인이 수십 개 변경에 흩어진다. 먼저 관찰 가능한 계약을 확보하고 작은 단계마다 실행한다.
4. 작은 리팩토링은 하나의 의도를 가진다
함수 추출, 이름 변경, 조건식 분해, 객체 이동, 의존성 주입처럼 한 번에 설명 가능한 구조변경을 수행한다. 한 커밋에서 기능 추가·DB 스키마 변경·프레임워크 교체까지 묶지 않는다.
Extract Function의 판단 순서
- 함께 설명되는 코드 구간을 찾는다.
- 입력·출력·부작용을 명시한다.
- 새 함수로 이동하고 의미 있는 이름을 붙인다.
- 호출부를 바꾼다.
- 테스트·정적검사를 즉시 실행한다.
- 다음 냄새는 별도 단계에서 다룬다.
함수 길이 숫자보다 책임과 추상수준의 경계를 본다.
RF-3: 조건식을 이름으로 바꿔 의도를 드러낸다
if age >= 18 and consent and not blocked라는 조건이 여러 곳에 반복된다면 isEligibleApplicant(...) 같은 의미경계로 추출할 수 있다. 단, 세 조건이 실제로 동일한 업무규칙을 공유하는지 먼저 확인한다.
RF-5: 전·후 코드의 동작이 같다는 말을 입력·출력으로 확인한다
다음 가상 주문금액 함수는 캠페인이 켜져 있고 주문금액이 100000원 이상이면 10%, 회원이면 추가 5%를 더하되 총 할인율을 12%로 제한한다. 음수 금액은 RangeError("amount")로 거부하고 최종 금액은 소수점 이하를 버린다. 이 규칙이 올바른 정책인지 승인하는 것이 아니라, 구조변경 전에 현재 관찰행위를 명시하는 예다.
변경 전 코드는 전역상태와 계산규칙이 한 함수에 섞여 있다.
코드를 읽을 때 rate는 금액이 아니라 할인율이고 0.10은 10%다. rate += 0.10은 현재 할인율에 0.10을 더해 저장한다. Math.min(rate, 0.12)는 두 값 중 작은 값을 골라 할인율을 12% 이하로 제한하며, Math.floor는 여기의 양수 금액에서 소수점 이하를 버린 정수를 돌려준다. campaignEnabled && amount >= 100_000은 캠페인이 켜져 있고 금액이 100000원 이상이라는 두 조건을 모두 만족해야 한다는 뜻이며 숫자의 밑줄은 자릿수 읽기를 돕는 표기다.
예를 들어 캠페인 ON·100000원·회원 입력이면 rate=0에서 캠페인 조건으로 0.10, 회원 조건으로 0.15가 된다. 상한을 적용하면 0.12이고, 금액 계산은 100000×(1−0.12)=88000원이므로 소수점을 버려도 88000원이다. 이 중간 상태를 추출한 discountRate에도 그대로 기대하고, 그 값을 받은 calculatePayable의 결과도 같아야 한다. 두 할인율을 합한 15%를 곧바로 최종 할인율로 쓰는 답은 상한 단계를 빠뜨린 것이므로 Math.min으로 돌아간다.
let campaignEnabled = true;
function payable(amount, isMember) {
if (amount < 0) throw new RangeError("amount");
let rate = 0;
if (campaignEnabled && amount >= 100_000) rate += 0.10;
if (isMember) rate += 0.05;
rate = Math.min(rate, 0.12);
return Math.floor(amount * (1 - rate));
}먼저 정상값만 몇 개 찍지 않고 경계·조합·오류를 고정한다.
| 캠페인 | amount | 회원 | 현재 관찰 결과 | 확인하는 경계 |
|---|---|---|---|---|
| ON | 99999 | 아니오 | 99999 | 캠페인 임계값 바로 아래 |
| ON | 100000 | 아니오 | 90000 | >= 100000 정확 경계 |
| ON | 99990 | 예 | 94990 | 5% 할인에서 버림·반올림 차이 |
| ON | 99999 | 예 | 94999 | 5% 할인과 Math.floor |
| ON | 100000 | 예 | 88000 | 10%+5%가 아니라 12% 상한 |
| OFF | 100000 | 예 | 95000 | 캠페인 상태 의존성 |
| ON | -1 | 아니오 | RangeError("amount") | 오류 계약 |
첫 번째 작은 변경에서는 계산책임만 추출하고 공개 함수 payable(amount, isMember)의 호출형태는 유지한다.
function discountRate(amount, isMember, campaignEnabled) {
let rate = 0;
if (campaignEnabled && amount >= 100_000) rate += 0.10;
if (isMember) rate += 0.05;
return Math.min(rate, 0.12);
}
function calculatePayable(amount, isMember, campaignEnabled) {
if (amount < 0) throw new RangeError("amount");
return Math.floor(
amount * (1 - discountRate(amount, isMember, campaignEnabled))
);
}
function payable(amount, isMember) {
return calculatePayable(amount, isMember, campaignEnabled);
}여기서 calculatePayable은 캠페인 상태를 인자로 받아 결정적으로 시험할 수 있지만, 외부 호출자는 새 매개변수를 알 필요가 없다. 이 단계의 테스트가 모두 통과한 뒤에만 전역상태를 읽는 어댑터 자체를 별도 작은 단계로 바꾼다. 함수 추출과 의존성 변경을 한꺼번에 묶지 않는다.
RF-5에서는 100000원 경계, 12% 상한, 내림 규칙, 음수 예외를 Characterization Test로 고정하고, 함수 추출 뒤 같은 입력의 출력과 예외가 모두 같아야 다음 단계로 진행한다. 예를 들어 추출 중 >=를 >로 바꾸면 100000원 비회원 결과가 90000원에서 100000원으로 변한다. 12% 상한을 빠뜨리면 100000원 회원이 88000원이 아니라 85000원이 되고, Math.floor를 Math.round로 바꾸면 99990원 회원이 94990원에서 94991원으로 변한다. 세 변경은 코드가 더 단순해 보여도 이 범위에서는 리팩토링 실패다.
5. 의존성 주입은 테스트 가능성과 변경경계를 개선할 수 있다
함수 내부에서 현재 시각을 조회하거나 파일·네트워크 의존성을 직접 생성하면 테스트가 실제 환경과 결합되기 쉽다. 필요한 의존성을 외부에서 전달하면 테스트에서 결정적인 대역을 넣고 실제 구현과 계약을 분리하기 쉬워진다.
인터페이스를 늘리는 것 자체가 리팩토링 성공은 아니다
모든 클래스마다 인터페이스를 만들거나 한 줄 함수까지 추출하면 간접층만 늘어 이해가 더 어려워질 수 있다. 실제 변경축·테스트경계·대체 필요성이 있는 곳에만 추상화를 둔다.
6. 리팩토링과 재작성의 위험은 다르다
| 항목 | 점진 리팩토링 | 대규모 재작성 |
|---|---|---|
| 행위 비교 | 단계마다 가능 | 끝단에서 큰 비교 |
| 변경폭 | 작음 | 큼 |
| 숨은 규칙 보존 | 기존 코드에 남아 있음 | 재발견 필요 |
| 롤백 | 작은 단위 | 큰 전환 |
| 새 구조 설계의 자유도 | 제약 있음 | 큼 |
| 주요 위험 | 국소 설계오류 | 기능누락·통합·전환위험 |
재작성 자체를 금지하는 것이 아니라, 재작성과 리팩토링에 같은 위험관리 방식을 적용해도 된다고 혼동하지 않아야 한다.
RF-4: 20개 규칙 중 2개가 문서에 없다면
기존 모듈에 실제 동작하는 업무규칙 20개가 있고 재작성 명세에는 18개만 명시됐다고 하자. 새 구현이 명세 18개를 완벽히 만족해도 숨은 2개 행위가 필요한지 확인하지 않으면 회귀가 생길 수 있다. 기존 행위 비교와 이해관계자 판단을 분리한다.
7. 성능개선과 리팩토링을 구분한다
알고리즘을 O(n²)에서 O(n log n)으로 바꾸어 같은 입력에 같은 결과를 내도록 할 수 있지만 자원사용·실행시간 같은 비기능 행위는 의도적으로 달라진다. 목표가 성능개선이면 그 변경을 명시하고 기능 동등성뿐 아니라 성능 오라클을 별도로 검증한다.
‘테스트 통과’가 외부 행위 전체 보존을 증명한다고 과장하지 않는다
현재 테스트는 관찰한 계약의 표본이다. 로깅 형식, 오류코드, 타이밍, 파일순서처럼 테스트하지 않은 외부 관찰점이 소비자 계약일 수 있다. 변경영향 분석과 통합·인수시험으로 안전망의 공백을 보완한다.
8. 리팩토링 우선순위는 변경 빈도와 위험을 함께 본다
자주 바뀌고 결함이 많으며 이해가 어려운 핵심 경로는 개선가치가 크다. 거의 변하지 않고 안정된 코드에 대규모 구조개편을 하는 것보다 다음 변경을 어렵게 만드는 병목을 먼저 다룬다.
안전한 리팩토링 완료조건
- 의도한 구조변경을 한 문장으로 설명할 수 있는가
- 기능변경이 별도 요구로 분리됐는가
- 핵심 안전망이 변경 전후 모두 통과하는가
- 정적분석·빌드·통합 계약이 유지되는가
- 성능·로그·API 등 외부 관찰점 변화가 있는가
- 작은 커밋 단위로 되돌릴 수 있는가
하나라도 불명확하면 변경을 더 작게 나눈다.
공식 범위 연결 — 애플리케이션 리팩토링
코드스멜을 변경위험과 연결하고, 테스트 안전망을 확보한 뒤 관찰 가능한 행위를 유지하는 작은 구조변경을 반복하여 내부구조를 개선한다.
출처와 적용 경계
리팩토링의 정의는 Martin Fowler의 Definition Of Refactoring(2004-09-01, 확인 2026-10-06)의 내부 구조 개선과 관찰 행위 보존을 참고한다. 작은 변경·안전망·주문금액 적용은 본문의 자체 사례다. 할인규칙·레거시 함수·RF-5 주문금액 코드·규칙 20개 예시는 교육용 자체 가정이며 실제 코드베이스 측정이나 사람 승인 결과가 아니다. RF-5의 JavaScript 형태도 특정 제품 구현을 권장하는 코드가 아니라 행위보존·경계시험을 설명하기 위한 유한 예다.
9. 한 장으로 정리하기
- 코드스멜은 조사 신호이지 자동 결함판정이 아니다.
- 리팩토링은 외부 행위를 의도적으로 바꾸지 않는 구조개선이다.
- 안전망을 먼저 확보하고 작은 변경마다 즉시 검증한다.
- 중복은 의미와 변경이유가 같을 때만 합친다.
- 기능추가·버그수정·성능개선은 구조변경과 분리해 목적을 명시한다.
- 재작성은 더 큰 자유도와 함께 기능누락·전환위험을 가진다.
독립 연습 — 실패한 오라클을 새 정책으로 숨기지 않는다
다음은 본문 사례의 조건을 적용하거나 바꾼 자체 연습이다. 먼저 판단과 근거를 적고 풀이를 펼친다.
기존 회원의 주문 금액 99,990 원에 대한 결제 결과는 94,990 원이다. 구조를 바꾼 뒤 94,991 원이 되었다. 요구 변경은 승인되지 않았다. 개발자는 반올림이 더 옳다며 기대값을 94,991 원으로 바꾸고 리팩토링 완료로 보고했다.
- 이번 변화의 분류와 바로잡을 행동을 쓰라.
- 반올림 정책 변경이 나중에 승인된다면 구조 변경과 정책 변경을 어떤 근거로 나눌지 설명하라.
풀이와 오답 교정 보기
관찰 가능한 외부 행동이 승인 없이 달라진 변경이다. 기대값을 결과에 맞춰 바꾸어 차이를 숨기지 않고, 원래 조건과 계산 규칙을 대조하여 구조 변경이 만든 결함을 고친다. 이후 반올림 요구가 승인되면 정책 변경은 별도 요구와 사례 오라클로 검증한다. 새 정책이 좋다는 주장만으로 현재 단계의 행동 보존 실패가 사라지지 않는다.
기능이 비슷하다는 말로 외부 차이를 지웠다면 리팩토링 정의, 새 정책으로 실패를 덮었다면 동작 보존과 기능 변경의 분리를 다시 읽는다.
10. 자체 10점 문제
코드스멜과 리팩토링의 관계를 설명하고, 리팩토링 시 테스트 안전망과 작은 변경 단위가 필요한 이유를 설명하시오.
요구어는 설명이다. 코드스멜→안전망→작은 변경→검증 흐름을 잡는다.
10점 풀이와 답안 구성 보기
10점 답안 목차
- 코드스멜의 의미와 한계
- 리팩토링 정의·행위보존
- Characterization/회귀 안전망
- 작은 구조변경과 즉시 검증
- 기능변경·성능개선과 분리
- 점진 리팩토링과 재작성 차이
10점 답안 도식
코드스멜에서 현재 행위 안전망을 먼저 확보하고 작은 반복 그림의 구조변경·검증을 수행한다. 실패하면 원인을 확인하거나 되돌리고 안전망을 다시 확인한다.
10점 모범답안
코드스멜은 중복·긴 함수·전역상태처럼 구조적 조사 필요성을 알리는 신호이며 그 자체가 결함은 아니다. 리팩토링은 외부에서 관찰 가능한 행위를 의도적으로 바꾸지 않으면서 내부구조를 개선하는 변경이다. 먼저 핵심 입력·출력·상태·오류를 회귀시험이나 Characterization Test로 고정하고, 함수 추출·이름 변경·조건 분해처럼 한 의도를 가진 작은 변경을 수행한 뒤 즉시 테스트·정적검사·빌드를 실행한다. 작은 단계는 실패원인과 롤백 범위를 줄인다. 새 기능·버그수정·성능목표 변경은 별도 요구로 분리해야 행위변화와 구조변화를 구분할 수 있다. 대규모 재작성은 숨은 규칙 누락과 전환위험이 커 별도의 위험관리와 인수증거가 필요하다.
11. 자체 25점 문제
2,000줄 주문 모듈에 할인규칙 중복, 전역상태, 긴 조건문이 있고 변경 때마다 여러 파일이 함께 수정된다. 테스트는 정상 주문 몇 개뿐이다. 기능변경 없이 구조를 개선하기 위한 리팩토링 전략을 코드스멜 분석, 안전망, 단계적 변경, 의존성 분리, 통합검증, 재작성 비교 관점에서 설계하시오.
요구어는 설계다. 먼저 안전망을 만들고 변경을 잘게 나눠야 한다.
25점 풀이와 답안 구성 보기
25점 답안 목차
- 변경 빈도·결함·스멜 지도
- 공개계약과 현재 행위 오라클
- 경계값·오류 Characterization Test
- 할인규칙의 책임 경계와 작은 함수 추출
- 전역상태의 내부 의존성 경계
- 단계별 회귀·정적검사·빌드
- 통합계약·API·로그·성능 확인
- 실패 시 즉시 되돌림과 변경 분리
- 점진 리팩토링과 재작성 위험 비교
25점 도식 대조표
작은 반복 그림의 각 변경·검증을 아래 표의 실제 계약에 대조한다.
| 작은 변경 단계 | 각 단계에서 유지·검증할 것 |
|---|---|
| 스멜·변경 빈도를 조사하고 현재 행위를 고정 | 공개계약과 현재 행위 안전망 |
| 의미가 같은 중복의 책임을 모으기 | 단위·통합·계약 시험과 행위 보존 |
| 한 의도의 함수 추출 | 같은 입력·출력·오류와 즉시 검증 |
| 상태·의존성의 내부 경계 개선 | 공개 API·통합 계약과 기존 오라클 |
각 단계의 행위 보존을 확인한 뒤 다음 단계로 간다. 중복·함수·상태를 한 번에 바꾸거나 끝에서만 시험하지 않는다.
25점 모범답안
1. 범위와 스멜 우선순위. 최근 변경 빈도·결함이력으로 2,000줄 주문 모듈의 중복 할인규칙, 긴 조건문, 전역상태, 산탄총 수정을 먼저 지도화한다. 구조를 예쁘게 만드는 것이 목표가 아니라 기능변경 없이 다음 변경의 위험범위를 줄이는 것이므로, 공개 API·오류코드·금액계산·로그처럼 외부에서 관찰되는 계약을 변경금지 범위로 적는다.
2. 현재 행위 안전망. 정상 주문 몇 개만으로는 부족하므로 RF-5처럼 캠페인 ON에서 99999원 비회원 99999원, 100000원 비회원 90000원, 99990원 회원 94990원, 99999원 회원 94999원, 100000원 회원 88000원, 캠페인 OFF의 100000원 회원 95000원과 음수의 RangeError("amount")를 Characterization Test로 고정한다. 이 값은 현 정책의 정당성을 승인하는 것이 아니라 구조변경 전후 차이를 검출하는 오라클이다.
3. 작은 구조변경과 실패 판정. 첫 단계에서는 할인율 계산과 금액 계산을 추출하되 공개 payable(amount, isMember) 호출형태를 유지하고 즉시 회귀·정적검사·빌드를 실행한다. 추출 중 >= 100000을 > 100000으로 바꾸면 100000원 비회원이 90000원에서 100000원으로 변하므로 그 커밋은 실패로 보고 되돌린다. 12% 상한 누락은 100000원 회원의 88000원 오라클을 깨고, Math.floor를 Math.round로 바꾸면 99990원 회원이 94990원에서 94991원으로 변하므로 별도 기능변경 승인 없이 섞지 않는다.
4. 중복과 의존성 경계. 의미와 변경이유가 같은 할인규칙만 한 책임 경계로 모으고, 숫자가 우연히 같은 신규고객·장기회원 규칙은 자동 통합하지 않는다. 전역 캠페인 상태는 내부 calculatePayable(amount, isMember, campaignEnabled) 경계로 전달해 ON/OFF를 결정적으로 시험하되 외부 함수에 새 매개변수를 노출하지 않는다. 시간·DB·네트워크도 실제 대체 필요와 테스트 경계가 있는 곳에만 같은 원칙을 적용한다.
5. 계층별 검증과 완료조건. 각 작은 변경 뒤 단위 회귀뿐 아니라 주문상태·오류·외부 API 계약·로그·필요 성능 기준을 통합시험에서 확인한다. 현재 테스트가 관찰하지 않은 행위까지 모두 보존됐다고 과장하지 않고 변경영향 분석을 함께 수행한다. 실패원인이 한 커밋에 국한되도록 구조변경과 버그수정·정책변경·DB 스키마 변경을 분리하며, 핵심 안전망·정적검사·빌드·통합계약이 모두 유지될 때 다음 단계로 간다.
6. 재작성 비교와 판단. 대규모 재작성은 새 구조 설계의 자유도가 있지만 기존 코드에만 남아 있는 규칙을 다시 발견해야 한다. 예를 들어 실제 규칙 20개 중 명세에 18개만 있으면 새 구현의 18개 통과만으로 나머지 2개를 삭제할 수 없다. 현재 시스템을 작은 단계로 검증할 수 있다면 점진 리팩토링으로 위험을 좁히고, 재작성이 필요하면 기능목록·전환·병행비교·롤백을 별도 변경계획과 인수증거로 관리한다.
12. 내 리팩토링 답안의 오류를 직접 고친다
| 잘못 쓴 문장 | 왜 틀렸는가·수정 방향 | 돌아갈 본문 |
|---|---|---|
| 테스트 10개가 통과했으니 외부 행위 전체가 보존됐다 | 테스트는 관찰한 계약의 표본이다. 미관찰 API·로그·타이밍도 영향분석한다 | 테스트의 한계 |
| 99999원과 100001원을 시험했으니 100000원 경계는 볼 필요 없다 | 비교연산자 >=와 > 차이는 정확 경계에서 드러난다 | RF-5 |
| 현재 10%가 같으므로 신규고객과 장기회원 규칙은 하나로 합친다 | 숫자가 같아도 변경이유가 다르면 다른 책임일 수 있다 | 중복의 의미 |
추출하면서 >=를 >로 바꾸는 것은 읽기 쉬운 정리다 | 100000원 비회원 결과를 90000원에서 100000원으로 바꾸는 기능변경이다 | RF-5 실패 |
Math.floor를 Math.round로 바꿔도 계산식은 같으니 리팩토링이다 | 99990원 회원 결과가 94990원에서 94991원으로 변한다 | RF-5 오라클 |
공개 payable에 campaign 인자를 추가해도 내부구조만 바뀐 것이다 | 호출 시그니처가 소비자 계약이면 외부 행위 변경이다. 내부 경계를 먼저 만든다 | 의존성 경계 |
| 의존성 주입이 좋으니 모든 클래스에 인터페이스를 만든다 | 실제 변경축·대체·시험 필요가 없는 간접층은 복잡도만 늘릴 수 있다 | 추상화 한계 |
| 재작성 명세 18개가 통과했으니 기존 20개 규칙 중 나머지 2개는 삭제해도 된다 | 숨은 행위의 필요 여부를 별도 판단해야 한다 | 재작성 누락 |
자기점검에서는 경계값, 오류계약, 공개 API, 반올림·정렬 같은 세부 행위, 중복의 의미, 단계별 롤백을 확인한다. “코드가 더 깨끗해졌다”는 설명만 있고 어떤 입력에서 무엇이 같아야 하는지 없다면 RF-5로 돌아가 안전망을 먼저 적는다.
조건을 바꾸면 구조변경과 기능변경을 다시 분리한다
연습 1 — 캠페인 기준을 100000원에서 120000원으로 바꾸라는 새 정책이 승인됐다면? 기존 100000원→90000원 Characterization Test를 조용히 100000원→100000원으로 고쳐 “리팩토링 통과”라고 하지 않는다. 먼저 구조변경을 기존 오라클로 끝낸 뒤, 정책변경을 별도 요구·테스트 변경으로 수행하여 무엇 때문에 출력이 달라졌는지 분리한다.
연습 2 — 금액처리를 버림에서 반올림으로 바꾸라는 요구가 생겼다면? 99990원 회원의 94990원→94991원 변화는 이제 의도된 기능변경이므로 별도 변경으로 승인하고 새 오라클을 만든다. 요구가 바뀌었다는 이유로 이전 리팩토링 단계에서 발생한 동일 차이까지 소급해 정당화하지 않는다.
연습 3 — 공개 API 자체를 새 버전에서 바꿔 campaign 상태를 호출자가 전달하게 한다면? 기존 payable(amount, isMember)를 유지하는 리팩토링과 새 버전 계약 payable(amount, isMember, campaignEnabled)의 도입을 한 커밋으로 숨기지 않는다. 구버전 호환층을 새로 만들라는 뜻이 아니라, 현재 요구가 새 API라면 그 계약변경을 명시하고 소비자·통합시험·전환 범위를 별도 검증한다.