동작은 그대로 두고 내부 구조만 개선하는 작업. 면접에서는 왜 지금·어떻게 안전하게·무엇으로 증명 세 가지를 본다.
실제 사례 — 12년 된 메시징 서버
-
문제 — 상태를 담는 Context 클래스가 10개 넘게 얽혀 전역 변수처럼 쓰였다
- 어딘가에서 A가 set 하고 다른 곳에서 B가 get 한다
- 수정 시 사용처 추적 불가, 장애 시 디버깅 불가
-
결과 — Context 10개 → 3개, 수백 라인 삭제, 기능은 동일
- 순환 의존성 제거, 성능 저하 없이 라이브 반영
기법 1 — 파라미터로 받아 set 하지 말고 반환하라
// ✗ 함수가 바깥 객체 상태를 직접 바꾼다
Result createCar(CarContext ctx, Something a) { ctx.setCar(new Car()); return r; }
// ✓ 만든 값을 돌려준다 — 호출부가 무엇이 바뀌는지 안다
Pair<Result, Car> createCar(Something a) { return new Pair<>(r, new Car()); }
기법 2 — 필요한 것만 명시적으로 받아라
// ✗ 시그니처만 봐선 무엇을 읽고 무엇을 바꾸는지 모른다
void process(CarContext ctx, Something a) { ... }
// ✓ 의도가 드러난다
void process(List<People> passengers, SomeValue v, Something a) { ... }
파라미터가 늘어 불편하면 함수를 더 작은 책임으로 쪼갤 신호로 읽는다.
기법 3 — 순환 의존성을 고차 함수로 끊는다
// Before — ServiceA 가 ServiceB 를 필드로 들고 있다
@Service class ServiceA { @Resource ServiceB b;
void methodA(Integer p) { b.methodB(p); } }
// After — 의존을 '함수' 로 낮춘다. ServiceA 는 ServiceB 를 모른다
@Service class ServiceA {
void methodA(Integer p, Function<Integer,Integer> methodB) { methodB.apply(p); } }
// 의존은 상위로 올리고 생성자 주입으로 조립한다
@Component class Handler {
Handler(ServiceA a, ServiceB b) { ... }
void execute() { a.methodA(2, b::methodB); } }
클래스 인터페이스보다 함수형 인터페이스가 더 작고 약한 결합이다.
안전하게 하는 법
-
① 특성화 테스트(characterization test) 먼저
- 바꾸기 전에 '현재 동작' 을 고정하는 테스트를 만든다
- 없으면 "동일하게 유지했다" 를 증명할 수 없다
-
② 점진적으로
- Context 를 하나씩 줄이고 단계마다 기능 동일성 확인
- 빅뱅 재작성은 되돌릴 수도 검증할 수도 없다
-
③ 수치로 증명
- 복잡도 · 의존성 · 결함률 · 변경 소요 시간 중 하나라도
면접 함정
- ❌ "전면 재작성했습니다" → 위험 신호로 읽힌다. 점진적 접근과 안전망을 말하라.
- ❌ "깔끔해졌습니다" → 근거가 아니다. 수치를 대라.
- ❌ 테스트 없이 시작 → 동작 보존을 증명할 방법이 사라진다.
언제 하지 말아야 하나
-
곧 폐기될 코드 — 없앨 것을 다듬을 이유가 없다
-
아무도 안 건드리는 코드 — 변경 비용이 발생하지 않으면 부채가 아니다
-
이해하지 못한 코드 — 먼저 읽고 테스트로 고정한 다음이다
-
릴리스 직전 — 검증 시간이 없다
-
"더러워서" 는 이유가 아니다. "변경 비용이 실제로 발생하고 있어서" 가 이유다
설득하는 법
❌ "코드가 지저분합니다"
- **✓ — "이 모듈은 지난 분기 결함의 40% 가 나왔고, 기능 하나 추가에**
- 평균 5일이 걸립니다. 다른 모듈은 1.5일입니다"
비용을 숫자로 보이면 리팩터링이 '취향' 이 아니라 '투자' 가 된다
기능 개발과 섞지 않는다
-
같은 커밋에 리팩터링과 기능 변경을 섞으면
- 리뷰어가 무엇이 동작 변경인지 구분할 수 없다
- 장애가 났을 때 어느 쪽이 원인인지 모른다
- 롤백하면 기능도 함께 사라진다
-
커밋(가능하면 PR)을 분리한다. '구조만 바꾸는 커밋' 과 '동작을 바꾸는 커밋'
보이스카우트 규칙
- 대규모 리팩터링 프로젝트를 따로 세우기 어려운 조직이 많다
- 그 코드를 건드릴 때마다 조금씩 낫게 만든다
다만 '조금' 의 범위를 정해 둔다 — 그러지 않으면 작은 수정이 거대한 PR 이 되어 리뷰가 막힌다