한 페이지 리팩터 맵: 무서운 레거시 코드를 안전하게 갈라 나가는 지도
빅뱅식 전면 재작성이나 끝이 안 보이는 정리 프로젝트 없이도, 실제 비즈니스 가치를 내면서 레거시 코드를 안전하게 개선할 수 있게 해주는 ‘한 페이지 리팩터 맵’을 설계하는 방법.
한 페이지 리팩터 맵: 무서운 레거시 코드를 안전하게 갈라 나가는 지도
성숙한 코드베이스에서 일하다 보면, 새 기능을 넣기 전에 “일단 좀 정리부터 하자”는 충동이 들 때가 많습니다. 이해할 만한 마음이지만, 동시에 위험한 생각이기도 합니다.
레거시 코드가 곧바로 나쁜 코드를 뜻하는 건 아닙니다. 레거시 코드는 살아남은 코드입니다. 중요한 워크플로를 돌리고, 도메인 지식을 품고 있으며, 조용히 수년간 돈을 벌어왔습니다. 단지 “지저분해 보여서” 가볍게 혹은 공격적으로 리팩터링을 하면, 보상이 보장되지 않은 위험만 떠안게 됩니다.
여기서 **한 페이지 리팩터 맵(one-page refactor map)**이 등장합니다.
리팩터 맵은 다음 세 가지 질문에 간결하게 답하는 계획입니다.
- 지금, 왜 이 레거시 코드를 건드리는가?
- 바꾸는 동안 사용자를 어떻게 안전하게 지킬 것인가?
- 오늘의 난장판에서 내일의 구조로 어떤 작은 단계들을 거쳐 옮겨 갈 것인가?
이 글에서는 기능 개발을 탈선시키지 않으면서, 무서운 레거시 코드를 통과하는 안전한 경로를 그리기 위해 한 페이지 리팩터 맵을 만들고 활용하는 방법을 살펴보겠습니다.
규칙 #1: “걍 보기 싫어서” 레거시 코드를 리팩터하지 말 것
첫 번째 원칙은 단순합니다.
실제로 그 레거시 코드를 건드릴 필요가 없다면, 리팩터링하지 마라.
안정적으로 돌아가던 레거시 영역을 변경하면 항상 위험이 따라옵니다. 장애, 미묘한 회귀(regression), 성능 저하, 디버깅이 어려운 엣지 케이스 등등이 생길 수 있습니다. 사용자가 만족하고 있고, 당장 손댈 필요도 없는 코드라면, 보기 좋게 만드는 것만으로 자동으로 얻는 이득은 없습니다.
리팩터링이 정당화되는 경우는 다음과 같습니다.
- 해당 영역에 기능을 추가하거나 변경해야 할 때
- 그 부분의 버그나 반복적으로 발생하는 문제들을 수정해야 할 때
- 실질적인 기술적 이슈(확장성, 보안, 성능, 규제/컴플라이언스 등)를 해결해야 할 때
리팩터 맵은 여기서 시작합니다. “변경하기로 한 결정”에서 출발하는 겁니다. 가까운 시일 내에 이 코드를 실제로 수정할 필요가 없다면, 가장 안전하고(그리고 종종 가장 현명한) 선택은 그냥 놔두는 것입니다.
1단계: 구체적인 이유를 먼저 분명히 하기
레거시 코드를 한 줄이라도 건드리기 전에, 리팩터링의 구체적인 비즈니스 목표나 기술적 목표를 글로 적으세요. 다음과 같은 애매한 동기는 피합니다.
- “이 모듈 진짜 엉망이야.”
- “이건 좀 현대화해야 해.”
- “그냥 깔끔하게 만들고 싶어서.”
대신, 이렇게 정의합니다.
- 비즈니스 목표 예시: “결제 게이트웨이 연동 문제로 인한 체크아웃 실패율을 50% 줄인다.”
- 기술적 목표 예시: “새 결제 제공업체를 추가하는 데 걸리는 시간을 2주에서 2일 이내로 줄인다.”
한 페이지 맵에 Purpose(목적) 섹션을 짧게 만듭니다.
Purpose: 결제 연동 코드를 리팩터링하여, 이번 분기 안에 결제 제공업체 X를 추가하고, 체크아웃 실패율을 Y% 감소시킨다.
이렇게 하면 다음이 가능해집니다.
- 우선순위를 세울 수 있습니다. (정말 목표를 뒷받침하는 일인가?)
- 성공을 측정할 수 있습니다. (목표 수치를 달성했는가?)
- 대화를 현실에 붙들어 둘 수 있습니다. (“싹 다 치우는 게 아니라, 이 목표를 달성하는 데 필요한 만큼만 정리하는 겁니다.”)
2단계: 먼저 안전망부터 구축하기
레거시 시스템에는 탄탄한 테스트가 부족한 경우가 많습니다. 이런 시스템을 보호 장치 없이 바꾸는 건, 모니터링 장비 없이 수술을 집도하는 것과 같습니다.
큰 리팩터링이나 위험한 리팩터링을 하기 전, 실제로 작성해야 할 첫 번째 코드는 회귀(regression) 방지를 위한 안전망입니다.
- 이번 변경으로 영향을 줄 수 있는 핵심 플로우(로그인, 결제, 청구 등)를 식별합니다.
- “원래 이랬으면 좋겠는 동작”이 아니라, 현재 동작을 그대로 캡처하는 테스트를 추가합니다.
- 비즈니스에 가장 중요하고, 위험도가 높은 경로부터 우선적으로 커버합니다.
이 테스트들은 조금 어색하거나 부자연스러워 보일 수 있습니다. 그래도 목적은 단 하나입니다.
중요한 무언가를 실수로 깨뜨렸을 때, 그 사실을 테스트가 바로 소리쳐 알려주게 하는 것.
맵에 Safety Net(안전망) 섹션을 추가합니다.
- 보호해야 할 핵심 플로우
- 주로 사용할 테스트 유형 (단위 테스트, 통합 테스트, 시스템 테스트, 컨트랙트 테스트 등)
- 배포 및 롤아웃 중에 지켜볼 모니터링/알림 항목
완벽한 커버리지가 필요하지는 않습니다. 정말 중요한 곳에, 충분한 수준으로 커버리지가 있으면 됩니다.
3단계: 현재 동작을 기록하는 Characterization 테스트 활용하기
레거시 시스템의 기존 테스트는 대개 다음과 같습니다.
- 아예 없거나
- 불완전하거나
- 오래되어 현재 코드와 맞지 않거나
- 해피 패스(happy path)에만 초점이 맞춰져 있거나
이건 당연하게 받아들이고, 그에 맞춰 계획을 세워야 합니다.
이 상황을 다루기 위해 Characterization Test를 만듭니다. Characterization 테스트는 “옳은 동작”을 정의하려 하기보다, 현재 시스템이 실제로 어떻게 동작하는지를 그대로 문서화하는 테스트입니다. 그 동작이 놀랍거나, 도메인 상으로 틀려 보이더라도 말이죠.
예를 들어, 어떤 할인 로직이 역사적인 이유로 세금 계산 이후에 적용된다는 사실을 발견했다고 해봅시다. 도메인 관점에서는 잘못된 것일 수 있지만, 동시에 다음을 의미할 수 있습니다.
- 다른 시스템들이 이 동작에 의존하고 있을 수 있고
- 계약이나 약관에 그 동작이 녹아 있을 수 있고
- 오랫동안 사용해온 고객들이 그 결과를 당연하게 여길 수 있습니다.
Characterization 테스트는 이렇게 명시합니다.
X, Y, Z 조건이 주어지면, 현재(today) 이 정확한 금액을 청구한다.
지금의 목표는 도메인 상의 문제를 바로잡는 것이 아니라, 의도치 않은 변경을 피하는 것입니다.
리팩터 맵의 해당 섹션에는 다음을 적습니다.
- Characterization이 필요한 핵심 동작들 (예: 할인 규칙, 반올림 규칙, 재시도 로직 등)
- 테스트로 고정해 두어야 할 이상한 엣지 케이스들
나중에 비즈니스에서 “이제 저 동작을 바로잡자”고 결정하면, 그때는 코드와 테스트를 함께 의도적으로 변경하면 됩니다.
4단계: 작고 되돌릴 수 있는 단계로 리팩터하기
대규모 일괄 리팩터링은 효율적으로 들리고, 심리적으로도 통쾌하게 느껴지지만, 실제로는 매우 깨지기 쉬운 방식입니다. 뭔가 잘못되면, 500개의 변경 중 무엇 때문에 문제가 생겼는지 알기 어렵습니다.
대신, 리팩터 맵은 다음과 같은 작고 점진적인 단계를 중심으로 설계합니다.
- 몇 시간에서 길어야 이틀 정도면 끝날 수 있고
- 코드 리뷰와 테스트가 쉬우며
- 각 단계가 독립적으로 배포 가능하고, 되돌리기도 쉬운 변화들
도움이 되는 패턴들을 몇 가지 예로 들면:
- Strangler Fig 패턴: 기존 동작을 감싸는 래퍼를 두고, 트래픽을 점진적으로 새 컴포넌트로 옮겨감.
- Branch by Abstraction: 인터페이스나 어댑터를 도입하고, 호출 측을 조금씩 새 구현으로 이관.
- 작은 이름 변경과 추출: 함수 추출, 클래스 이동, 메서드 이름 변경 등, 한 번에 하나의 초점을 가진 변경만 수행.
맵에서 Steps(단계) 섹션은 다음처럼 보일 수 있습니다.
- 현재 결제 플로우에 대한 Characterization 테스트 추가
PaymentProvider인터페이스 도입, 기존 구현을 이 인터페이스로 감싸기- 제공업체별 로직을 개별 클래스들로 분리
- 새 결제 제공업체를 인터페이스를 통해 추가
- 이전 인라인 구현에서 쓰이지 않는 코드를 정리하고 제거
각 단계는 다음을 가져야 합니다.
- 명확한 “완료” 기준
- 롤백 전략(코드 되돌리기, 기능 토글, 트래픽 우회 등)
5단계: 진짜 “지도” 그리기 – 경계, 위험, 순서
리팩터 맵은 한 페이지에 담기는 시각적 + 텍스트 요약본입니다. 예쁘게 만들 필요는 없고, 대신 명료하고 공유 가능하면 됩니다.
맵에는 다음이 들어가야 합니다.
1. Boundaries(경계)
- 어떤 모듈이나 컴포넌트가 범위 안에 들어오는가?
- 어떤 외부 시스템이나 팀에 영향을 줄 수 있는가?
- 이번에 의도적으로 건드리지 않을 영역은 어디인가?
2. Risky Areas(위험 구역)
- 트래픽이 많이 지나는 핫 패스
- 돈, 규제/컴플라이언스, 보안과 엮인 코드
- 버그가 자주 났거나, 플래키(flaky) 하기로 악명 높은 영역
3. Sequence of Changes(변경 순서)
- 작은 리팩터 단계들의 번호 매겨진 목록
- 단계 간 선후 관계 (무엇이 무엇보다 먼저 되어야 하는지)
- 몇 주간 멈춰도 “수술 도중” 상태가 아닌, 안정적인 중간 중간 마일스톤
좋은 기준 하나:
어느 시점에서든, 지금 진행 중인 단계를 마친 뒤 멈추더라도, 시스템은 안정적인 상태여야 한다.
현실 세계에서는 우선순위가 바뀌고, 긴급한 일이 터지고, 로드맵이 수시로 조정됩니다. 이런 상황에서도 리팩터링이 “살아남으려면” 이 기준이 중요합니다.
6단계: 리팩터링을 일회성 프로젝트가 아니라, 상시적인 활동으로 다루기
조직에서는 종종 리팩터링을 “이번에 한 번 크게 하는 프로젝트”처럼 이야기합니다. 스프린트 한두 번이면 끝내고, 그다음엔 “정리 완료”가 될 것처럼 말이죠. 현실은 거의 그렇지 않습니다.
레거시 시스템이 지금 모습에 이른 건, 수년간의 압박 속 변경들이 쌓인 결과입니다. 이를 단기간의 영웅적인 한 번의 푸시로 바로잡으려 하면, 엄청난 위험과 비용을 감수해야 합니다.
대신, 대규모 리팩터링은 일상 업무와 통합된, 장기적이고 반복적인 활동으로 다뤄야 합니다.
- 리팩터 단계들을 기능 개발 티켓 안에 자연스럽게 녹여 넣습니다.
- 리팩터링 마일스톤을 비즈니스 마일스톤(새 제공업체 론칭, 새 국가 진출, 새 SLA 도입 등)과 정렬합니다.
- 진행하면서 테스트 안전망을 계속 강화하고 확장합니다.
이렇게 하면 리팩터 맵은 살아 있는 아티팩트가 됩니다.
- 시스템을 이해할수록 내용을 업데이트하고
- 우선순위가 바뀌면 계획을 조정하며
- 신규 팀원이 온보딩할 때 참고 자료로 재사용할 수 있습니다.
맵은 첫날부터 완벽할 필요가 없습니다. 당장 다음 몇 걸음을 안전하게 안내할 만큼만 명확하면 충분합니다.
한눈에 보는 한 페이지 템플릿
아래는 바로 가져다 쓸 수 있는, 가벼운 템플릿 예시입니다.
Title: Refactor Map – [영역/모듈 이름]
Purpose (Why)
- 비즈니스/기술적 목표:
- 성공 지표:
Scope & Boundaries
- In scope:
- Out of scope:
- External dependencies:
Safety Net
- 활용할 기존 테스트:
- 새로 추가할 테스트(Characterization + 핵심 플로우):
- 모니터링/알림 항목:
Risks & Constraints
- 고위험 영역:
- (당분간) 유지해야 할 이상한 현재 동작들:
- 규제/보안/성능 관련 제약:
Incremental Steps (How)
- …
- …
- …
각 단계는 다음을 만족해야 합니다.
- 그 자체로 가치가 있거나, 다음 단계를 준비하는 의미가 있어야 하고
- 명확한 시작/종료(완료) 기준이 있어야 하며
- 배포 가능하고, 가능하다면 되돌리기도 쉬워야 합니다.
마무리: 더 안전한 변경, 더 적은 드라마
레거시 코드 자체가 적은 아닙니다. 계획 없는, 범위 없는 변경이 문제일 뿐입니다.
다음과 같이 하면:
- 실제로 필요할 때만 리팩터링하고
- 그 이유를 명시적이고 측정 가능하게 만들고
- 회귀/Characterization 테스트로 안전망을 구축하고
- 작고 되돌릴 수 있는 단계로 움직이고
- 모두가 공유할 수 있는 간단한 한 페이지 리팩터 맵을 사용하며
- 리팩터링을 일회성 이벤트가 아닌, 꾸준한 습관으로 운영하면
…아무리 무서운 시스템이라도, 매 릴리스마다 회사를 건 도박을 하지 않고도 충분히 개선할 수 있습니다.
다음에 “그냥 싹 정리하고 싶다”는 생각이 들면, 잠깐 멈추세요. 한 장의 종이를 집어 들고, 지도를 그리세요. 그리고 그 지도를 따라 한 걸음씩, 안전하게 나아가면 됩니다.