# 코드 리팩토링: 실제로 작동하는 기법들

URL: https://whatshouldibuildnext.com/ko/journal/code-refactoring-techniques
Type: blog
Locale: ko
Published: 2026-06-29
Updated: 2026-07-09

---

> 리팩토링을 하면 좋다는 건 알지만, 언제 시작할지, 어디서 멈춰야 할지는 불명확하다. 여기 6개월간 레거시 추천 시스템을 다루며 배운 신호와 기법들.

## 코드 리팩토링: 실제로 작동하는 기법들

대부분의 개발자는 리팩토링의 중요성을 안다. 무엇을 하지 않으면 더 어려워지는지, 코드가 정말로 정리가 필요한지 vs 전체 재작성이 필요한지 판단하는 건 덜 명확하다. 여기 6개월간 레거시 추천 시스템을 다루며 깨달은 것들, 그리고 다음 번엔 어떻게 할지에 대한 이야기다.

![다크 홈 오피스에서 두 모니터로 코드 리팩토링 작업 중인 개발자](https://fdzlnqpwsaniezitwiuw.supabase.co/storage/v1/object/public/cms-media/whatshouldibuildnext/2026-06/e1f301-inline1.webp)

## 리팩토링이 정말 필요하다는 신호들

엉망으로 보이는 함수가 다 손 댈 가치는 없다. 진지하게 봐야 할 신호 3가지:

**중복이 여러 곳에서 나타난다.** 같은 데이터 변환을 3개의 컨트롤러에서 반복하고 있다면, 이건 스타일 문제가 아니다. 메인터넌스의 폭탄이다. 상위의 한 줄 수정이 3곳을 고쳐야 한다는 뜻이고, 당신은 한 곳을 빠뜨릴 것이다.

**한 곳의 수정이 전혀 다른 기능을 부순다.** 결제 플로우의 버그 수정이 갑자기 유저 프로필 렌더링에 영향을 준다면, 결합도가 너무 높다. 코드베이스가 별개인 책임들을 하나로 취급하고 있다.

**신입 엔지니어가 30분 안에 방향을 잡지 못한다.** 이건 과소평가된 신호다. 파일 전체를 읽지 않고는 그 파일이 뭐 하는지 설명할 수 없다면, 그 파일은 너무 많은 일을 한다. Appier에선 온보딩 시간을 재기 시작했다. 설명에 15분 이상 걸리는 파일들이 리팩토링 백로그에 들어갔다.

**코드 체인지 집중도.** `git log --stat`을 실행해서 어떤 파일이 가장 자주 건드려지는지 봐라. 같은 3개 파일이 전체 커밋의 60%를 차지한다면, 그게 신호다. 높은 변경량 + 높은 복잡도 = 80/20 핫스팟이고, 여기부터 시작할 가치가 있다.

리팩토링을 건너뛰는 경우: 코드가 작동하고, 아무도 건드리지 않으며, 확장할 이유가 없다. Boy Scout Rule(쓰고 떠날 땐 더 깨끗하게)은 좋은 원칙이지만, 쓰지 않는 캠프사이트까지 청소할 필요는 없다.

## Extract Method: 80% 상황에서 쓸 기법

Extract Method는 긴 함수에서 의미 있는 로직 덩어리를 빼내서 이름을 붙이는 것이다. 그 이름이 문서다.

Before:

`def process_order(order):
    # validate
    if not order.get('user_id'):
        raise ValueError('Missing user_id')
    if order.get('amount', 0) <= 0:
        raise ValueError('Invalid amount')
    # apply discount
    if order.get('coupon') == 'LAUNCH20':
        order['amount'] = order['amount'] * 0.8
    # save
    db.save(order)`After:

`def process_order(order):
    validate_order(order)
    apply_discount(order)
    db.save(order)

def validate_order(order):
    if not order.get('user_id'):
        raise ValueError('Missing user_id')
    if order.get('amount', 0) <= 0:
        raise ValueError('Invalid amount')

def apply_discount(order):
    if order.get('coupon') == 'LAUNCH20':
        order['amount'] = order['amount'] * 0.8`"After" 버전이 더 길다. 괜찮다. 각 함수는 이제 존재 이유가 하나씩만 있고, 할인 로직이 바뀔 때(그리고 바뀐다), 정확히 어디로 가야 하는지 안다.

보람이 있으려면: 원래 함수가 20-25줄 정도 이상이거나, `# validate` 같은 주석을 달아야 할 때다. 그 주석이 함수 이름 대기 중인 것이다. `# apply discount` 주석을 달았다면, 메서드의 이름은 이미 정해진 거고, 아직 추출만 안 한 거다.

실전 법칙: 다음 5줄을 설명하는 인라인 주석을 달고 싶으면, 그건 추출 후보다.

## Composing Methods: Extract Method로 충분하지 않을 때

Composing은 더 나간다. 한 메서드를 추출하는 대신, 전체 클래스나 모듈을 더 작은, 집중된 컴포넌트들로 분해한다. 파일이 600줄로 자라서 5개의 별개 책임을 한다면 이게 필요하다.

패턴: 각 책임을 구분하고, 각각 자기 클래스나 모듈을 주고, 상위 레벨에서 연결한다. Martin Fowler는 이걸 "한 개의 비대해진 클래스를 협력하는 여러 작은 클래스의 집합으로 변환"이라고 부른다.

실전에서 어려운 부분은 메커니즘이 아니라, 한 책임이 끝나고 다른 책임이 시작하는 지점을 찾는 것이다. 유용한 휴리스틱: 함수나 클래스가 뭐 하는지 "그리고(and)"라는 말 없이 설명할 수 있다면, 책임이 하나다. "그리고"가 나오면, 나눠야 한다.

찾는 다른 방법: 파일 맨 위의 import들을 보자. 하나의 모듈이 6개의 다른 영역에서 import한다면(데이터베이스, 이메일, 로깅, 결제, 리포팅), 하는 일이 너무 많다. 각 import 클러스터가 자기 모듈의 후보다.

Composing은 테스트 가능성도 높인다. 400줄의 클래스는 단위 테스트하기 어렵다. 명확한 인터페이스의 100줄 클래스 4개는 모킹과 격리가 훨씬 쉽다.

![코드 아키텍처 리팩토링 다이어그램을 계획 중인 화이트보드 개발자](https://fdzlnqpwsaniezitwiuw.supabase.co/storage/v1/object/public/cms-media/whatshouldibuildnext/2026-06/9e14e1-inline2.webp)

## Branch-by-Abstraction: 배포 중에 리팩토링하기

이건 더 아키텍처 쪽이지만, 프로덕션 환경에서 대규모 리팩토링을 생존시키는 기법이다.

아이디어: 오래된 구현에서 새 구현으로 한 번에 바꾸는 대신, 추상화 레이어(보통 인터페이스나 프로토콜)를 그 코드 주위에 도입한다. 오래된 것과 새로운 것 모두 인터페이스를 만족한다. 새 구현을 기능 플래그 뒤에서 배포하고, 양쪽을 동시에 돌리다가, 새 것이 안정화되면 옛날 것을 지운다.

사이드 프로젝트에서 중요한 이유: 결제 통합을 다시 짜거나 ORM을 옮길 때, branch-by-abstraction은 현재 버전이 프로덕션에서 작동하는 동안 새 것을 구워낼 수 있다는 뜻이다. 3주간 메인과 멀어지는 "리팩토링 브랜치" 같은 건 없다.

대가: 깔끔한 인터페이스를 정의하는 데 선행 작업이 더 필요하다. 교체할 것이 위험할 때(인증, 빌링, 데이터 접근) 가치 있다.

구체적 예: 레거시 이벤트-트래킹 모듈이 PostgreSQL 테이블에 직접 썼다. Kafka 기반 파이프라인으로의 이동은 40+ 호출 지점을 건드려야 했다. Branch-by-abstraction으로, `EventTracker` 인터페이스를 도입했다. 플래그 뒤에서 구현을 바꿨다. 2주간 양쪽을 동시에 돌린 후, 옛날 것을 지웠다. 다운타임 0, 긴급 배포 0.

## 조건문 정리하기: 과소평가된 정리

조건문 로직이 복잡함을 숨긴다. 5개의 중첩된 `if`를 가진 함수는 5개의 별개 조건을 가드 클로즈나 다형성으로 표현한 것보다 테스트, 읽기, 확장이 다 어렵다.

**중첩된 조건을 가드 클로즈로 바꾼다:**

Before:

`function getDiscount(user) {
  if (user) {
    if (user.isActive) {
      if (user.plan === 'pro') {
        return 0.20;
      } else {
        return 0.05;
      }
    } else {
      return 0;
    }
  } else {
    return 0;
  }
}`After:

`function getDiscount(user) {
  if (!user) return 0;
  if (!user.isActive) return 0;
  if (user.plan === 'pro') return 0.20;
  return 0.05;
}`같은 로직. 훨씬 읽기 쉽다. 앞의 리턴들이 들여쓰기 피라미드를 없애고, 각 조건을 따로 읽게 해준다.

**조건을 다형성으로 바꾼다**: 같은 `if type === X` 체크가 여러 함수에서 반복되면, 베이스 클래스나 프로토콜을 정의하고, 타입마다 하나씩 구현하고, 호출 지점에서 디스패치되게 한다. 건너뛰는 경우: 타입이 2개뿐이고 3번째가 생길 가능성이 낮다면, 추상화 오버헤드는 이진 선택지에 맞지 않다.

**중복 조건을 통합한다**: 같은 조건을 3곳에서 체크하고 있으면, 그 조건을 이름 붙은 predicate 함수로 추출한다. `if (isEligibleForDiscount(user))`는 3부로 나뉜 체크를 반복하는 것보다 낫다.

![공유 책상에서 페어 프로그래밍 및 코드 리뷰 세션 중인 두 개발자](https://fdzlnqpwsaniezitwiuw.supabase.co/storage/v1/object/public/cms-media/whatshouldibuildnext/2026-06/78aadd-inline3.webp)

## 기능을 객체들 사이에서 옮기기: 덜 알려진 기법

때론 메서드가 잘못된 클래스에 있다. 코드 자체가 잘못된 게 아니라, 자기 클래스보다 다른 클래스의 데이터를 더 많이 쓰기 때문이다. 이게 옮길 신호다.

Move Method 패턴은 간단하다: 메서드를 찾고, 실제로 어떤 클래스에 더 의존하는지 확인하고, 거기로 옮기고, 호출자들을 갱신한다. IDE의 린트가 깨진 레퍼런스를 잡아낼 것이다.

사이드 프로젝트에서 가장 많이 보이는 것: 어디 속할지 명확하지 않은 모든 걸 모으는 유틸리티 클래스들. `helpers.js`나 `utils.py` 파일이 400줄로 자라난다. 어느 시점 이 함수들은 자신이 조작하는 도메인 객체 위에 있어야 한다. `User` 객체를 받아서 5가지를 하는 함수는 `utils`가 아니라 `User` 위에 있어야 한다.

건너뛰는 경우: 프로젝트가 2주 되고 아직 형태를 찾고 있다면, 구조를 오버-엔지니어링하지 마라. 실제 쓰임에서 설계가 나타나게 하고, 그 다음에 깨끗한 도메인 모델에 투자해라. 너무 일찍 옮기면 요구사항이 바뀔 때 다시 옮긴다.

## 리팩토링 전에 해야 할 것: 테스트 베이스라인

테스트 없는 리팩토링은 도박이다. 정리하는 줄 알았는데, 망가뜨리는 거다.

최소 안전망: characterization tests. 건드리려는 코드의 현재 행동을 포착하는 테스트를 쓴다. 뭐 해야 하는지가 아니라, 지금 뭐 하는지. 이 테스트는 설계를 검증하는 게 아니라 회귀를 잡는 데 있다.

베이스라인을 잡은 후, 작은 커밋으로 리팩토링한다. 패턴 하나씩 한 커밋. 뭔가 깨지면, 뭐가 깨뜨렸는지 정확히 안다. 다른 방법과 비교: 3일짜리 리팩토링 세션을 하나의 거대한 diff로 커밋하기. CI가 새벽 2시에 실패하면, 40개 변경 중 뭐가 깨뜨렸는지 모른다.

리팩토링 시작 전 실전 체크리스트:

- 
영향받는 모듈의 characterization 테스트 쓰기

- 
시작 전에 CI 파이프라인이 초록색인지 확인

- 
80/20 핫스팟 파악: 복잡하면서도 자주 건드려지는 코드(git log가 데이터를 보여줄 것)

- 
한 기법 선택, 적용, 초록색, 커밋, 다음으로 이동

- 
리팩토링 커밋을 기능 커밋에서 분리해서 diff가 읽기 좋게 유지

유용한 제약: 커밋 메시지 한 문장으로 리팩토링을 설명할 수 없다면, 한 번에 너무 많이 한다.

## 데이터 정리하기: 다른 모든 걸 개선하는 패턴

지저분한 조건문이 많은 이유는 데이터 모델이 잘못되었기 때문이다. raw 프리미티브를 넘겨다니면, 타입 체크 조건이 곳곳에 생긴다.

프리미티브를 객체로 바꾼다(`string email`이 `EmailAddress`가 되고, `int cents`가 `Money`가 된다). 검증 로직의 집이 생긴다. 12곳에서 `if not re.match(EMAIL_REGEX, email)`을 체크하는 대신, `EmailAddress` 생성자가 한 번만 한다.

이건 Fowler의 "Replace Type Code with Class" 패턴이다. 화려하진 않지만, 조건문 무성을 거의 다른 어떤 것보다 잘 줄인다.

매직 넘버도 같은 원칙: `if status == 3`은 읽기 어렵다. `if status == OrderStatus.SHIPPED`는 아니다. 매직 넘버를 이름 붙은 상수로 바꾸는 데 5분이 들고, 누군가 코드를 읽을 때마다 값어치를 한다.

## 리팩토링하면 안 될 때: 사람들이 건너뛰는 경우들

대부분의 가이드가 여기서 끝난다. 모든 코드베이스가 리팩토링할 가치는 없고, 정리를 항상 좋은 것으로 취급하면, 다른 종류의 기술 부채가 될 뿐이다.

**하드 마감 직전에는 리팩토링하지 마라.** 리팩토링은 변화를 가져온다. 변화는 위험을 가져온다. 48시간 안에 클라이언트 데모가 있다면, 지금은 아니다.

**삭제할 코드를 리팩토링하지 마라.** 다음 스프린트에서 기능이 잘려나간다면, 구현을 정리하는 건 낭비다. 정리에 투자하기 전에 삭제를 확인해라.

**스타일만으로는 리팩토링하지 마라.** 코드가 작동하고, 테스트되고, 아무도 건드리지 않으면, 당신이 다르게 썼을 거라는 사실은 바꿀 이유가 아니다. 회귀의 위험은 실제이고, 이득은 미학이다.

**명확한 목표 없이는 리팩토링하지 마라.** "이거 정리해"는 작업이 아니다. "OrderProcessor의 검증 로직을 전용 Validator 클래스로 추출"은 작업이다. 모호한 범위는 범위 증대로 이어지고, 이는 3주간 메인에서 멀어졌다 컨플릭트로 병합되는 브랜치를 만든다.

이건 완벽한 규칙 집합이 아니다. 실무에서 쓸 수 있는 규칙이고, 프로덕션 코드베이스에서는 실무용이 완벽함을 이긴다.

## 6개월 증분 리팩토링이 실제로 어떤 모습인지

팀이 증분적으로, 매 스프린트 조금씩, 자신들이 배포하는 기능과 맞춰서 리팩토링하면, 작업이 더 싼 코드베이스를 갖춘다. 처음엔 더 빠르지 않지만, 확장이 빠르고, 디버그가 빠르고, 신입 동료 온보딩이 빠르다.

솔로 빌더: 이득은 개인적이다. 당신은 6개월 뒤의 신입 엔지니어다. 오늘 정리한 코드는 1월에 다시 읽지 않아도 이해하는 코드다.

추천 시스템 프로젝트의 6개월 스냅샷: 1200줄의 서비스 파일로 시작했고, 테스트는 없었다. 매 기능 스프린트마다 메서드를 추출했고, 각 섹션을 건드릴 때마다 characterization 테스트를 더했다. 도메인을 더 잘 이해하면서, 것들을 맞는 클래스로 옮겼다. 6개월 후, 같은 로직이 9개 파일에 나뉘었고, 각각 평균 130줄, 모두 테스트, 모두 뭐 하는지를 이름으로 표현했다. 배포 빈도는 주 1회에서 매일로. 버그로부터 복구 시간은 대략 40% 줄었다.

그 숫자들은 연구가 아니다. 한 팀, 한 시스템에서 나온 거다. 하지만 방향은 맞다. 더 작고 깨끗한 단위는 빠르게 바뀐다. 전부가 그 베팅이다.

## FAQ

### 코드 리팩토링이란 뭐고, 왜 중요한가?

코드 리팩토링은 외부 동작을 바꾸지 않고 코드 구조를 재구성하는 작업이다. 중요한 이유는 코드베이스를 이해하고 확장하고 디버그하기 쉽게 만들기 때문이고, 앞으로의 기능 개발과 버그 수정 비용을 직접 줄인다.

### Extract Method 리팩토링 기법이란?

Extract Method는 긴 함수에서 의미 있는 로직 블록을 빼내 자신의 이름을 붙인다. 그 이름이 인라인 문서 역할을 한다. 가장 자주 쓰는 리팩토링 기법이고, 함수가 20-25줄을 넘을 때 첫 선택지로 쓸 가치가 있다.

### 리팩토링하면 안 될 때는?

하드 마감 직전, 삭제할 코드, 작동하고 아무도 건드리지 않는 코드에서는 리팩토링하지 마라. 리팩토링은 변화를 가져오고 위험을 만들기 때문에, 명확한 이득이 있을 때만 가치 있다.

### Branch-by-Abstraction이란?

Branch-by-Abstraction은 교체할 코드 주위에 인터페이스 레이어를 도입하고, 옛 구현과 새 구현을 모두 그 인터페이스가 만족하게 한다. 새 버전을 기능 플래그 뒤에 배포하고 양쪽을 병렬로 돌린 뒤, 안정화되면 옛 것을 지움으로써 위험한 일괄 교체를 피한다.

### 가드 클로즈는 조건문을 어떻게 정리하는가?

가드 클로즈는 중첩된 if-else 블록을 함수 맨 위의 조기 리턴으로 바꾼다. 각 조건이 만족 안 되면 즉시 나가고, 들여쓰기 피라미드를 제거하고, 행복한 경로를 한눈에 읽게 한다.

### 리팩토링 전에 테스트가 필요한가?

필요하다. Characterization 테스트는 건드리려는 코드의 현재 행동을 포착한다. 테스트 없이는 리팩토링이 추측일 뿐이고, 뭔가를 깨뜨렸는데 프로덕션까지 모를 수도 있다. 테스트부터, 작은 커밋으로 리팩토링한다.

### Move Method 리팩토링 기법이란?

Move Method는 함수를 자신이 가장 많이 의존하는 클래스로 옮긴다. 함수가 다른 클래스의 데이터를 자기 클래스보다 더 많이 쓰면, 그건 잘못된 위치의 신호다. 대부분의 IDE가 메커니즘 이동과 레퍼런스 갱신을 자동으로 처리한다.