코드 스멜과 리팩토링 (Code Smell & Refactoring)

개발자 팀·2026년 6월 2일

self-study-series

목록 보기
15/16

코드가 동작은 잘 되는데 읽기는 답답하고, 한 줄 바꾸려고 들어갔다가 다섯 군데를 손대야 했던 경험이 있으실 겁니다. 그게 바로 코드에서 나는 냄새, 코드 스멜(code smell)입니다. 그 냄새를 맡고 구조를 다듬는 작업이 리팩토링(refactoring)이고요.

이 글에서는 리팩토링의 정의부터 시작해 코드 스멜의 5대 분류, 가장 흔한 스멜들과 대응하는 리팩토링 기법, 안전하게 진행하는 사이클, 그리고 언제 리팩토링하고 언제 미뤄야 하는지까지 정리합니다.


목차

  1. 리팩토링이란 무엇인가
  2. 리팩토링과 재작성의 차이
  3. 코드 스멜이란
  4. 코드 스멜 5대 분류
  5. Long Method (Long Method)
  6. 메서드 추출 (Extract Method)
  7. Large Class (Large Class)와 책임 분리
  8. Data Clumps와 매개변수 객체
  9. 분기문 남용과 다형성
  10. Shotgun Surgery과 죽은 코드
  11. 리팩토링 안전 사이클
  12. 언제 리팩토링하고 언제 미루나
  13. 자주 만나는 실수
  14. 핵심 요약

1. 리팩토링이란 무엇인가

리팩토링 은 외부 동작은 그대로 두면서 내부 구조를 개선하는 작업입니다. 핵심은 동작이 변하지 않는다는 점입니다. 사용자가 보는 결과는 똑같습니다. 그래서 새 기능 추가나 버그 수정과는 분리된 활동입니다.

비유로 풀어보면 공구함 정리와 비슷합니다. 정리정돈된 공구함은 같은 작업을 더 빠르게 만들어줍니다. 안에 있는 도구는 그대로지만 배치가 달라지면 효율이 완전히 달라집니다. 리팩토링은 코드의 공구함을 정리하는 일이라고 보시면 됩니다.

2. 리팩토링과 재작성의 차이

비슷해 보이지만 완전히 다른 두 활동입니다.

구분리팩토링재작성
규모작은 단위, 점진적큰 단위, 전면적
동작 변화없음있을 수 있음
위험낮음높음
테스트기존 테스트로 검증새 테스트 필요
비용일상 활동큰 투자

리팩토링은 매일의 작은 정리, 재작성은 가끔의 큰 결단입니다. 둘을 혼동하면 위험이 커지거나 효율이 떨어집니다.

3. 코드 스멜이란

코드 스멜 은 마틴 파울러가 정리한 개념으로, 더 깊은 문제를 암시하는 코드의 표면적 징후를 말합니다. "냄새"라는 비유로 정확한 이름이 없는 직관을 표현한 단어입니다. "뭔가 이상한데 정확히 뭐라고 말하기 어려운" 느낌을 한 마디로 정리한 표현이죠.

여기서 중요한 건 스멜 자체가 곧 버그는 아니라는 점 입니다.

  • 컴파일은 잘 됨 (표면적으로는 정상)
  • 테스트도 통과할 수 있음 (동작은 맞음)
  • 다만 읽기 어렵고 변경하기 무서움
  • 그 자체보다 누적되어 시스템을 마비시킴

4. 코드 스멜 5대 분류

분류의미대표 스멜
Bloaters비대화Long Method, Large Class
OO Abusers객체지향 오용switch 남용, Temporary Field
Change Preventers변경 방해Shotgun Surgery, Divergent Change
Dispensables불필요주석, 중복 코드, 데드 코드
Couplers결합 문제Feature Envy, Inappropriate Intimacy

5. Long Method (Long Method)

가장 흔한 스멜입니다. 한 화면을 넘기는 메서드는 거의 항상 문제입니다. 여러 책임이 섞여 있고, 변수 스코프가 너무 커서 머릿속에서 추적하기 어렵습니다. 일반적으로 한 메서드는 한 화면(약 20~30줄) 안에 끝나는 게 이상적입니다.

증상의미
주석으로 단락을 나눔각 단락이 별도 메서드 후보
한 메서드에 여러 if 블록분기마다 책임 분리 가능
변수가 너무 많음객체로 묶을 수 있음
깊은 중첩조기 반환으로 평탄화 가능

6. 메서드 추출 (Extract Method)

Before

public void printReport(Order order) {
    System.out.println("=== 주문 리포트 ===");
    System.out.println("주문 번호: " + order.getId());
    double total = 0;
    for (OrderItem item : order.getItems()) {
        total += item.getPrice() * item.getQuantity();
    }
    System.out.println("총 합계: " + total);
    System.out.println("=== 끝 ===");
}

한 메서드에 헤더 출력, 총합 계산, 푸터 출력 세 책임이 섞여 있습니다. 총합 계산 로직만 따로 테스트하거나 재사용하기 어렵습니다.

After

public void printReport(Order order) {
    printHeader(order);
    double total = calculateTotal(order);
    System.out.println("총 합계: " + total);
    printFooter();
}

private double calculateTotal(Order order) {
    return order.getItems().stream()
            .mapToDouble(i -> i.getPrice() * i.getQuantity())
            .sum();
}

각 메서드가 한 가지 일만 하고, 이름이 그 의도를 설명합니다. calculateTotal은 단독 테스트와 재사용이 자유로워집니다.

7. Large Class (Large Class)와 책임 분리

한 클래스가 너무 많은 일을 하는 경우입니다. 단일 책임 원칙(SRP) 위반 의 흔한 증상입니다. 사용자 정보를 다루는 클래스가 인증, 권한, 프로필 수정, 이메일 발송까지 다 떠안고 있으면 Large Class입니다.

해결책은 필드 그룹마다 별도 객체로 분리하거나, 책임별로 클래스를 추출하는 것입니다.

기법효과
Extract Class관련 필드와 메서드를 별도 클래스로
Extract Superclass공통 부분을 부모로 끌어올림
Extract Interface사용 측이 보는 단면만 노출

8. Data Clumps와 매개변수 객체

public void createUser(String firstName, String lastName,
                       String street, String city,
                       String zipCode, String country) {
    // ...
}

같은 매개변수 묶음이 여러 메서드에 반복 등장하면 Data Clumps(Data Clumps) 스멜입니다. AddressFullName 같은 매개변수 객체로 묶으면 호출이 훨씬 깔끔해집니다. 매개변수 검증 로직도 그 클래스 안으로 옮길 수 있어서, 같은 검증이 여러 곳에 흩어지지 않습니다.

매개변수가 4개를 넘는 메서드를 보면 Data Clumps를 의심해 보세요.

9. 분기문 남용과 다형성

switch 문이 같은 분류 기준으로 여러 곳에 반복되면 다형성으로 바꿀 후보입니다. 예를 들어 결제 수단별로 처리 로직이 다른데, 카드 처리·환불 처리·통계 메서드마다 똑같은 switch (paymentType) 분기가 들어 있다면 문제입니다.

해결책은 각 결제 수단에 해당하는 클래스를 만들고 PaymentMethod 인터페이스로 통일하는 것입니다.

항목switch 방식다형성 방식
케이스 추가여러 곳 수정새 클래스 추가만
가독성분기 누적짧은 메서드
테스트모든 분기 검증클래스별 단위 테스트

10. Shotgun Surgery과 죽은 코드

Shotgun Surgery(Shotgun Surgery) 은 한 변경이 여러 클래스에 흩어진 작은 수정을 강요하는 스멜입니다. 가격 정책 한 군데를 바꿨더니 결제·정산·리포트·통계까지 다섯 군데를 손대야 한다면 Shotgun Surgery입니다. 응집도가 낮다는 신호이고, 관련 코드를 한 클래스에 모아 해결합니다.

죽은 코드(Dead Code) 는 호출되지 않는 메서드, 사용되지 않는 매개변수, 미사용 클래스입니다. 형상관리(Git)가 있으니 백업 걱정 없이 안전망 안에서 과감히 삭제하면 됩니다.

처리 순서는 다음과 같습니다.

  1. 정적 분석 도구로 미사용 코드 식별
  2. 호출 그래프 확인 후 안전한 것부터 삭제
  3. 삭제 후 테스트 전체 통과 확인
  4. 커밋 단위는 작게 유지

11. 리팩토링 안전 사이클

리팩토링을 안전하게 만드는 핵심은 짧은 사이클입니다.

  1. 테스트로 안전망 — 변경 전후 동작 일치를 보장
  2. 스멜 식별 — 한 번에 하나씩
  3. 작은 단위 변경 — 큰 변경은 작은 걸음의 합
  4. 테스트 즉시 실행 — 빨간불이면 곧바로 롤백
  5. 다음 스멜로 반복

이 사이클을 짧게 자주 돌리는 게 리팩토링을 안전하게 만드는 핵심입니다.

12. 언제 리팩토링하고 언제 미루나

언제 하나

  • 같은 영역을 세 번째 수정할 때 ("세 번이면 추출" 격언)
  • 기능 추가 직전
  • 버그 수정 중 발견했을 때
  • 코드 리뷰 시점

언제 미루나

  • 전면 재작성 직전 (어차피 다 바뀜)
  • 테스트가 없는 코드 (안전망 부재)
  • 마감 임박 (위험)
  • 명세가 아직 불확정 (다시 바뀔 가능성)

13. 자주 만나는 실수

테스트 없이 리팩토링 시작
변경 후 동작이 같은지 검증할 수가 없습니다. 테스트가 없는 코드라면 특성 테스트(characterization test), 즉 현재 동작을 그대로 캡처하는 테스트부터 작성해야 합니다.

큰 단위로 한 번에 변경
실패했을 때 어디서 깨졌는지 찾기 어렵습니다.

기능 추가와 리팩토링 동시 진행
문제가 생겼을 때 진단이 어렵고 책임이 섞입니다. 두 활동은 분리하는 게 좋습니다.

완벽주의
모든 스멜을 한 번에 잡으려다 어느 것도 끝내지 못합니다.

안전한 기본 패턴은 다음과 같습니다.

  • 리팩토링 전 안전망 테스트부터 확인하거나 작성
  • 커밋 단위는 작게 — 리팩토링 커밋과 기능 커밋을 분리
  • 변경 후 테스트 즉시 실행 — 빨간불이면 곧바로 롤백
  • 한 스멜에 집중 — 다른 스멜은 이슈로 기록만 하고 다음에

14. 핵심 요약

항목핵심 내용
리팩토링동작 유지하며 내부 구조 개선
코드 스멜깊은 문제의 표면적 징후
5대 분류Bloaters, OO Abusers, Change Preventers, Dispensables, Couplers
Extract MethodLong Method 해소의 기본
Extract ClassLarge Class의 책임 분리
매개변수 객체Data Clumps 정리
다형성switch 남용 대체
안전 사이클테스트 → 변경 → 테스트 → 반복

실무 적용 팁

  • 리팩토링은 매일의 작은 정리입니다. 큰 결단으로 미루지 마세요.
  • 테스트 안전망이 없는 코드라면 특성 테스트부터 확보 후 시작합니다.
  • 한 번의 변경은 작게, 커밋은 자주 해서 롤백 가능성을 확보합니다.
  • 코드 리뷰에서 발견한 스멜은 그 자리에서 작게 정리합니다.

영상으로 보기

이 글의 내용을 영상으로도 정리해 두었습니다.

유튜브 영상 보기

profile
공부하고 기록하고 공유하는 개발자 팀(Tim) 입니다. 늘끄적입니다.

0개의 댓글