Refactoring

정선용·2025년 4월 14일
0

Refactoring 개요

Refactoring (Martin Fowler)

  • 소프트웨어의 겉보기 동작은 그대로 유지한 채,
    코드를 이해하고 수정하기 쉽도록
    내부 구조를 변경

코드 변경의 주요 목적 - 기능구현과 리팩토링

  • 기능 구현 : 기능의 요구 사항에 집중
  • 리팩토링 : 코드의 가독성, 유지 보수성에 집중

리팩토링 목적

  • S/W 설계의 개선
  • S/W의 가독성의 개선
  • 버그를 쉽게 찾게 도와줌
  • 프로그래밍 속도를 높임

리팩토링을 해야할 때

The Rule of Three

처음에는 리팩토링 없이 하고, 비슷한 일을 세 번 이상 하게된다면 리팩토링 한다.

같은 로직/패턴이 3번 이상 반복되면 중복 제거하고 구조화하자는 의미

모든 코드에 대해 리팩토링할필요는 없고, 필요할 때 한다.

리팩토링 필요한 시점 유형

  • 준비과정에서의 리팩토링 (Preparatory Refactoring)
    • 코드 베이스에 기능을 새로 추가하기 직전에 수행
  • 이해를 위한 리팩토링 (Comprehension Refactoring)
    • 코드 변경 이전에 코드를 이해하기 쉽도록 코드 정리
  • 쓰레기 줍기 리팩토링 (Litter-Pickup Refactoring)
    • 비효율적으로 기능을 수행하는 코드
  • 계획된 리팩토링 (Planned Refactoring)
  • 오래 걸리는 리팩토링 (Long-Term Refactoring)
    • 전체 라이브러리 교체와 같은 대규모 리팩토링
    • 리팩토링 중에도 SW 가 정상 동작하도록 유념
  • 코드 리뷰 (Code Review)
    • 리팩토링을 통해, 한 차원 높은 아이디어나, 리뷰 결과를 더 구체적으로 도출(pair programming)

low quality code처럼 새로 작성하는게 더 쉬운 경우는 리팩토링을 포기한다.

리팩토링 고려사항

  • 기능 vs. 리팩토링 밸런스
    리팩토링 수행 여부의 판단 기준은 경제적 효과 : 기능개발 우선.
  • 코드 소유권 문제 (Code Ownership)
  • 브랜치 전략
    CI(지속적 통합)가 잘 되도록 작은 단위로 자주 통합 필요
  • 테스트 (Testing)
    리팩토링 후에도 기능이 동일함을 보장해야 함
    → 빠르고 자동화된 테스트 필요
  • 레거시 코드 (Legacy Code)
    테스트 없는 코드 → 리팩토링이 매우 어려움
    이런 경우, 먼저 테스트 코드 추가하고 → 그다음 리팩토링 수행
  • 데이터베이스 (Database)

리팩토링 vs 성능

  • 리팩토링을 통해 코드 구조가 명확해지면 병목을 찾는 것이 용이해져 성능 개선을 위한 발판이 될 수 있다.
  • 성능 문제는 10%의 코드에서 일반적으로, 발생하므로 모든 코드에 성능 고려하기보다 핵심 10%를 나중에 따로 다듬는 게 전략적으로 현명
  • 성능 비교와 프로파일링으로 리팩토링 범위와 방법을 결정하라

리팩토링 trade-off

리팩토링 기법들 또한 상충적 관계 존재

변수 추출하기 <-> 변수 인라인 하기
위임 숨기기 <-> 중개자 제거하기

리팩토링 절차

  • A series of small changes
    • 조금씩 개선, 단계를 수행해나감
  • slightly better, still leaving in working order
    • 겉보기 동작 그대로 유지

Step 1. 견고한 테스트 코드 준비

🔍 “기존 코드가 원래 어떻게 작동했는지를 보호할 안전망 만들기”

이 테스트는 리팩토링 후에도 동작이 동일함을 보장하는 기준이 되는 TC 작성

특히 엣지 케이스나 부작용 있는 로직을 확실히 커버할 수 있도록 준비해야 함

🧪 목표: 리팩토링 후에도 "원래 하던 일은 그대로 한다"는 것을 검증할 수 있게 만들기

Step 2. 코드 스멜(Code Smell) 탐지

🔍 “어디가 문제인지 직관적으로 냄새를 맡는 단계”

중복된 코드, 긴 함수, 과도한 책임, 네이밍 문제, 의존성 꼬임
→ 대표적인 Code Smell 들을 기준으로 문제점 탐색

이 단계는 "문제가 생기진 않았지만, 나중에 분명 터질 수 있는 구조"를 선제적으로 잡아내는 데 초점

🧪 목표: 리팩토링의 타겟과 이유를 명확히 설정

Step 3. 리팩토링 기법 적용

🔧 “문제에 맞는 리팩토링 도구를 꺼내어 적용하는 작업”

Extract Method, Rename, Replace Temp with Query, Introduce Parameter Object
→ 상황에 맞는 정형화된 리팩토링 패턴을 적용

성급하게 바꾸기보다 작은 단위로 천천히 개선하며 테스트 통과를 계속 확인

🧪 목표: 기능은 그대로, 구조만 개선

Step 4. 테스트 수행 (회귀 확인)

“리팩토링 후에도 기존 테스트가 모두 통과하는지 확인”

Step 1에서 만든 테스트가 여전히 모두 통과해야 리팩토링이 성공적

이때 실패가 발생하면 → 어떤 리팩토링이 기존 동작을 깨트렸는지 추적해야 함

🧪 목표: 구조를 개선했지만 기능은 동일함을 증명

Code Smell

"기능은 돌아가지만, 유지보수성과 확장성이 나쁜 코드의 이상 징후"

Heuristic한 도구이다.

Code Smells

우선순위Code Smell 예시비고
⭐️ 필수중복 코드, 긴 함수, 메시지 체인, 기능 편애 등실습/실무에서 자주 접함
🔍 중급가변 데이터, 거대한 클래스, 기본형 집착구조 고민이 필요한 시점에서 중요
🧠 심화추측성 일반화, 내부자 거래, 성의 없는 요소 등리팩토링 수준이 올라갔을 때 고려

1순위: 실무에서 자주 마주치는 Code Smells

Code Smell설명
중복 코드유지보수 최악의 적. 한 군데만 바꿔도 여러 곳 영향을 줌
긴 함수너무 많은 책임이 한 곳에 몰려 있음
기이한 이름코드를 읽어도 무슨 일인지 감이 안 옴
메시지 체인A().B().C().D() → 의존성 꼬임의 원인
산탄총 수술한 변경에 여러 파일, 클래스가 동시에 수정됨
기능 편애어떤 메서드가 다른 클래스의 데이터에 집착
데이터 뭉치늘 같이 다니는 변수 그룹 (→ 클래스로 묶을 수 있음)

✅ 2순위: 중급 이상에서 구조 리팩토링 시 고려할 요소

Code Smell설명
가변 데이터공유된 값이 쉽게 바뀔 수 있으면 위험 (side effect)
임시 필드어떤 조건에서만 쓰이는 필드 (→ 클래스 책임 모호)
중개자단순 전달만 하는 객체 (ex: getXXX().getYYY())
거대한 클래스책임 분리가 안 된 덩치 클래스
상속 포기자식 클래스가 부모 기능을 쓰지 않음
추측성 일반화쓰이지도 않을 기능을 미리 만들어둠
기본형 집착int, string 등 primitive로 모든 걸 처리하려는 습관

🧊 3순위: 상황에 따라 보는 고급 smells

Code Smell설명
반복문종종 map/filter/reduce 같은 고차 함수로 대체 가능
전역 데이터테스트 어려움, side effect 유발
다른 인터페이스의 대안 클래스들비슷한 기능인데 API 구조가 서로 달라 혼란 유발
성의 없는 요소더 이상 쓰이지 않지만 남아있는 클래스/함수
내부자 거래너무 깊게 남의 내부 구조를 엿봄
주석나쁜 코드의 핑계로 쓰이는 경우 많음 (좋은 코드라면 설명이 필요 없음)

→ 리팩토링 중 후속 개선을 위해 리뷰나 리팩터링 가이드라인에서 주로 언급되는 요소들

✅ 기이한 이름 (Mysterious Name)

🔍 증상 (Symptoms)

  • 함수/변수/필드 이름만 봐서는 무슨 역할인지 알 수 없음

🛠️ 리팩토링 방법 및 예시

Rename Function / Variable / Field

마땅한 이름이 떠오르지 않는다면, 더 근본적인 문제가 있을 가능성이 크다


✅ 중복 코드 (Duplicated Code)

🔍 증상 (Symptoms)

  • 동일한 코드 구조가 두 군데 이상 있을 때

🛠️ 리팩토링 방법 및 예시

① Extract Function (함수 추출하기)
  • 공통된 코드를 별도 함수로 추출하여 재사용
def render_welcome():
    print("Welcome")
    print("Enjoy your stay")

def render_goodbye():
    print("Goodbye")
    print("Enjoy your stay")

def render_footer():
    print("Enjoy your stay")

def render_welcome():
    print("Welcome")
    render_footer()

def render_goodbye():
    print("Goodbye")
    render_footer()
② Slide Statement (코드 이동하기)
  • 유사한 코드들의 위치를 정렬하여 중복을 명확히 식별 가능하게 함
def process_a():
    do_first()
    do_common()
    do_last()

def process_b(): #순서 변경에 영향 없다면 a,b프로세스 동일.
    do_common()
    do_first()
    do_last()
③ Extract Class
  • 중복된 기능이 여러 클래스에 존재할 경우 공통 클래스로 분리
class Order:
    def get_customer_info(self):
        return self.customer.name + self.customer.address

class Invoice:
    def get_customer_info(self):
        return self.customer.name + self.customer.address

class Customer:
    def get_info(self):
        return self.name + self.address

class Order:
    def get_customer_info(self):
        return self.customer.get_info()

class Invoice:
    def get_customer_info(self):
        return self.customer.get_info()
④ Pull Up Method (슈퍼클래스로 올리기)
  • 두 클래스에 동일한 메서드가 있을 경우 상위 클래스로 이동
class Dog:
    def speak(self):
        print("Woof!")

class Cat:
    def speak(self):
        print("Woof!")

class Animal:
    def speak(self):
        print("Woof!")

class Dog(Animal):
    pass

class Cat(Animal):
    pass

✅ 긴 함수 (Long Function)

🔍 증상 (Symptoms)

  • 하나의 함수가 너무 많은 일을 함 (20줄 이상이면 의심)

🛠️ 리팩토링 방법 및 예시

① Extract Function
  • 단계별로 책임을 나누어 명확한 함수로 분리
def calculate_total(order):
    total = 0
    for item in order.items:
        if item.category == "sale":
            total += item.price * 0.9
        else:
            total += item.price
    if order.customer.is_vip:
        total *= 0.95
    return total

def calculate_total(order):
    subtotal = calculate_subtotal(order.items)
    return apply_vip_discount(subtotal, order.customer)

def calculate_subtotal(items):
    return sum(apply_discount(item) for item in items)

def apply_discount(item):
    return item.price * 0.9 if item.category == "sale" else item.price

def apply_vip_discount(total, customer):
    return total * 0.95 if customer.is_vip else total
② Decompose Conditional (조건문 분해)
  • 복잡한 if-else나 switch문을 명확한 조건별 함수로 분리
if customer.is_vip:
    apply_vip_discount()
elif customer.is_new:
    apply_welcome_discount()

def get_discount_strategy(customer):
    if customer.is_vip:
        return apply_vip_discount
    if customer.is_new:
        return apply_welcome_discount
    return apply_standard_discount
③ Replace Temp with Query
  • 임시 변수 대신 메서드 호출로 대체하여 흐름 단순화
tax = order.total * 0.1
final = order.total + tax

def get_tax(order):
    return order.total * 0.1
final = order.total + get_tax(order)
④ Introduce Parameter Object
  • 많은 인자들을 묶어서 객체로 전달
def register_user(name, age, gender, email, phone): ...

def register_user(user_info):  # user_info: UserInfo 객체
    ...
⑤ 함수를 명령으로 바꾸기 (Replace Function with Command)

복잡한 계산 로직 → 클래스로 감싸서 run() 메서드 실행

ex) SalaryCalculator(employee).run()

✔️ 상태와 함수 분리, 테스트 용이

⑥ 조건문 → 다형성 (Replace Conditional with Polymorphism)

if-else가 타입/클래스에 따라 분기된다면 → 서브클래스로 분산

ex) if type == A: → class A(Employee)
동일 함수에 다른 로직이라면, 각 클래스에 정의된 같은 이름의 함수 호출로 분기 제거 가능. (클래스가 알아서 맞는 로직을 실행)

✔️ 조건문 제거, 책임 분리 명확

⑦ 반복문 쪼개기 (Split Loop)

하나의 for문에서 여러 일을 동시에 하고 있다면 분리

✔️ 목적별로 나누면 가독성·최적화에 유리


✅ 긴 매개변수 목록 (Long Parameter List)

🔍 증상 (Symptoms)

  • 함수나 메서드에 4개 이상 매개변수가 주르륵 연결돼 있을 때
  • 예: register(name, age, gender, email, phone, country, address, ... )

💣 문제점 (Problems)

  • 순서 실수 위험 증가
  • 테스트, 재사용 불편
  • 의미 전달 어려움
  • 호출부 가독성 저하 (노이즈 ↑)

🛠️ 리팩토링 기법 + 예시

① Introduce Parameter Object
  • 관련 있는 매개변수들을 하나의 클래스로 묶어 전달

Before

def register_user(name, age, gender, email, phone):
    ...

After

class UserInfo:
    def __init__(self, name, age, gender, email, phone):
        ...

def register_user(user_info: UserInfo):
    ...
② Preserve Whole Object
  • 매개변수가 어떤 객체에서 파생된 값이라면 객체 통째로 전달

Before

def apply_discount(price, customer_type, purchase_date):
    ...

After

def apply_discount(order):  # order 안에 모든 정보 포함
    ...
③ Replace Parameter with Method
  • 매개변수가 객체 내부 값이라면 getter로 대체

Before

def get_shipping_cost(weight):
    ...

After

def get_shipping_cost():
    return self.weight * 0.1
⑤ 🧩 플래그 인수 제거하기 (Remove Flag Argument)
  • true/false 같은 플래그 인자는 조건문 중첩을 만들고 함수 책임을 흐리게 함
    분기되는 경우는 아예 다른 함수로 분리

예시

# Before
def print_msg(is_error):
    if is_error:
        print("ERROR")
    else:
        print("OK")

# After
def print_error(): print("ERROR")
def print_ok(): print("OK")

⑥ 🧩 여러 함수를 클래스로 묶기 (Combine Functions into Class)
  • 여러 함수가 같은 데이터/맥락을 쓴다면 클래스에 묶어서 응집도 향상

예시

# Before
def login(): ...
def logout(): ...

# After
class Session:
    def login(self): ...
    def logout(self): ...

🧩 전역 데이터 (Global Data)

  • 어디서든 접근 가능한 데이터는 side effect 유발 위험이 큼
    캡슐화, 접근자(getter/setter) 제공, 의존성 주입(DI) 고려
Encapsulate Variables

예시

# Before
CONFIG = {}
CONFIG["lang"] = "ko"

# After
class Config:
    def __init__(self):
        self._lang = "ko"
    def set_lang(self, lang):
        self._lang = lang
    def get_lang(self):
        return self._lang

✅ 테스트 용이, 추적 가능, 변경 안전성 증가


🧩 가변 데이터 (Mutable Data)

  • 데이터가 의도하지 않은 곳에서 바뀌는 구조는 디버깅 어렵고 신뢰성 낮아짐
    → 캡슐화, 불변 구조화, 책임 분리 등으로 제어 필요

🔧 주요 원인 상황 & 대응 리팩토링

1. 여러 곳에서 직접 값을 바꾸는 경우
Encapsulate Variable

# Before
self.name = "sunyong"

# After
self._name = "sunyong"
def get_name(self): return self._name
def set_name(self, name): self._name = name

2. 같은 값을 여러 함수에서 계산하거나 조회/변경을 함께 하는 경우
Separate Query from Modifier

3. 데이터 역할이 섞여 명확하지 않을 경우
Extract Function, Slide Statement 등으로 기능 분리

4. 하나의 변수에 다양한 역할이 섞여 있을 경우
Split Variable

5. 생성자 외의 setter 메서드로 변경되는 경우
Remove Setting Method

6. 파생값을 필드로 유지 중이라면?
Replace Derived Variable with Query

📦 구조적 대응 전략

✅ 관련 동작이 분산된 경우
Combine Functions into Class (응집도 향상)
Combine Functions into Transform (변환 로직 통합)

✅ 참조보다는 복사가 안전한 경우
Change Reference to Value

✅ 예시 요약

# Before
profile = {"name": "sunyong"}
profile["name"] = "changed"

# After
class Profile:
    def __init__(self, name):
        self._name = name
    def get_name(self):
        return self._name
    # set_name은 의도적으로 제공하지 않음 → 불변 설계

데이터 흐름을 명확히 하고, 의도한 방식으로만 변경되게 만듬


🧩 뒤엉킨 변경 (Divergent Change)

  • 하나의 모듈이 서로 다른 이유로 인해 여러 가지 방식으로 변경되는 일이 많은 경우 (단일 책임 위반) : Responsibility 여러가지가 한가지에 <-> shotgun surgery (한 Responsibility가 여러 흩어져있음)
    기능별로 클래스 분리 (Split Class)

예시

# Before
class Report:
    def render(): ...
    def send_email(): ...
    def archive(): ...

# After
class ReportRenderer: ...
class ReportMailer: ...
class ReportArchiver: ...

✅ 변경에 유연한 구조, 각 기능에 집중


✅ 산탄총 수술 (Shotgun Surgery)

🔍 증상 (Symptoms)

  • 작은 변경에도 여러 클래스/함수를 수정해야 하는 구조 : Responsibility가 여러 모듈에 흩어져 있는 경우

🛠️ 리팩토링 방법 및 예시

Move Method + Combine Functions into Class
# 각기 다른 클래스에서 중복된 할인 정책을 갖고 있음
class Order:
    def apply_discount(self):
        return self.amount * 0.95

class Invoice:
    def apply_discount(self):
        return self.amount * 0.95

class DiscountPolicy:
    def apply(self, amount, customer):
        return amount * 0.95 if customer.is_vip else amount

# 사용처에서는 하나의 정책 클래스만 호출
final_price = DiscountPolicy().apply(order.amount, order.customer)
Encapsulate
이후 후속 리팩토링
  • 여러 함수 → 변환 함수로 묶기 (Combine Functions into Transform)
  • 함수 인라인하기 (Inline Function)
  • 클래스 인라인하기 (Inline Class)

✅ 기능 편애 (Feature Envy)

🔍 증상 (Symptoms)

  • 어떤 함수가 자신의 속한 모듈보다, 다른 모듈의 함수나 데이터와 상호작용이 더 많음. 그 메서드는 거기 말고, 다른 클래스에 있는 게 더 자연스럽지 않나? 하는 상황
    → 응집도 떨어짐, 책임 분산(데이터-동작 떨어져있어 파악/유지보수 힘듬), 코드이동 자주 생김

🛠️ 리팩토링 방법 및 예시

Move Function
def total_price(order): #order클래스에 있지 않으나 order의 데이터만으로 연산.
    return order.quantity * order.unit_price

class Order:
    def total_price(self):
        return self.quantity * self.unit_price
Hide Delegate : 중간 객체에 대신 접근 메서드 제공
zip_code = order.get_customer().get_address().get_zip_code()

# After
zip_code = order.get_zip_code()

# 내부 구현
class Order:
    def get_zip_code(self):
        return self.customer.address.zip_code

🧩 데이터 뭉치 (Data Clumps)

  • 항상 같이 등장하는 변수 묶음은 하나의 개념일 가능성 ↑
    클래스나 객체로 묶어 응집력 강화

🔧 리팩토링 기법

  • Introduce Parameter Object – 관련 인자 하나로 묶기
  • Extract Class – 함께 다니는 필드를 별도 클래스로 추출

🧩 기본형 집착 (Primitive Obsession)

  • int, string, bool 같은 기본 자료형만으로 모든 데이터를 표현하려는 습관
    → 데이터의 의미와 책임이 불분명해지고, 구조가 약해짐
  • 전화번호, 이메일, 좌표 등 → 전부 string으로 처리
  • 금액, 날짜, 범위, 상태값 등 → 전부 intbool로 처리
  • 관련 값 여러 개가 유지보수 없이 흩어져 사용됨

💥 문제점

  • 의미 불분명 → 실수 발생 쉬움
  • 데이터와 동작 분리 → 응집도 낮음
  • 중복된 유효성 검사 로직 → 재사용 어려움

🛠️ 리팩토링 기법

  • Introduce Value Object
    값을 나타내는 클래스를 도입해서, 의미 있는 타입으로 치환

  • Replace Primitive with Object
    string, intMoney, Email, PhoneNumber 등 구체적 클래스화


✅ 예시 Replace Primitive with Object

# Before
def register(name: str, email: str): ...

# After
class Email:
    def __init__(self, value):
        if '@' not in value:
            raise ValueError("Invalid email")
        self.value = value

def register(name: str, email: Email): ...

데이터의 의미, 검증, 동작을 하나의 타입으로 응집

✅ 예시2 Replace Type Code with Subclasses + Replace Conditional with Polymorphism

  • 조건문이 type, category, role 같은 문자열/숫자 값(primitive)에 따라 동작을 분기할 때
    조건을 없애고, 각 타입을 서브클래스로 분리하여 다형성으로 책임 분산
#Before
class Bird:
    def get_speed(self):
        if self.type == "European":
            return 10
        elif self.type == "African":
            return 5
  • self.type 값에 따라 분기됨 → 타입 코드 사용
  • 조건문이 많아질수록 복잡도, 유지보수성, 안정성 모두 낮아짐
🛠️ 리팩토링 : Replace Type Code with Subclasses + Replace Conditional with Polymorphism
  • "European", "African" 같은 문자열 type → 클래스 자체로 분리
  • Bird 클래스를 상속받는 서브클래스를 만들어 분기 제거
  • 조건문으로 동작을 분기하는 대신
    각 서브클래스가 적절한 메서드를 override
class Bird:
    def get_speed(self):
        raise NotImplementedError()

class European(Bird):
    def get_speed(self):
        return 10

class African(Bird):
    def get_speed(self):
        return 5
  • 조건문 없이 역할에 따라 다형적으로 동작
  • 새로운 타입이 생겨도 기존 코드 수정 없이 확장 가능 (OCP 만족)
  • 테스트도 타입별로 분리되어 간결

🧩 반복되는 Switch문 (Repeated Switch Statements)

  • 같은 조건 분기(if, switch)가 여러 곳에 반복됨
    → 유지보수 어려움, 새 조건 추가 시 누락 위험

🔧 리팩토링 기법

  • Replace Conditional with Polymorphism
    → 조건문 대신 클래스 다형성 활용
  • 전략(Strategy) 패턴도 적용 가능

🧩 반복문 (Loops)

  • 목적보다 절차 중심으로 작성된 반복문은 가독성 ↓, 실수 ↑
    → 선언형 스타일로 리팩토링 권장

🔧 리팩토링 기법

  • Replace Loop with Pipeline – map/filter/reduce, 리스트 컴프리헨션 등
  • Extract Function – 반복문 내부 작업을 목적별 함수로 분리

✅ 예시

# Before
names = []
for p in people:
  if p.job == “programmer”:
    names.append(p.name)

# After
names = list(.filter(lamda p : p.job == programmer", people)

🧩 성의 없는 요소 (Lazy Element)

  • 역할이 불분명하거나, 거의 사용되지 않는 클래스/메서드/필드
    → 코드 복잡도만 증가시키는 쓸모없는 구조

🛠️ 리팩토링 기법

  • Inline Function – 너무 단순한 함수는 인라인 처리
  • Collapse Hierarchy – 의미 없는 상속 구조는 통합
  • Remove Dead Code – 쓰이지 않는 코드 제거

✅ 예시

# Before
class NameFormatter:
    def format(self, user):
        return user.name
# After
# 그냥 user.name 직접 사용
# Before
class Animal: pass

class Dog(Animal):
    def bark(self): print("Woof")

# After
class Dog:
    def bark(self): print("Woof")

✅ 기능이 없는 상위 클래스는 제거하여 계층 단순화


🧩 추측성 일반화 (Speculative Generality)

  • 혹시 해서 만들어 둔 사용되지 않는 추상 클래스, 인터페이스, 훅 메서드, 매개변수, 설정 등이 코드에 남아 있는 경우

🛠️ 리팩토링 기법

  • 제거한다. (Collapse Hierarchy,Remove Dead Code,Inline Class / Inline Function)

🧩 임시 필드 (Temporary Field)

  • 어떤 필드가 항상 사용되지 않고,
    → 특정 조건, 특정 메서드에서만 사용
    문제 : 클래스 책임이 모호해지고, 불필요한 필드
    있을 수도 있고, 없을 수도 있는 상태가 자주 발생(None체크 多), 유지보수 시 버그 가능성↑

🛠️ 리팩토링 기법

Extract Class , Move Function + Move Field
→ 특정 상황에서만 쓰이는 필드를 담당하는 클래스를 따로 분리 / 그 필드가 더 적합한 객체로 책임 이
Introduce Null Object
→ 빈 동작/기본 동작을 하는 객체를 만들어 null 분기 제거

✅ 예시

# Before
class Booking:
    def __init__(self, customer, is_premium):
        self.customer = customer
        if is_premium:
            self.special_discount = 0.1
        else:
            self.special_discount = None

→ special_discount는 프리미엄 고객만 쓰는 필드
→ 일반 고객에겐 항상 None


# After1
class Booking:
    def __init__(self, customer):
        self.customer = customer

class PremiumBooking(Booking):
    def __init__(self, customer):
        super().__init__(customer)
        self.special_discount = 0.1

→ 프리미엄 고객만 사용하는 필드는 서브클래스로 분리

Introduce Null Object

# Before
class Customer:
    def __init__(self, name):
        self.name = name

class Booking:
    def __init__(self, customer=None):
        self.customer = customer

    def get_customer_name(self):
        if self.customer is None:
            return "Guest"
        return self.customer.name
        
# After
class NullCustomer:
    @property
    def name(self):
        return "Guest"

class Customer:
    def __init__(self, name):
        self.name = name

class Booking:
    def __init__(self, customer):
        self.customer = customer or NullCustomer()

    def get_customer_name(self):
        return self.customer.name

🧩 메시지 체인 (Message Chain)

  • 메서드/속성 호출이 ., ., . 식으로 여러 단계로 이어지는 체인 형태
    → 내부 구조에 대한 강한 의존성 발생
  • 구조가 바뀌면 호출부 전부 수정해야 함
  • 구조를 아는 호출자 = 캡슐화 위반

💥 문제점

  • 중간 구조 변경에 호출자 전부 영향
  • 내부 객체까지 외부에 노출됨
  • 코드 읽는 사람이 구조를 너무 자세히 알아야 함

🛠️ 리팩토링 기법

  • Hide Delegate – 대신 호출해주는 위임 메서드 제공
  • Move Method – 더 적절한 객체로 기능을 옮김

✅ 예시

# Before
zip_code = order.get_customer().get_address().get_zip_code()

# After
class Order:
    def get_zip_code(self):
        return self.customer.address.zip_code

zip_code = order.get_zip_code()

✅ 내부 구조 은닉 + 호출부 간결화

💡 기능 편애와의 차이?

항목기능 편애 (Feature Envy)메시지 체인 (Message Chain)
문제의 본질메서드가 다른 클래스의 데이터에 집착호출자가 내부 구조를 너무 깊게 탐색
예시price = order.customer.discount_rate * 100order.get_customer().get_address().get_zip_code()
주체문제 있는 건 메서드의 위치 (옮겨야 함)문제 있는 건 호출 방식 (너무 탐색적임)
주 리팩토링Move Method, 때때로 Hide DelegateHide Delegate, Move Method (간혹)
Hide Delegate의 역할데이터에 집착하는 대신, 간접 접근 경로만 제공체인 끊고 구조 은닉, 호출부 정리

🧩 중개자 (Middle Man)

  • 어떤 클래스의 절반 이상 메서드가 다른 객체에 위임만 하는 경우
    → 실질적인 기능이 없고, 호출만 전달하는 쓸모없는 중간 계층

🛠️ 리팩토링 기법

✅ ① Remove Middle Man (중개자 제거하기)
  • 위임만 하는 메서드를 제거하고,
  • 호출자가 직접 진짜 객체에 접근
# Before
class Order:
    def get_customer_name(self):
        return self.customer.get_name()

order.get_customer_name()

# After
order.customer.get_name()
Hide Delegate와 반대 상황!
Hide DelegateMiddle Man
호출자가 너무 구조를 따라감 → 위임시켜서 캡슐화 강화위임만 하고 있는 메서드가 쓸모 없음
캡슐화를 위해 위임을 만드는 리팩토링위임이 불필요하면 과감히 제거
"숨기는 위임""제거하는 위임"
✅ ② Inline Function (함수 인라인하기)
  • 위임만 하는 함수는 호출부에 직접 삽입 or 함수제거
✅ ③ Replace Delegation with Inheritance (위임을 상속으로 바꾸기)
  • 위임 외에도 약간의 기능이 있는 경우,
  • 위임 객체를 상속 구조로 바꿔서 서브클래스로 전환
# Before
class Manager:
    def __init__(self):
        self.employee = Employee()

    def get_salary(self):
        return self.employee.get_salary()

# After
class Manager(Employee):  # 상속으로 위임 제거
    pass

Proxy, Decorator, Adapter 패턴처럼
기능이나 제어 흐름 목적이 있다면 중개자 유지하는 게 맞음


🧩 내부자 거래 (Insider Trading)

클래스 간 결합도(coupling)가 지나치게 높아진 상태. 구조가 취약해짐

💥 문제점

문제설명
📉 캡슐화 깨짐객체 내부 구조가 외부에 노출됨
🔁 변경 전파하나 수정 시 여러 클래스 수정 필요
🧪 테스트 어려움강한 의존 때문에 단위 테스트 어려움
🔀 책임 모호행동을 누가 책임져야 하는지 불분명

✅ 예시 1: 내부 구조 과도한 접근

# ❌ Before
class Account:
    def __init__(self, transactions):
        self.transactions = transactions

class BankSystem:
    def calculate_total(account):
        total = 0
        for t in account.transactions:
            if t.type == "deposit":
                total += t.amount
        return total

여기서 BankSystem은 Account의 내부 자료구조 (transactions)와 그 내부 구조 (type, amount)에 깊게 의존

앞으로 transactions 구조나 필드가 바뀌면 BankSystem도 다 같이 터짐
→ 💥 내부 구조가 외부 시스템에 직접 노출되고 조작됨
→ 따라서 캡슐화가 완전히 깨짐
→ = 내부자 거래

# ✅ After
class Account:
    def __init__(self, transactions):
        self.transactions = transactions

    def total_deposits(self):
        return sum(t.amount for t in self.transactions if t.type == "deposit")

class BankSystem:
    def calculate_total(account):
        return account.total_deposits()

✅ 이제 BankSystem"total_deposits라는 기능"만 호출
→ 내부 구조는 몰라도 됨 → 캡슐화 성공

✅ 예시 2: 상속 관계의 결합 → 위임으로 리팩토링

# ❌ Before (강한 상속 구조)
class Report:
    def content(self):
        return "Base content"

class ConfidentialReport(Report):
    def content(self):
        return "*** CONFIDENTIAL ***
" + super().content()

→ 부모 구조에 깊이 의존하고 있어 유연하지 않음

# ✅ After (위임 구조)
class Report:
    def content(self):
        return "Base content"

class ConfidentialWrapper:
    def __init__(self, report):
        self.report = report

    def content(self):
        return "*** CONFIDENTIAL ***
" + self.report.content()

→ 위임 구조로 바꾸면 결합도는 낮추고 유연성은 증가

🔧 주요 리팩토링 기법

리팩토링설명
Move Method , Move Field
Extract Class얽혀 있는 로직이 많다면 새 클래스로 분리
Hide Delegate중간 구조를 감추고 필요한 기능만 노출
Replace Subclass with Delegation상속 관계를 위임 구조로 바꿔 유연성 확보

🧠 비교: 유사 스멜과의 차이

✅ 1. 공통점 – 셋 다 “캡슐화 깨짐”과 관련된 스멜
공통 키워드설명
❗ 캡슐화 위반객체 내부의 구조나 데이터를 외부에서 과도하게 조작함
🔄 책임 분리 실패특정 로직의 책임이 애매한 위치에 있음
📉 구조적 결합도 ↑하나 바꾸면 다른 데도 연쇄적으로 영향을 받음
💡 목표는 같음각 객체가 “자기 일만 하게 만들자!”
✅ 2. 차이점 – 무엇이 문제고 어디서 발생하는가?
항목메시지 체인 (Message Chain)기능 편애 (Feature Envy)내부자 거래 (Insider Trading)
핵심 문제구조를 너무 따라감메서드가 남의 데이터만 신경 씀두 객체가 서로 너무 많이 앎
대표 증상a.b.c.d() 같이 체인이 길어짐this는 일 안 하고, other의 속성만 씀한쪽이 다른 쪽 구조/행동을 깊게 침투
관계 구조객체를 탐색하는 코드잘못 위치한 메서드클래스 간 결합도 자체가 높음
해결 방식Hide Delegate, Move MethodMove Method, Extract MethodMove Method, Extract Class, Delegate
목표구조 숨기기메서드 위치 바로잡기결합도 낮추고 역할 재배분

🧩 거대한 클래스 (Large Class)

\하나의 클래스가 너무 많은 책임을 가지는 상태
→ 메서드, 필드, 기능이 과도하게 몰려 있어 SRP(단일 책임 원칙)을 위반함
변경 취약, 독립적 테스트 어려움, 재사용성 낮음

🧪 예시 (Extract Class 중심)

# ❌ Before
class Employee:
    def __init__(self, name, age, salary, performance_reviews):
        self.name = name
        self.age = age
        self.salary = salary
        self.performance_reviews = performance_reviews

    def calculate_bonus(self):
        ...

    def save_to_database(self):
        ...

    def render_employee_card(self):
        ...

Employee가 급여 계산, DB 저장,UI 렌더링까지…
→ 역할이 뒤죽박죽

# ✅ After
class Employee:
    def __init__(self, name, age, salary, reviews):
        ...

    def calculate_bonus(self):
        ...

class EmployeeRepository:
    def save(employee):
        ...

class EmployeeCardRenderer:
    def render(employee):
        ...

Extract Class를 통해 데이터 처리, DB 저장, UI 렌더링을 역할별로 분리

🛠️ 리팩토링 기법

기법설명
Extract Class한 클래스에 몰린 책임을 별도의 클래스로 나눔
Extract Superclass비슷한 클래스들 간 공통 기능을 상위 클래스로 이동
Replace Type Code with Subclasses타입 구분 필드를 클래스로 분리해 구조화

🧪 예시2 (Replace Type Code with Subclasses)

# ❌ Before
class Employee:
    def __init__(self, name, emp_type):
        self.name = name
        self.type = emp_type  # "ENGINEER", "MANAGER", "SALESPERSON"

    def get_bonus(self):
        if self.type == "ENGINEER":
            return 1000
        elif self.type == "MANAGER":
            return 2000
        elif self.type == "SALESPERSON":
            return 1500

타입 코드로 분기하게 되면 왜 거대한 클래스일 수 있는가?

기준설명
📌 책임이 많음타입별 보너스 로직을 모두 Employee가 떠맡고 있음
📌 조건문이 반복됨type 코드가 바뀔 때마다 if self.type == ... 패턴이 증가
📌 응집도 낮음각 조건문은 서로 관련 없음. 사실상 세 가지 다른 동작
📌 확장에 불리함새로운 타입이 생기면 Employee를 계속 고쳐야 함 (OCP 위반)

→ 하나의 Employee 클래스가 너무 많은 책임을 떠안고 있어 결국 필요보다 거대한 클래스로 진화하게됨.

# ✅ After
class Employee:
    def __init__(self, name):
        self.name = name

    def get_bonus(self):
        raise NotImplementedError()

class Engineer(Employee):
    def get_bonus(self):
        return 1000

class Manager(Employee):
    def get_bonus(self):
        return 2000

class Salesperson(Employee):
    def get_bonus(self):
        return 1500

→ 조건문 제거 → 서브클래스가 책임을 개별적으로 가짐 → 유지보수 & 확장성 향상


🧩 다른 인터페이스의 대안 클래스들 (Alternative Classes with Different Interfaces)

같은 기능을 수행하지만 인터페이스가 달라서
서로 바꿔 끼우기 어렵고 일관성이 없는 클래스들이 존재할 때 발생하는 스멜

🧨 예시

# ❌ Before
class ImageDownloader:
    def download(self, url):
        ...

class PictureFetcher:
    def fetch(self, uri):
        ...
# ✅ After
class ImageSource(ABC):
    def fetch_image(self, path):
        ...

class ImageDownloader(ImageSource):
    def fetch_image(self, path):
        ...

class PictureFetcher(ImageSource):
    def fetch_image(self, path):
        ...

→ 인터페이스 통합으로 교체 가능성과 일관성 확보

🛠️ 리팩토링 기법

기법설명
함수 선언 바꾸기 (Change Function Declaration), 옮기기공통된 메서드 구조로 통합(unify interface)
Extract Superclass상위 클래스나 인터페이스로 정리
Adapter Pattern기존 구조를 유지하되 어댑터로 감싸 통일

🧩 데이터 클래스 (Data Class)

필드(데이터)만 있고, 동작(메서드)가 거의 없는 클래스(책임x)
실제 로직은 다른 클래스에서 이 데이터를 가져와 처리
→ 데이터와 동작이 분리

🛠️ 리팩토링 기법

기법설명
Encapsulate Field필드를 직접 노출하지 말고 캡슐화
Move Function데이터를 사용하는 함수는 그 데이터를 가진 객체로 옮기기

🧩 상속 포기 (Refused Bequest)

형식적 상속은 있지만 실제 기능적 상속은 없는 상태( 실제 상속된 필드나 메서드를 거의 사용하지 않거나 의미를 왜곡하거나 오버라이드해서 깨트리는 경우)

  • “A는 B다” 관계가 성립하지 않음
    설계 오류 위험 객체지향 원칙 위배 (SRP, OCP 등)

🛠️ 리팩토링 기법

상황리팩토링
일부 자식만 사용하는 기능Push Down Method/Field
자식이 인터페이스 따르지 않음Replace Superclass with Delegation
공통 로직만 뽑고 싶을 때Extract Superclass

✅ 1. Push Down Method / Push Down Field

💡 언제?
  • 부모의 메서드나 필드를 일부 자식만 사용하는 경우
📌 Before
class Bird:
    def fly(self): ...
    def swim(self): ...

class Eagle(Bird): pass
class Penguin(Bird):
    def fly(self): raise Exception("Penguin can't fly")
📌 After
class Bird:
    def fly(self): ...

class Eagle(Bird): pass

class Penguin:
    def swim(self): ...

✅ Penguin은 더 이상 Bird가 아님 → 구조 명확화

✅ 2. Replace Superclass with Delegation

💡 언제?
  • 자식 클래스가 부모의 일부 기능만 필요할 때
  • 전체 인터페이스를 따르기는 어색한 경우
📌 Before
class DataStructure:
    def add(self): ...
    def remove(self): ...

class Stack(DataStructure):
    def add(self): ...
    def remove(self): ...
📌 After
class Stack:
    def __init__(self):
        self._internal = DataStructure()

    def push(self): return self._internal.add()
    def pop(self): return self._internal.remove()

✅ 상속보다 유연하며, 의미에 맞는 구조로 전환

profile
정선용

0개의 댓글