체크리스트 먼저 리팩터링: 코드를 건드리기 전에 단계부터 쓰면 왜 더 안전하고 빨라지는가
코드를 바꾸기 전에 먼저 리팩터링 체크리스트를 상세히 만들어 두면, 변경을 더 안전하고 빠르게 진행하면서도 유지보수성과 아키텍처를 꾸준히 개선해 나갈 수 있는 방법을 소개합니다.
소개
대부분의 리팩터링은 첫 줄의 코드를 바꾸기도 전에 이미 실패할 준비를 끝낸 상태입니다.
문제는 계획 단계에서 생깁니다.
큰 파일을 열어 지저분한 로직을 보고 이렇게 생각합니다. "이거 그냥 금방 좀 깔끔하게 정리해야겠다." 두 시간 뒤, 깨지는 테스트, 꼬여버린 의존성, 배포하기 무서운 어정쩡한 반쯤 완료된 리라이트를 붙잡고 씨름하고 있게 되죠.
이 위험을 크게 줄여 주는 아주 단순한 습관이 있습니다. 바로 체크리스트 먼저 리팩터링입니다.
코드부터 시작하는 대신, 파일별·변경 단위별 단계형 체크리스트를 먼저 작성합니다. 각 단계마다 어떻게 테스트하고 검증할지도 함께 정의합니다. 그리고 리팩터링 내내 이 체크리스트를 단일 진실 공급원(single source of truth) 으로 취급합니다.
이 글에서는 이 접근이 왜 효과적인지, 어떻게 실천하는지, 그리고 이렇게 만든 리팩터링 체크리스트를 시간이 지날수록 팀의 엔지니어링 수준을 끌어올리는 살아 있는 재사용 도구로 만드는 방법까지 살펴봅니다.
체크리스트 먼저 리팩터링이 효과적인 이유
리팩터링은 본질적으로 위험합니다. 복잡한 시스템 안에서 원래 동작은 유지한 채로 코드 경로를 바꾸는 일이기 때문입니다. 대부분 문제는 우리의 실력보다는 프로세스에서 비롯됩니다.
- 한 번에 너무 많은 걸 바꾸려 합니다.
- “깔끔하게 정리한다” 말고는 명확한 목표 없이 리팩터링을 합니다.
- 변경 사항과 검증 단계가 정렬되어 있지 않습니다.
- 가독성, 성능 같은 비기능적 개선을 놓치기 쉽습니다.
체크리스트 먼저 접근은 이런 문제를 다음처럼 해결합니다.
- 생각을 외부화 – 머릿속이 아니라 텍스트로 리팩터링을 설계합니다.
- 스코프를 좁힘 – 작업을 작고 테스트 가능한 단계로 쪼개도록 강제합니다.
- 의도를 명확히 – 각 단계에 목적과 검증 전략이 생깁니다.
- 모범 사례를 명시화 – SOLID, 디자인 패턴, 성능 고려 사항을 체크리스트에 녹입니다.
- 재사용 가능 – 한 번 잘 만든 리팩터링 체크리스트는 계속 개선·공유하며 재사용할 수 있습니다.
벽돌을 쌓기 전에, 먼저 Markdown으로 건축 설계를 끝내는 것과 같습니다.
1단계: 코드를 건드리기 전에 체크리스트부터 쓴다
.md 파일을 하나 엽니다. 예: refactor-checklist.md. 그리고 변경 사항을 단계별로 서술합니다. 먼저 다음부터 시작하세요.
- Context(맥락): 현재 설계의 어떤 점이 문제인가? (예: God 객체, 순환 의존성, 중복 코드 등)
- Goals(목표): 이 작업이 끝났을 때 무엇이 좋아져 있어야 하는가?
- Non-goals(비목표): 스코프 크립을 막기 위해 명시적으로 변경하지 않을 것은 무엇인가?
그 다음, 리팩터링을 파일·단계별로 쪼개서 작성합니다.
예시 구조:
# Refactor: Extract PaymentStrategy from OrderService ## Context OrderService has too many responsibilities: validation, payment processing, notifications. ## Goals - Improve readability by separating payment logic - Make adding new payment methods easy (extensibility) - Reduce risk with small, testable steps ## Non-Goals - No change to notification system - No changes to API contracts --- ## Plan by File and Step ### Step 1: Add PaymentStrategy interface - [ ] Create `PaymentStrategy` interface in `payment/PaymentStrategy.ts` - [ ] Define `charge(amount, paymentDetails): PaymentResult` - [ ] Add unit tests for the interface contract (doc tests or example implementations) ### Step 2: Implement CreditCardPaymentStrategy - [ ] Create `CreditCardPaymentStrategy` in `payment/CreditCardPaymentStrategy.ts` - [ ] Move credit card logic from `OrderService` into this class - [ ] Write unit tests for `CreditCardPaymentStrategy` ### Step 3: Wire strategy into OrderService (without removing old code yet) - [ ] Inject `PaymentStrategy` dependency into `OrderService` - [ ] Add feature flag or config switch for using the strategy - [ ] Add tests verifying behavior unchanged when flag is on ### Step 4: Remove duplicate payment logic - [ ] Delete old payment logic from `OrderService` - [ ] Run regression tests and compare behavior ### Step 5: Cleanup and documentation - [ ] Update architecture docs to describe new payment strategy - [ ] Add notes on how to add a new payment method
여기서 중요한 점은: 이 모든 걸 코드 변경 전에 끝낸다는 것입니다. 즉, 여정 자체를 먼저 설계하는 셈입니다.
2단계: 체크리스트를 단일 진실 공급원으로 취급하기
코딩을 시작하고 나면, 체크리스트는 곧 리팩터링 계약서가 됩니다.
- 코드에서 갑자기 새로운 일을 하기로 했다면, 반드시 체크리스트를 먼저 업데이트합니다.
- 항목을 체크하는 기준은 단 하나입니다.
- 코드 변경이 완료됐고,
- 그 항목을 위해 계획했던 테스트를 통과했을 때만 체크합니다.
이 규율이 주는 장점은 다음과 같습니다.
- 추적 가능성 – 언제든지 “어디가, 왜, 어떻게 바뀌었는가?”에 답할 수 있습니다.
- 리뷰 용이성 – 코드 리뷰어가 체크리스트를 따라가며 커밋·PR을 훑을 수 있습니다.
- 안전한 중단 지점 – 각 항목 사이마다 시스템이 안정된 상태로 잠시 멈출 수 있습니다.
실무에서 쓸 수 있는 패턴은 다음과 같습니다.
- 체크리스트의 한 섹션 → 하나의 논리적 커밋 또는 PR.
- 체크리스트 항목을 체크 → 테스트 그린 + 리뷰 완료.
이 Markdown 파일은 곧 살아 있는 변경 로그이자 안전망이 됩니다.
3단계: 리팩터링을 작고 점진적으로 유지하기
리팩터링은 “한 번에 다 해치우려” 할 때 망가지기 쉽습니다.
체크리스트 먼저 접근은 점진적으로 생각하도록 강제합니다.
- 변경을 도입(introduce) 단계와 제거(remove) 단계로 나눌 수 있는가?
- 기존 코드와 새 코드를 한동안 병행 운영한 뒤에 천천히 전환할 수 있는가?
- 각 단계를 서로 독립적으로 구현·리뷰·검증할 수 있는가?
체크리스트에 담아둘 만한 대표적인 점진적 패턴들은 다음과 같습니다.
-
Branch-by-Abstraction 패턴
- 1단계: 추상화(인터페이스/어댑터)를 도입하되, 기존 경로는 그대로 둔다.
- 2단계: 일부 클라이언트를 새 추상화로 연결한다.
- 3단계: 모든 클라이언트를 새 추상화로 마이그레이션한다.
- 4단계: 기존 구현을 제거한다.
-
Strangler 패턴(모듈/서비스 교체)
- 1단계: 기존 모듈 옆에 새 모듈을 도입한다.
- 2단계: 일부 유즈케이스를 새 모듈로 라우팅한다.
- 3단계: 트래픽을 점차 모두 새 모듈로 우회한다.
- 4단계: 기존 모듈을 삭제한다.
각 리팩터링 단계는 다음을 만족해야 합니다.
- 가능한 한 적은 수의 파일만 건드릴 것.
- 명확한 롤백(되돌리기) 계획이 있을 것.
- 해당 단계까지만 진행해도 시스템이 정상적으로 동작해야 할 것.
어떤 체크리스트 항목이 너무 크거나 위험해 보인다면, 코드를 쓰기 전에 더 잘게 쪼개세요.
4단계: 체크리스트와 테스트 전략을 정렬시키기
리팩터링은 코드가 컴파일된 시점이 아니라, 행동이 검증된 시점에 끝납니다.
모든 체크리스트 항목에는 어떻게 검증할 것인지가 포함되어야 합니다. 테스트 전략을 명시적으로 선언하세요.
- TDD / Red-Green-Refactor
- 먼저 실패하는 테스트를 추가한다(Red).
- 테스트를 통과하게 만드는 최소한의 구현을 한다(Green).
- 테스트를 초록색으로 유지한 채 리팩터링한다(Refactor).
- BDD / 시나리오 기반
- Given-When-Then 형식으로 사용자 시나리오를 정의한다.
- 각 리팩터링 단계가 이 시나리오들을 계속 초록색 상태로 유지하는지 확인한다.
예시:
### Step 3: Wire strategy into OrderService - [ ] Add integration test: `OrderService charges via injected PaymentStrategy` - Red: write test, see it fail with current implementation - Green: inject `PaymentStrategy` and adapt code - Refactor: inline cleanup and remove duplication - [ ] Run full payment-related regression test suite
이 정렬이 주는 효과는 다음과 같습니다.
- 리팩터링 도중에 동작이 조용히 깨지는 일을 방지합니다.
- 체크리스트 항목 → 테스트 → 그린 → 체크 라는 자연스러운 리듬이 생깁니다.
5단계: 모범 사례를 체크리스트에 바로 녹여 넣기
체크리스트는 “무엇을 바꿀지”뿐 아니라, “어떻게 바꿀지”를 담는 도구이기도 합니다.
여기에 설계 원칙을 함께 녹여 두세요.
- SOLID
- 단일 책임 원칙(SRP): “이 단계를 마친 뒤, 이 클래스는 단 하나의 변경 이유만 가져야 한다.”
- 개방-폐쇄 원칙(OCP): “새 결제 수단은 조건문이 아니라 새 전략 클래스를 추가해서 확장한다.”
- 디자인 패턴
- Strategy, Factory, Adapter, Decorator 등.
- 성능 고려
- “변경 전후 지연(latency)을 측정한다.”
- “이 경로에서 DB 라운드 트립이 추가되지 않도록 한다.”
예시 스니펫:
### Design & Quality Checklist (applies to all steps) - [ ] SRP: Each class touched has a single clear responsibility - [ ] No new static/global coupling introduced - [ ] Dependencies point inward (toward domain), not outward - [ ] New abstractions are tested at their boundaries - [ ] No performance regressions in hot paths (compare metrics if available)
이런 항목을 명시적으로 적어 두면:
- 단순히 코드를 이리저리 옮겨놓기만 하는 “가짜 리팩터링”을 피할 수 있습니다.
- 각 변경이 시스템 아키텍처를 조금씩 더 나은 방향으로 밀어 주게 됩니다.
6단계: 비기능적 목표를 1급 시민으로 만들기
리팩터링을 하면 가독성이나 확장성이 좋아진다고들 하지만, 이 목표는 종종 모호하게 남습니다.
체크리스트에서 이걸 구체적인 기준으로 만드세요.
- 가독성(Readability)
- 메서드 이름이 어떻게가 아니라 무엇을 하는지 말해 준다.
- 함수/클래스 길이가 기준 X 라인 이하를 유지한다(팀 기준으로 정의).
- 복잡한 로직은 이름이 잘 붙은 헬퍼로 추출한다.
- 확장성(Extensibility)
- 새 기능 X를 추가할 때 기존 코어 로직은 수정하지 않아도 된다.
- 구현 Y를 교체할 때 하나의 모듈만 바꾸면 된다.
- 성능(Performance)
- 크리티컬 패스에 네트워크 호출을 추가하지 않았다.
- 메모리 사용량이 허용 범위 내에 머문다.
아예 이런 “전·후 비교” 항목을 추가해도 좋습니다.
### Final Non-Functional Review - [ ] 새로 합류한 팀원이 봐도 이해하기 쉬워졌는가? - [ ] 새 결제 수단을 추가하기가 이전보다 쉬워졌는가? - [ ] 불필요한 복잡성을 추가하지 않았는가?
이 질문에 자신 있게 “그렇다”고 답하지 못한다면, 그 리팩터링은 아직 머지하기 이른 것일 수 있습니다.
7단계: 체크리스트를 재사용 가능한 살아 있는 문서로 만들기
진짜 힘은 체크리스트가 한 번 쓰고 버리는 문서가 아니라, 계속 다듬어지는 팀의 도구가 될 때 나옵니다.
이런 패턴을 도입해 보세요.
- 레포에
/doc/refactoring/폴더를 만듭니다. - 다음과 같은 템플릿들로 시작합니다.
extract-class-refactor-template.mdmodule-strangulation-template.mdperformance-sensitive-refactor-template.md
- 각 리팩터링을 마친 뒤, 템플릿을 업데이트합니다.
- 잘 됐던 점
- 겪었던 함정
- 새로 배운 모범 사례
예시:
# Template: Extract Class Refactor ## Pre-checks - [ ] Existing behavior covered by tests? If not, add characterization tests. - [ ] Identify owners/stakeholders for this code. ## Steps - [ ] Identify cohesive responsibilities to extract - [ ] Define new class interface - [ ] Move logic incrementally, keep tests passing - [ ] Replace old usage sites - [ ] Remove dead code ## Post-checks - [ ] Review for SRP and clear naming - [ ] Run full test suite - [ ] Capture any new learnings in this template
시간이 지나면, 팀은 자기 조직의 아키텍처와 제약에 딱 맞는 리팩터링 플레이북을 갖게 됩니다.
마무리
리팩터링이 완전히 위험에서 자유로울 수는 없습니다. 하지만 규율 있고 예측 가능한 작업으로 만들 수는 있습니다.
코드를 건드리기 전에 상세한 체크리스트를 작성함으로써, 우리는 다음을 달성할 수 있습니다.
- 작업을 파일·단계 단위로 쪼갠다.
- 변경 범위를 작고 점진적으로 유지한다.
- 모든 단계를 테스트·검증 전략과 연결한다.
- SOLID 원칙, 디자인 패턴, 성능 고려를 프로세스 안에 녹인다.
- 비기능적 개선을 명시적이고 측정 가능하게 만든다.
- 미래의 리팩터링을 더 안전하고 빠르게 만들어 주는, 재사용 가능한 체크리스트를 쌓아 간다.
다음에 “이거 그냥 빨리 좀 정리해야지”라는 생각이 들면, 잠시 멈추세요.
Markdown 파일을 여세요.
체크리스트를 쓰세요.
그리고 그다음에 코드를 바꾸세요. 한 번에 하나씩, 안전하고 의도적인 단계로.