아날로그 리팩터링 기차 세트: 안전하고 순차적인 코드 변경을 위한 종이 레일 깔기
아이들이 종이 기차 레일을 하나씩 깔아가듯, 리팩터링을 작고 순차적인 변경의 연속으로 바라보면서 개발 과정 속에 자연스럽고 저위험으로 녹여 넣는 방법에 대해 다룹니다.
아날로그 리팩터링 기차 세트: 안전하고 순차적인 코드 변경을 위한 종이 레일 깔기
어렸을 때 나무 기차나 아날로그 기차 세트로 놀아본 기억이 있다면, 아마도 레일을 어떻게 깔았는지 떠올릴 수 있을 겁니다.
바닥에 박스를 한 번에 다 쏟아붓고, 단 한 번의 완벽한 시도로 거대한 철도망을 만들지는 않았을 겁니다.
항상 레일 하나에서 시작했죠. 그다음에 또 하나. 곡선을 깔아봤다가 안 맞는 걸 보고, 분기점을 조정하고, 다리를 하나 얹었습니다. 그렇게 조금씩 바닥의 어질러진 조각들이 모여 하나의 작은 철도 네트워크가 되었죠.
리팩터링도 이와 아주 비슷합니다. 적어도, 그래야 합니다.
하지만 현실에서 팀들은 리팩터링을 종종 극적이고 고위험한 이벤트로 취급합니다. 예를 들어, “3개월 동안 전부 다시 쓸 거야” 같은 식의 대규모 이니셔티브 말이죠. 이런 계획은 깔끔한 새 시스템을 약속하지만, 실제로는 번아웃, 일정 지연, 절반만 완성된 브랜치들을 남기기 쉽습니다.
대신 리팩터링을 종이 기차 레일을 까는 일처럼 생각해 봅시다. 작고, 되돌릴 수 있고, 눈에 보이는 명확한 단계들을 통해 코드베이스의 한 안전한 상태에서 다음 안전한 상태로 연결해 나가는 작업 말입니다.
이 글에서는 다음 내용을 다룹니다.
- 리팩터링을 지속적인 일상적인 습관으로 다루는 법
- 실제로 리팩터링이 일어나도록 시간을 명시적으로 배분하는 법
- 리팩터링을 작고 안전하게 유지하는 방법
- 변경을 계획하고 순서를 잡기 위해 시각적 코드 맵(visual code map) 같은 도구를 활용하는 법
- 성공하기 어려운 빅뱅(Big‑Bang) 리라이트의 함정을 피하는 방법
리팩터링은 이벤트가 아니다. 습관이다.
리팩터링은 종종 하나의 단계나 기간으로 포장됩니다.
“일단 기능부터 만들고, 나중에 리팩터링하자.”
여기서 “나중에”는 대부분 “영원히 안 할 때”를 뜻합니다. 아니면 더 나쁘게는, “너무 고통스러워져서 모든 걸 멈추고 대청소를 할 수밖에 없을 때”를 의미하죠. 그때쯤이면 코드베이스는 이미 심각하게 꼬여 있고, 어떤 리팩터링이든 위험하게 느껴집니다.
더 건강한 관점은 이것입니다. 리팩터링은 매일 하는 개발의 일부이지, 따로 떼어낸 프로젝트가 아니라는 것.
- 기능을 추가하려고 파일을 건드릴 때, 그 주변 코드도 함께 개선합니다.
- 버그를 고칠 때, 그 주변의 로직을 더 명확하게 정리합니다.
- 어떤 모듈이 이해하기 어려워지면, 더 커지기 전에 쪼개거나 이름을 바꿉니다.
이건 요리할 때 설거지를 전부 나중에 3시간 동안 몰아서 하는 대신, 중간중간 조금씩 해두는 것과 비슷합니다. 작업은 더 가볍고 덜 스트레스이며, 부엌(코드베이스)은 감당 못 할 정도로 엉망이 되지 않습니다.
핵심 아이디어: 리팩터링은 변경 비용을 낮게 유지해 주는 지속적인 백그라운드 작업입니다.
리팩터링을 명시적으로: 캘린더에 올려라
리팩터링이 중요하다고 말하는 건 쉽습니다. 실제로 하는 건 어렵죠. 특히 마감, 고객, 기능 출시 압박이 있을 때는 더더욱 그렇습니다.
그래서 리팩터링에는 반드시 명시적인 시간을 할당해야 합니다.
실전에 쓸 수 있는 몇 가지 패턴입니다.
-
스프린트별 예산 설정
- 각 스프린트에서 일정 비율(예: 10–20%)을 리팩터링 작업에 예약합니다.
- 다른 작업과 똑같이 관리합니다: 티켓, 추정, 완료 기준(acceptance criteria).
-
보이스카우트 룰(Boy Scout Rule) 적용
- 누군가 기능 개발이나 버그 수정을 위해 파일을 수정할 때마다 최소한 한 가지 작은 개선을 합니다. 예: 더 나은 네이밍, 메서드 추출, 죽은 코드 제거, 더 명확한 주석.
-
Refactor‑at‑Touch 정책
- 코드베이스의 어떤 영역을 작업할 때, 그 영역을 처음보다 더 깨끗하게 만들어 두는 책임을 집니다.
- 필요한 정리가 너무 크다면, 거대한 보이지 않는 한 번의 노력으로 몰아 하지 말고 작은 리팩터링 태스크들로 쪼개서 별도의 작업으로 관리합니다.
가장 중요한 포인트는 이것입니다.
리팩터링 시간이 일정에 잡혀 있지 않으면, 결국 제물로 바쳐진다.
시간을 명시적으로 예산 편성함으로써, “기능 하나 더”를 위해 항상 미래를 저당 잡히는 대신, 시스템의 장기적인 건강을 지킬 수 있습니다.
작은 레일의 힘: 작고 안전한 리팩터링
기차 세트로 놀 때, 머릿속에서 전체 레이아웃을 다 계산하고 시작하지는 않았습니다.
그 대신 이렇게 했죠.
- 레일 몇 개를 깐다
- 잘 맞는지 확인한다
- 조정한다
- 반복한다
리팩터링도 똑같이 작동해야 합니다. 작고, 조합 가능하며, 이해하기 쉽고, 바로 배포해도 되는 단계들이어야 합니다.
좋은 “레일 조각”의 예시는 다음과 같습니다.
- 헷갈리는 변수, 메서드, 클래스의 이름을 바꾼다
- 중복 코드를 없애기 위해 메서드를 추출한다 (extract method)
- 지나치게 긴 함수를 더 작은 함수들로 쪼갠다
- 외부 의존성을 인터페이스 뒤로 숨겨 격리한다
- 응집력 있는 함수 묶음을 새 모듈로 옮긴다
각 변경은 다음을 만족해야 합니다.
- 한 번의 코드 리뷰 안에서 무리한 정신적 점프 없이 이해 가능해야 합니다.
- 명확한 목적을 가져야 합니다. (예: “결제 게이트웨이 API를 위한 어댑터 도입”)
- 테스트로 뒷받침되거나, 최소한 독립적으로 테스트 가능해야 합니다.
이러한 작은 단계들은 종이 레일과 같습니다. 시스템 전체를 한 번에 재설계하지 않고도, 현재 동작하는 상태에서 다음 동작 가능한 상태로 안전하게 연결해 줍니다.
작은 변화의 반복이 만드는 큰 효과
개별적으로 보면 이런 리팩터링은 사소해 보일 수 있습니다.
- “그냥 이름만 바꾼 건데.”
- “그냥 메서드 하나 뺀 건데.”
- “그냥 파일 위치만 옮긴 건데.”
하지만 누적 효과는 매우 큽니다.
몇 주, 몇 달이 지나면 작은 리팩터링들이:
- 인지 부하를 줄입니다: 코드를 읽고 이해하기가 쉬워집니다.
- 변경의 파급 범위를 줄입니다: 결합도가 낮아지고, 예상치 못한 영향이 줄어듭니다.
- 온보딩 속도를 높입니다: 새로운 개발자가 훨씬 빠르게 적응합니다.
- 버그율을 낮춥니다: 복잡성이 줄어들수록, 그 안에 숨어 있던 결함도 줄어듭니다.
단순히 표면을 다듬는 게 아니라, 시간이 지나면서 아키텍처 자체를 서서히 좋은 방향으로 재형성하는 셈입니다.
일 년에 한 번 하는 극단적인 다이어트 대신, 꾸준한 운동을 하는 것과 비슷합니다. 매일의 변화를 체감하지 못할 수도 있지만, 6개월이 지나면 차이가 극명해집니다.
작은 리팩터링들의 누적 효과가 코드베이스를 건강하게 유지하는 방법입니다.
빅뱅 리팩터링이 보통 실패하는 이유
작고 꾸준한 리팩터링이 그렇게 효과적인데도, 왜 여전히 팀들은 거대한 리라이트나 빅뱅 리팩터링을 시도할까요?
현재 시스템이 너무 견디기 힘들게 느껴지고, 새로 시작하면 깨끗하고 단순해 보이기 때문입니다.
하지만 빅뱅 리팩터링에는 심각한 리스크가 따릅니다.
- 숨겨진 복잡성: 수년간의 버그 수정, 우회로, 엣지 케이스들이 스펙(document)이 아니라 코드 안에 녹아 있습니다. 전부 처음부터 다시 구현하면, 예전에 고쳤던 버그를 다시 도입하기 쉽습니다.
- 긴 타임라인: 대규모 리라이트는 몇 달씩 걸리곤 합니다. 그 기간 동안 기존 시스템의 기능 개발은 느려지거나 멈추고, 새 시스템은 계속 움직이는 타깃을 뒤쫓게 됩니다.
- 통합 쇼크: 새 아키텍처는 흔히 레거시 데이터, 외부 서비스, 성능 특성 등 현실의 제약과 완전히 딱 맞아떨어지지 않습니다.
- 사기 저하: 끝이 보이지 않는 장기 리라이트 작업은 팀의 동기를 크게 떨어뜨립니다.
반대로, 점진적인 단계별 리팩터링을 하면:
- 코드베이스를 개선하는 동시에 계속 기능을 출시할 수 있습니다.
- 각 단계에서 배우고, 그에 따라 계획을 조정할 수 있습니다.
- 특정 변경이 효과가 없으면, 롤백하거나 방향을 틀 수 있습니다.
여전히 더 나은 아키텍처를 향해 나아가지만, 거대한 한 방짜리 초고속 열차가 아니라, 작은 기차 여러 대가 차례차례 들어오는 형태인 셈입니다. 그리고 그 작은 기차들은 실제로 도착합니다.
트랙을 설계하는 도구: 시각적 코드 맵 활용하기
리팩터링은 시스템을 눈으로 볼 수 있을 때 훨씬 쉬워집니다.
여기서 코드 매핑 툴(예: Codemaps 같은 시각화 도구)이 강력한 도우미가 됩니다.
이런 도구들은 다음을 조망하게 해 줍니다.
- 모듈 경계
- 의존성 방향
- 순환 의존성(cycle)과 핫스팟
- 변경 빈도나 복잡도가 높은 구역
이 맵을 가지고 나면, 다음과 같은 일을 할 수 있습니다.
-
안전한 변경 시퀀스 식별
- 먼저 독립적으로 리팩터링 가능한 모듈들을 찾습니다.
- 예를 들어 이런 시퀀스를 계획할 수 있습니다: “A와 B 사이의 순환 제거 → B에서 인터페이스 추출 → 구현을 C로 이동”.
-
가치가 높은 영역에 우선순위 부여
- 복잡하면서 자주 변경되는 코드에 리팩터링 시간을 집중합니다.
- 거의 손대지 않는 주변부를 지나치게 다듬느라, 진짜 핵심이 엉켜 있는 상태를 방치하지 않습니다.
-
계획을 눈에 보이게 공유
- 설계 회의나 Pull Request 리뷰에서 시각적 맵을 활용합니다.
- 현재 아키텍처에서 목표 아키텍처로 어떻게 이동할지, 그 진행 상황을 팀 전체가 볼 수 있게 만듭니다.
맵 자체가 코드를 대신 리팩터링해 주지는 않지만, 커밋하기 전에 바닥에 종이 레일을 먼저 펼쳐 보는 역할을 합니다. 머릿속으로 여러 시퀀스를 시뮬레이션해 보고, 막다른 길을 피하고, 지금 위치에서 원하는 지점까지 가는 가장 짧고 안전한 경로를 선택할 수 있게 해줍니다.
실전 워크플로우: 리팩터링을 기차 운행표처럼 운영하기
이 마인드셋을 일상 업무에 녹여 넣기 위한 가벼운 워크플로우를 하나 제안합니다.
-
먼저 맵을 만든다 (선택 사항이지만 매우 유용)
- 시각화 도구로 모듈 경계와 의존 관계를 파악합니다.
-
완벽한 목적지 대신, 방향을 정한다
- 예: “UI 레이어가 DB 코드에 직접 의존하지 않는 계층형 아키텍처로 점진적으로 이동하자.”
- 처음부터 완벽한 최종 설계를 알 필요는 없습니다. 더 나은 방향만 정하면 됩니다.
-
짧은 시퀀스의 작은 단계들을 계획한다
- 예: “서비스 인터페이스 추출 → 애플리케이션 레이어 도입 → 컨트롤러가 점진적으로 새로운 레이어를 거치도록 변경.”
-
스프린트마다 시간을 배정한다
- 이 단계들을 일반 태스크처럼 보드에 올립니다.
- 기능 개발 태스크와 섞어서 진행합니다.
-
모든 단계는 항상 배포 가능하게 유지한다
- 각 변경 이후 시스템은 빌드되고, 테스트를 통과하며, 배포 가능한 상태여야 합니다.
- 필요하다면 Feature Flag나 Branch‑by‑Abstraction 같은 기법을 사용합니다.
-
주기적으로 리뷰하고 조정한다
- 몇 스프린트마다 맵과 진행 상황을 다시 봅니다.
- 그동안 배운 것을 바탕으로 방향을 조정합니다.
시간이 지나면, 이렇게 짜여진 “리팩터링 기차 운행표”가 조용히 아키텍처를 바꿔 놓습니다.
결론: 레일을 계속 깔아라
엉망이 된 코드베이스를 구제하기 위해, 영웅적인 리라이트가 꼭 필요한 건 아닙니다.
정말 필요한 것은 다음과 같습니다.
- 리팩터링을 끊임없이 이어지는 작업으로 보는 마인드셋
- 리팩터링이 실제로 일어나도록 하는 전용 시간
- 작고 이해 가능한 변경을 고집하는 태도
- 수많은 미세 개선이 만들어내는 복리 효과에 대한 이해
- 세상을 바꿀 것처럼 보이지만 실제 성과는 미미한 빅뱅 리팩터링에 대한 경계심
- 안전하고 순차적인 단계를 계획하기 위한 시각적 코드 맵 같은 도구들
당신의 시스템을 그 옛날 아날로그 기차 세트라고 생각해 봅시다.
한 번에 완벽한 철도망을 만들지 않습니다. 대신 이렇게 하죠.
- 레일을 하나 깐다
- 잘 맞는지 본다
- 조정한다
- 하나를 더 잇는다
이걸 매일 반복하다 보면, 어느 순간 고개를 들어봤을 때 이미 기차들이 깔끔하고 일관성 있으며 유지보수하기 쉬운 레일 위를 잘 달리고 있다는 걸 발견하게 될 겁니다. 그 과정에서 단 한 번도 전체 운행을 멈출 필요는 없습니다.
변경은 작게 유지하세요. 끊임없이 이어가세요. 그리고 그 종이 레일을 계속 깔아 나가세요.