레거시 코드 현대화 전략
동작하는 레거시 시스템을 멈추지 않고 현대화하는 방법을 다룹니다. 특성화 테스트로 안전망을 먼저 만들고, 스트랭글러 패턴으로 파사드를 두어 레거시를 조금씩 신규 코드로 대체하는 점진적 현대화 전략을 실전 코드와 함께 정리합니다.
지난 글에서 폐기·제거된 API를 무엇으로 대체할지 정리했다. 그런데 현실의 레거시 현대화는 “이 API를 저 API로 바꾸면 끝”처럼 깔끔하지 않다. 수십만 줄의 코드가 매출을 만들며 돌아가고 있고, 테스트는 부실하며, 원래 설계자는 이미 회사를 떠났다. 이번 글에서는 이렇게 동작하는 시스템을 멈추지 않고 현대화하는 전략을 다룬다. 핵심은 두 가지다. 바꾸기 전에 안전망을 만들 것, 그리고 한 번에 다 바꾸려 하지 말 것.
레거시란 “테스트 없는 코드”다
레거시의 정의는 사람마다 다르지만, 실무에서 가장 쓸모 있는 정의는 마이클 페더스의 것이다. 테스트가 없는 코드가 곧 레거시다. 오래됐든 새것이든, 손댈 때 무엇이 깨지는지 알 수 없다면 그것은 이미 레거시다. 이 정의가 유용한 이유는 처방을 곧바로 알려주기 때문이다. 현대화의 첫걸음은 프레임워크 교체가 아니라 테스트로 현재 동작을 붙잡는 일 이다.
여기서 함정이 하나 있다. 레거시 코드의 동작이 “옳은지”는 아무도 모른다. 그러니 옳고 그름을 따지지 말고, 지금 이렇게 동작한다는 사실 자체를 고정 해야 한다. 이것을 특성화 테스트(characterization test)라고 부른다.
안전망: 특성화 테스트
특성화 테스트는 코드의 의도를 검증하는 일반 테스트와 다르다. 코드가 지금 무엇을 반환하는지 관찰해서, 그 값을 그대로 기대값으로 못박는다. 값이 이상해 보여도 상관없다. 목표는 “리팩터링 후에도 이 동작이 유지되는가”를 감지하는 것이지, 동작이 옳은지 판단하는 것이 아니다.
// 값을 이해하지 못해도 현재 출력을 그대로 못박는다
@Test
void captureLegacyOutput() {
var out = legacy.calc("A", 3, true);
// 처음엔 아무 값이나 넣고 실행해 실제 출력을 확인한 뒤,
// 그 값을 기대값으로 고정한다
assertThat(out).isEqualTo("A-3-DISCOUNTED");
}
이렇게 주요 경로들을 특성화 테스트로 감싸두면, 이후 내부 구조를 아무리 바꿔도 겉보기 동작이 달라지는 순간 테스트가 빨갛게 터진다. 리팩터링의 그물망이 생기는 것이다. 테스트하기 어려운 곳(정적 메서드, 숨은 의존성)은 이음새(seam) 를 찾아 의존성을 주입 가능하게 살짝 열어주는 최소 변경부터 시작한다.
점진적 교체: 스트랭글러 패턴
안전망이 생겼다면 이제 바꿀 차례다. 하지만 전체를 새로 짜서 한 번에 교체(big bang rewrite)하는 방식은 거의 항상 실패한다. 재작성 기간 동안 레거시에는 계속 새 요구사항이 쌓이고, 신규 시스템은 영영 따라잡지 못한 채 두 시스템을 동시에 유지하는 지옥이 열린다.
대안은 마틴 파울러가 정리한 스트랭글러 패턴(strangler fig) 이다. 열대 나무를 감고 자라나 결국 원래 나무를 대체하는 교살무화과에서 이름을 땄다. 레거시 앞에 파사드(라우터)를 하나 두고, 요청을 기능 단위로 신규 구현으로 조금씩 돌린다. 신규가 검증되면 그 경로를 신규로 확정하고, 이 과정을 반복하며 레거시를 서서히 말려 죽인다.
// 파사드가 기능 단위로 신규/레거시를 분기한다
public Receipt checkout(Cart cart) {
if (features.isEnabled("new-checkout")) {
return newCheckout.process(cart); // 신규 경로
}
return legacyCheckout.process(cart); // 아직 남은 레거시
}
핵심은 파사드와 피처 플래그 의 조합이다. 신규 경로를 배포하되 플래그로 꺼둔 채 내보내고, 일부 트래픽에만 켜서(카나리) 지표를 비교한다. 문제가 생기면 코드 롤백 없이 플래그만 내리면 즉시 레거시로 복귀한다. 이 즉각적인 후퇴 경로가 있기에, 큰 리스크 없이 대담한 교체를 이어갈 수 있다.
현대화의 순서
무엇부터 손댈지도 전략이다. 모든 곳을 동시에 개선할 수는 없으니, 다음 기준으로 우선순위를 정한다.
| 우선순위 | 대상 | 이유 |
|---|---|---|
| 높음 | 자주 바뀌면서 버그가 잦은 모듈 | 개선의 투자 대비 효과가 가장 큼 |
| 높음 | 보안·컴플라이언스 위험 코드 | 방치 비용이 시간이 지날수록 급증 |
| 중간 | 새 기능이 자주 붙는 경계 | 스트랭글러 파사드를 두기 좋은 지점 |
| 낮음 | 안정적이고 거의 안 바뀌는 코드 | ”돌아가면 건드리지 말라” — 굳이 위험 감수 안 함 |
마지막 원칙이 중요하다. 현대화의 목표는 코드를 예쁘게 만드는 것이 아니라 변경 비용을 낮추는 것 이다. 몇 년째 조용히 잘 도는 코드는 아름답지 않아도 건드릴 이유가 없다.
정리
- 레거시의 실용적 정의는 테스트 없는 코드 이며, 현대화의 시작은 프레임워크 교체가 아니라 테스트로 현재 동작을 고정하는 일이다.
- 특성화 테스트 로 안전망을 먼저 친다. 동작의 옳고 그름이 아니라 “현재 이렇게 동작함”을 그대로 못박는다.
- 전면 재작성 대신 스트랭글러 패턴 으로 파사드를 두고 기능 단위로 점진 교체한다. 피처 플래그 가 즉각적인 후퇴 경로를 제공한다.
- 우선순위는 “자주 바뀌고 위험한 곳”부터, “조용히 잘 도는 곳”은 그대로 둔다.
다음 글에서는 이렇게 새로 쓰거나 정리한 코드가 팀 안에서 일관되게 읽히도록 만드는 코딩 컨벤션 을 다룬다. 안전망과 점진 교체가 개별 변경의 규율이라면, 컨벤션은 코드베이스 전체의 규율이다.
지난 글: 제거·폐기된 API 정리
다음 글: 자바 코딩 컨벤션
읽어주셔서 감사합니다. 😊