레거시 코드 한 줄 고치기 전 — 5분 점검
수정 전 5분 체크로 레거시 사고를 막는 실전 절차
레거시 코드를 열었을 때 가장 먼저 드는 생각은 “한 줄만 바꾸면 되겠다”다. 그 한 줄이 3시간짜리 장애로 이어진 걸 나는 최소 두 번 경험했다.
먼저 — 이 파일이 어디에 쓰이는지 확인한다
막상 열어보면 파일 이름과 실제 호출처가 다른 경우가 반이다. 올해 초 payment_utils.py 를 고쳤는데, 이름만 보고 결제 모듈 전용인 줄 알았다. 실제로는 정산 배치 세 개가 동일하게 import 하고 있었다. grep 한 번으로 잡힐 문제였는데 그냥 넘겼다.
- 호출처 확인은
grep -rn '파일명\|함수명' ./로 시작한다. IDE 의 Find Usages 가 편하지만, 동적 import 나 문자열 참조는 놓치기 쉽다. 실제로importlib.import_module(module_name)패턴을 쓰는 코드에서 IDE 가 0건을 반환했고, grep 으로 다시 뒤지자 4군데가 더 나왔다. - 마지막 커밋 날짜와 작성자를 본다.
git log -5 --follow 파일명으로 최근 변경 이력을 훑는다. 3년 넘게 아무도 안 건드린 파일이라면 그만한 이유가 있거나, 아무도 겁나서 못 건드린 거다. 둘 다 위험 신호다. - 함수 시그니처가 외부 API 와 맞닿아 있는지도 본다. 내부 헬퍼인 줄 알았는데 파트너사 webhook 이 직접 호출하는 엔드포인트였던 적이 있다. 그날 배포 후 파트너사로부터 슬랙 DM 이 10분 안에 왔다.
테스트가 있는지, 실제로 돌아가는지 확인한다
테스트 파일이 존재한다고 안심하면 안 된다. 존재하는 것과 통과하는 것은 다르고, 통과하는 것과 실제 케이스를 커버하는 것도 다르다.
pytest --collect-only로 먼저 수집 단계에서 오류가 없는지 본다. 의존 패키지가 삭제되거나 환경 변수가 빠진 채로 오래된 테스트는 import 오류로 수집조차 안 된다. 고치기 전에 테스트가 이미 깨져 있으면, 내가 고친 다음에 깨진 건지 원래 깨진 건지 구분이 안 된다.- 커버리지를 숫자로 본다. 라인 커버리지 80% 라도 내가 수정할 분기가 커버되는지는 별개다.
pytest --cov=모듈명 --cov-report=term-missing으로 missed 라인 번호를 직접 확인한다. 최근 수정 작업에서 전체 커버리지는 76% 였는데 내가 바꾼 else 분기는 미스였다. 테스트 하나 추가하고 나서야 실제 버그가 잡혔다. - 테스트가 아예 없으면, 수정 전에 현재 동작을 characterization test 로 먼저 박아둔다. 이름이 거창하지만 실제로는 “지금 입력 X 를 넣으면 Y 가 나온다” 를 단순 assert 로 고정하는 것. 나중에 의도치 않은 동작 변경이 생기면 이게 잡아준다.
사이드이펙트 범위를 머릿속에 그린다
함수 하나가 DB 를 쓰는지, 외부 API 를 호출하는지, 파일을 쓰는지. 이 세 가지 중 하나라도 해당하면 “한 줄 수정” 이 아니다.
- DB write 가 있으면 트랜잭션 경계를 확인한다. 올해 중순 작업에서
update_status()한 줄 수정이 상위 호출 스택에 걸린 트랜잭션과 충돌해서 일부 레코드가 롤백됐다. 에러 로그도 안 났고 슬랙 알림도 없었다. 이틀 뒤 데이터 정합성 점검에서 발견됐다. - 외부 API 호출이 있으면 재시도 로직과 타임아웃을 체크한다. 기존 코드에 타임아웃이 없으면 내가 고친 후에도 그대로 남겨두지 말고 같이 박는다. 레거시는 고치는 김에 최소한 타임아웃 하나씩은 넣고 나오는 게 원칙이 됐다.
- 파일 I/O 는 경로 하드코딩 여부를 본다.
/tmp/report.csv처럼 절대경로로 박힌 게 있으면 여러 프로세스가 동시에 쓸 때 덮어쓰기가 생긴다. 배치가 병렬로 두 개 뜨는 환경에서 이게 한 번 터졌다.
변경 범위를 최소화한다
레거시 코드를 고칠 때 가장 흔한 실수는 “어차피 건드리는 김에” 리팩터링까지 같이 하는 것이다. 나도 매번 유혹을 받는다.
- 수정 목적 한 줄을 PR description 첫 줄에 먼저 적는다. “왜 바꾸는가” 를 먼저 써두면 그 목적에서 벗어나는 변경이 생겼을 때 스스로 멈추게 된다. 막상 해보면 이게 생각보다 잘 작동한다.
- diff 를 커밋 전에 한 번 더 본다. 수정 의도와 무관한 공백 변경, 주석 삭제, import 정렬이 섞여 있으면 리뷰어가 실제 변경을 찾기 어렵다. 지난 분기에 리뷰어가 “변경 사항이 뭔지 모르겠다” 고 한 PR 이 있었는데, 열어보니 실제 수정 2줄 옆에 자동 포매터가 파일 전체를 건드린 거였다.
- 리팩터링이 필요하면 별도 커밋 혹은 별도 PR 로 분리한다. 기능 변경과 구조 변경을 한 커밋에 섞으면 나중에 git bisect 로 버그를 추적할 때 지옥이 된다.
롤백 계획을 30초만 생각한다
배포 전에 “이게 틀렸을 때 어떻게 되돌리나” 를 딱 30초만 생각한다. 길게 할 필요 없다. 그냥 머릿속에 한 번 그려보는 것.
- DB 스키마 변경이 없으면 이전 커밋으로 revert 하나면 된다. 배포 파이프라인에서 이전 이미지 태그를 바로 올릴 수 있는지 확인해두면 충분하다.
- 스키마 변경이 있으면 롤백 migration 을 미리 작성해둔다. 올해 배포 중 컬럼 추가 작업에서 애플리케이션 에러가 났는데, 롤백 migration 을 안 만들어둬서 수동으로 SQL 을 치는 데 7분이 걸렸다. 그 7분이 꽤 길었다.
- feature flag 가 있으면 배포 전에 OFF 상태로 머지한다. 트래픽을 조금씩 흘려보다가 이상하면 flag 만 꺼버리면 된다. 레거시 수정일수록 이 방식이 심리적으로 안전하다.
다음 한 가지
다음 번 레거시 수정 PR 을 열기 전에 — 이 5분 체크리스트를 README 에 한 줄짜리 링크로 박아둘 것.
🛒 이 글과 어울리는 추천 상품
위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.