티스토리 뷰

CS/Design Pattern

리팩토링

minsu20 2024. 6. 10. 19:02

리팩토링이란?

시스템의 외부 행위는 바뀌지 않고 기능적 요구는 유지한 상태로 시스템의 내부 구조를 개선하는 것이다. 즉, 프로그램이 작성된 후에 설계를 개선하는 것이다. 리팩토링은 처음 한번에 설계가 올바로 되기 어렵다는 것과 프로그램의 요구가 바뀌면 설게도 계속 바뀌어야 한다는 이해가 깔려 있다. 따라서, 설계를 조금식 점증적으로 전환시키는 기술을 제공하는 것이다. 코드의 크기가 줄어들고 혼란스러운 구조가 단순한 구조로 바뀐다는 장점이 있다.

 

Bad Smell 코드

1. 기능 산재 (Shotgun Surgery)

변경할 때마다 여러 다른 클래스를 수정해야 하는 경우이다. 예를 들어, 로깅 시스템이 여러 클래스에 걸쳐 구현되어 있어 로깅 방식을 변경할 때마다 모든 관련 클래스를 수정해야 하는 상황이 이에 해당한다.

class Account {
    void withdraw(float amount) {
        // withdraw 로직
        System.out.println("Withdraw: " + amount); // 로깅
    }

    void deposit(float amount) {
        // deposit 로직
        System.out.println("Deposit: " + amount); // 로깅
    }
}

class TransferService {
    void transfer(Account from, Account to, float amount) {
        from.withdraw(amount);
        to.deposit(amount);
        System.out.println("Transfer from " + from + " to " + to + ": " + amount); // 로깅
    }
}

리팩토링: 로깅 기능을 하나의 클래스로 중앙 집중화하여 변경 사항을 한 곳에서만 관리할 수 있도록 수정

class Logger {
    static void log(String message) {
        System.out.println(message); // 모든 로깅을 이 메소드를 통해 처리
    }
}

class Account {
    void withdraw(float amount) {
        // withdraw 로직
        Logger.log("Withdraw: " + amount); // 로깅 중앙화
    }

    void deposit(float amount) {
        // deposit 로직
        Logger.log("Deposit: " + amount); // 로깅 중앙화
    }
}

class TransferService {
    void transfer(Account from, Account to, float amount) {
        from.withdraw(amount);
        to.deposit(amount);
        Logger.log("Transfer from " + from + " to " + to + ": " + amount); // 로깅 중앙화
    }
}

 

2. 잘못된 소속(Feature Envy)

메서드가 다른 클래스의 데이터나 메서드에 지나치게 의존하고 있을 때 발생한다. 이는 메서드가 그 데이터의 "진정한" 위치에 있지 않다는 신호일 수 있다.

class AccountData {
    private String accountNumber;
    private double balance;

    String getAccountNumber() { return accountNumber; }
    double getBalance() { return balance; }
}

class AccountService {
    void printAccountDetails(AccountData accountData) {
        System.out.println("Account Number: " + accountData.getAccountNumber() +
                           ", Balance: " + accountData.getBalance());
    }
}

리팩토링: printAccountDetails 메서드를 AccountData 클래스로 이동

class AccountData {
    private String accountNumber;
    private double balance;

    String getAccountNumber() { return accountNumber; }
    double getBalance() { return balance; }

    void printAccountDetails() {
        System.out.println("Account Number: " + getAccountNumber() +
                           ", Balance: " + getBalance());
    }
}

class AccountService {
    // AccountService는 이제 printAccountDetails를 호출할 필요가 없습니다.
}

 

3. 데이터 뭉치(Data Clumps)

여러 클래스에서 동일한 데이터 뭉치(필드 그룹)가 반복되는 경우, 이 데이터들은 아마도 별도의 클래스로 분리되어야 할 것이다. 

class Order {
    private String customerName;
    private String customerEmail;
    private String customerAddress;

    // 여러 메서드에서 위 세 필드를 동시에 사용
}

class CustomerService {
    void createCustomer(String name, String email, String address) {
        // 고객 생성 로직
    }
}

리팩토링: 고객 정보를 별도의 클래스로 추출

class Customer {
    private String name;
    private String email;
    private String address;

    Customer(String name, String email, String address) {
        this.name = name;
        this.email = email;
        this.address = address;
    }
}

class Order {
    private Customer customer;

    // Order 클래스는 이제 Customer 객체를 사용
}

class CustomerService {
    void createCustomer(Customer customer) {
        // 고객 생성 로직
    }
}

4. 강박적 기본 타입 사용(Primitive Obsession)

객체 지향 프로그래밍에서 기본 데이터 타입을 과도하게 사용하는 경향을 말한다. 종종, 몇 개의 관련된 데이터가 함께 묶여 클래스로 표현될 수 있음에도 불구하고, 개별적인 기본 타입으로 처리된다.

class Order {
    private String customerName; // 강박적 기본 타입 사용
    private String customerAddress;
    private String customerCity;
    private String customerCountry;
}

class Address {
    String address;
    String city;
    String country;
}
class Customer {
    private Address address;
}

class Address {
    private String streetAddress;
    private String city;
    private String country;

    public Address(String streetAddress, String city, String country) {
        this.streetAddress = streetAddress;
        this.city = city;
        this.country = country;
    }
}

 

5. Switch 문장

Switch 문장은 대체로 코드에 중복을 가져온다. 다형성을 사용하여 변경할 수 있다.

class Animal {
    private String type;

    public Animal(String type) {
        this.type = type;
    }

    public void makeSound() {
        switch (type) {
            case "dog":
                System.out.println("Bark");
                break;
            case "cat":
                System.out.println("Meow");
                break;
            case "duck":
                System.out.println("Quack");
                break;
            default:
                System.out.println("Unknown animal sound");
        }
    }
}

 

interface Animal {
    void makeSound();
}

class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark");
    }
}

class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow");
    }
}

class Duck implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Quack");
    }
}

 

public class Main {
    public static void main(String[] args) {
        List<Animal> animals = Arrays.asList(new Dog(), new Cat(), new Duck());
        for (Animal animal : animals) {
            animal.makeSound();
        }
    }
}


6. 평행 상속 계층(Parallel Inheritance Hierarchies)

기능 산재와 유사하다. 한 클래스를 상속하여 서브클래스를 만들 때마다, 다른 클래스도 상속하여 새로운 서브클래스를 만들어야 하는 구조이다. 코드의 변경을 어렵게 만들 수 있다. 따라서, 상속보다는 컴포지션을 사용하거나 다른 디자인 패턴을 적용해 상속 계층을 줄인다. (물론 어떤 디자인 패턴은 평행 상속 계층의 사용을 권장한다.)

 

7. 직무 유기 클래스(Lazy Class)

리팩토링으로 인해 다운사이즈 된 크래스나 다른 것을 호출하지 않는 계획된 기능을 나타내 더 이상 기능을 제공하지 않는 클래스이다. 따라서 이런 경우에는 클래스와 메서드를 그냥 제거한다. 


8. 막연한 범용 코드(Speculative Generality)

미래의 잠재적 사용을 위해 추가된 코드, 클래스, 인터페이스 등이 실제로 사용되지 않는 경우이다. 실제로 사용되지 않는 메서드, 클래스는 제거한다.


9. 임시 필드(Temporary Field)

객체의 일부 필드가 특정 상황에서만 사용되는 경우이다. 이는 클래스가 항상 필요로 하지 않는 속성을 갖게 하여, 클래스의 상태를 이해하기 어렵게 만든다.

class OrderProcessor {
    private float discountRate; // 일부 주문 처리시에만 사용됨

    void processOrder(Order order, boolean isDiscounted) {
        if (isDiscounted) {
            discountRate = 0.05f;
            applyDiscount(order, discountRate);
        }
    }
}

리팩토링: 임시 필드를 가지는 클래스를 리팩토링하여, 필드를 사용하는 행동을 전용 클래스로 분리

class OrderProcessor {
    void processOrder(Order order, Discount discount) {
        if (discount != null) {
            discount.apply(order);
        }
    }
}

class Discount {
    private float rate;

    public Discount(float rate) {
        this.rate = rate;
    }

    void apply(Order order) {
        // 할인 적용 로직
    }
}

10. 메시지 체인

클라리언트가 다른 객체를 위하여 객체를 요청하고 또한 그 객체를 다른 객체를 위하여 요청한다. 즉, 객체 요청이 연쇄적으로 이어져 객체의 내부 구조에 대한 종속성이 증가하는 상황이다. 이는 데메테르의 법칙을 위반하며, 한 객체의 변경이 연쇄적인 수정을 요구할 수 있다.

class Customer {
    Account account;
    public Account getAccount() { return account; }
}

class Account {
    Profile profile;
    public Profile getProfile() { return profile; }
}

class Profile {
    String name;
    public String getName() { return name; }
}

// 사용 예:
String customerName = customer.getAccount().getProfile().getName();
class Customer {
    private Account account;
    public String getCustomerName() {
        return account.getProfileName();
    }
}

class Account {
    private Profile profile;
    public String getProfileName() {
        return profile.getName();
    }
}

// 사용 예:
String customerName = customer.getCustomerName();

11. 과잉 중개 메서드 (Middle Man)

반 이상의 책임을 다른 클래스에 위임하는 경우 과연 클래스인가?? 데코레이터와 같은 디자인 패턴으로 조정한다.

 

class Processor {
    public void process() {
        System.out.println("Processing data...");
    }
}

class Handler {
    private Processor processor;

    public Handler(Processor processor) {
        this.processor = processor;
    }

    public void handle() {
        processor.process();  // 단순히 Processor의 메소드를 호출
    }
}

class Client {
    public static void main(String[] args) {
        Processor processor = new Processor();
        Handler handler = new Handler(processor);
        handler.handle();  // Client는 Handler를 통해 Processor를 사용
    }
}

Handler 클래스가 하는 일이 거의 없으므로, 이 클래스를 제거하고 클라이언트가 직접 Processor 클래스를 사용하도록 할 수 있다.

class Processor {
    public void process() {
        System.out.println("Processing data...");
    }
}

class Client {
    public static void main(String[] args) {
        Processor processor = new Processor();
        processor.process();  // Client가 직접 Processor를 사용
    }
}

또는, 데코레이터 패턴을 사용하여, Handler 클래스가 추가적인 기능을 Processor에 제공하는 방식으로 리팩토링

interface IProcessor {
    void process();
}

class Processor implements IProcessor {
    @Override
    public void process() {
        System.out.println("Processing data...");
    }
}

class ProcessorDecorator implements IProcessor {
    private IProcessor wrappedProcessor;

    public ProcessorDecorator(IProcessor processor) {
        this.wrappedProcessor = processor;
    }

    @Override
    public void process() {
        System.out.println("Additional behavior before processing");
        wrappedProcessor.process();
        System.out.println("Additional behavior after processing");
    }
}

class Client {
    public static void main(String[] args) {
        IProcessor processor = new Processor();
        IProcessor decoratedProcessor = new ProcessorDecorator(processor);
        decoratedProcessor.process();  // Client uses the decorated processor
    }
}

12. 지나친 관여 (Inappropriate Intimacy)

서로 다른 클래스가 서로의 내부 구현에 너무 많이 관여하는 경우이다. 이는 클래스의 캡슐화가 약화되는 원인이 된다.

class Department {
    private List<Employee> employees;

    public void addEmployee(Employee employee) {
        employees.add(employee);
    }

    public double getTotalSalary() {
        double total = 0;
        for (Employee employee : employees) {
            total += employee.getSalary();  // 직접 Employee의 salary 필드에 접근
        }
        return total;
    }
}

class Employee {
    private double salary;

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }
}

리팩토링 : 캡슐화를 강화하고, 각 클래스의 책임을 명확히 분리하여 지나친 관여를 줄인다. 

class Department {
    private List<Employee> employees;

    public void addEmployee(Employee employee) {
        employees.add(employee);
    }

    public double getTotalSalary() {
        double total = 0;
        for (Employee employee : employees) {
            total += employee.calculateSalary();  // 각 Employee가 급여 계산을 캡슐화
        }
        return total;
    }
}

class Employee {
    private double salary;
    private double bonus;

    public double calculateSalary() {
        return salary + bonus;  // 급여 계산 로직을 Employee 내부로 이동
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }
}

13.데이터 클래스  (Information holder)

데이터를 저장하고, 데이터에 접근(getters) 및 설정(setters) 메서드만 제공하는 클래스이다. 이러한 클래스는 일반적으로 행동을 거의 포함하지 않는다. 자료 저장소이지만 데이터와 동작이 있어야 한다.

class UserData {
    private String userName;
    private String userEmail;
    
    public String getUserName() { return userName; }
    public void setUserName(String userName) { this.userName = userName; }
    public String getUserEmail() { return userEmail; }
    public void setUserEmail(String userEmail) { this.userEmail = userEmail; }
}
class UserData {
    private String userName;
    private String userEmail;
    
    // 데이터에 대한 행동을 클래스 내부에 추가
    public void displayUserInfo() {
        System.out.println("User: " + userName + ", Email: " + userEmail);
    }
}

14. 방치된 상속물 (Refused Bequest)

서브클래스가 상속받았지만 사용하지 않는 메서드나 데이터를 포함하는 경우

class Vehicle {
    public void drive() {}
    public void fly() {}  // 모든 차량이 fly 기능을 필요로 하지 않음
}

class Car extends Vehicle {
    public void drive() {
        System.out.println("Driving a car");
    }
    // fly 메서드는 사용되지 않음
}
class Vehicle {
    public void drive() {}
}

class FlyingVehicle extends Vehicle {
    public void fly() {}
}

class Car extends Vehicle {
    public void drive() {
        System.out.println("Driving a car");
    }
}

15. 불필요한 주석

코드 자체로 명확하지 않아 주석을 필요로 하는 경우, 코드 리팩토링이 필요할 수 있다. 주석이 코드의 동작을 설명하기보다는 왜 그렇게 작성되었는지를 설명해야 한다.

// 아래 메서드는 사용자를 추가합니다.
// 입력: 사용자 이름
// 출력: 없음
public void addUser(String userName) {
    // 사용자 추가 로직
}
// 사용자 이름을 시스템에 추가합니다.
public void addUser(String userName) {
    // 사용자 추가 로직
}

리팩토링 패턴

1. 메서드 추출 (Extract Method)

그룹으로 묶어도 될 코드 블록은 메서드로 바꾸고 이름이 그 블록의 목적을 설명하도록 만든다.

void printowing (double amount) {
    printBanner ()
    //print details
    System.out.println("name: " + _name) :
    System.out.println("amount: " + amount) ;
}
void printowing (double amount) {
    printBanner ()
    printDetails (amount)
}

void printDetails (double amount) {
    System.out.println("name: " +_name) ;
    System.out.println("amount: " + amount) ;
}

 

2. 임시변수를 메서드 호출로 전환

산술식의 결과를 저장하는 임시 변수를 사용하고 있는 경우, 산술식 자체를 메서드로 추출한다. 그리고 모든 임시변수에 대한 참조를 산술식으로 치환해 새로운 메서드를 다른 메서드에서 사용 가능하게 한다.

double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
	return basePrice * 0.95;
else
	return basePrice * 0.98;
if (basePrice() › 1000)
	return basePrice() * 0.95;
else
	return basePrice() * 0.98;
double basePrice() {
	return _quantity * _itemPrice;
}

 

3. 메서드 이동

메서드가 정의된 클래스보다 다른 클래스의 기능을 더 많이 사용한다면 많이 사용하느 클래스 안에 동일한 코드를 가진 새로운 메서드를 생성한다. 기존 메서드를 간단한 위임으로 바꾸거나 같이 제거한다. 


class AccountType {
    private boolean isPremium;

    public boolean isPremium() {
        return isPremium;
    }
}

class Account {
    private AccountType _type;
    private int _daysOverdrawn;

    double overdraftCharge() {
        if (_type.isPremium()) {
            double result = 10;
            if (_daysOverdrawn > 7) {
                result += (_daysOverdrawn - 7) * 0.85;
                return result;
            } else {
                return _daysOverdrawn * 1.75;
            }
        } else {
            return _daysOverdrawn * 1.75; // 예시로 기본 요금을 같게 했습니다.
        }
    }

    double bankCharge() {
        double result = 4.5;
        if (_daysOverdrawn > 0) {
            result += overdraftCharge();
        }
        return result;
    }
}
  • overdraftCharge() 를 AccountType 클래스로 이동.
  • 새로운 클래스로 메서드를 이동할 때, 원래 클래스의 내부 속성을 사용하는지 잘 살펴야
    • overdraftCharge()가 _daysOverdrawn 사용
  • 이런 속성들은 새로 만들 클래스의 메서드에 대한 파라미터가 되어야 한다.

daysOverdrawn은 파라미터로

_type.isPremium()->isPremimum()

class AccountType {
    ...
    double overdraftCharge(int daysOverdrawn) {
        if (isPremium()) {
            double result = 10;
            if (daysOverdrawn > 7) {
                result += (daysOverdrawn - 7) * 0.85;
            }
            return result;
        } else {
            return daysOverdrawn * 1.75;
        }
    }
    ...
}

Account 클래스에서는 overdraftCharge() 메서드에서 AccountType 클래스의 overdraftCharge() 메서드에 위임하거나

class Account {
    private AccountType _type;
    private int _daysOverdrawn;

    double overdraftCharge() {
        return _type.overdraftCharge(_daysOverdrawn);
    }

    double bankCharge() {
        double result = 4.5;
        if (_daysOverdrawn > 0) {
            result += overdraftCharge();
        }
        return result;
    }
}

 

Account 안에 있는 overdraftCharge() 메서드를 완전히 제거하고, Accoun tType.overdraftCharge()에 대한 호출을 bankCharge()로 이동

class Account {
    private AccountType _type;
    private int _daysOverdrawn;


    double bankCharge() {
        double result = 4.5;
        if (_daysOverdrawn > 0) {
            result += _type.overdraftCharge(_daysOverdrawn);
        }
        return result;
    }
}

 

4. 조건문을 재정의로 전환

객체의 타입에 따라 다른 동작을 선택하는 조건문을 가지고 있을 때 각 조건을 서브클래스의 재정의 메서드로 전환하고 원래의 메서드는 추상화를 한다.

double getSpeed() {
    switch (_type) {
        case EUROPEAN:
            return getBaseSpeed();
        case AFRICAN:
            return getBaseSpeed() - getLoadFactor() * _numberOfCoconuts;
        case NORWEGIAN_BLUE:
            return (_isNailed) ? 0 : getBaseSpeed(_voltage);
        default:
            throw new RuntimeException("Unknown Type of Bird");
    }
}

리팩토링 : 다형성을 이용해 새로운 서브클래스를 쉽게 추가할 수 있다.

abstract class Bird {
    abstract double getSpeed();
}

class European extends Bird {
    @Override
    double getSpeed() {
        return getBaseSpeed();
    }
}

class African extends Bird {
    private int numberOfCoconuts;

    public African(int numberOfCoconuts) {
        this.numberOfCoconuts = numberOfCoconuts;
    }

    @Override
    double getSpeed() {
        return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
    }
}

class NorwegianBlue extends Bird {
    private boolean isNailed;
    private double voltage;

    public NorwegianBlue(boolean isNailed, double voltage) {
        this.isNailed = isNailed;
        this.voltage = voltage;
    }

    @Override
    double getSpeed() {
        return isNailed ? 0 : getBaseSpeed(voltage);
    }
}

public class Main {
    public static void main(String[] args) {
        Bird european = new European();
        Bird african = new African(3);
        Bird norwegianBlue = new NorwegianBlue(true, 1.5);

        System.out.println("European Speed: " + european.getSpeed());
        System.out.println("African Speed: " + african.getSpeed());
        System.out.println("Norwegian Blue Speed: " + norwegianBlue.getSpeed());
    }
}

5. Null 검사 객체에 위임

Null 값의 반복 체크하는 대신 Null 검사 객체를 생성해 거기에 널 검사를 완전히 위임한다. 

class Customer {
    public String getName() {
        return "John Doe"; // 예시 이름
    }
}

class CustomerService {
    public Customer findCustomer(String criteria) {
        // 여기서 고객을 찾아 반환하거나 고객이 없으면 null을 반환합니다.
        return null; // 이 예제에서는 null을 반환하고 있습니다.
    }

    public void processCustomer(String criteria) {
        Customer customer = findCustomer(criteria);
        String name;
        if (customer == null) {
            name = "occupant";
        } else {
            name = customer.getName();
        }

        // 고객이 null인 경우를 다시 한 번 검사합니다.
        if (customer == null) {
            // null 관련 로직 처리
        }

        System.out.println("Processing " + name);
    }
}

리팩토링!

interface Customer {
    String getName();
}

class RealCustomer implements Customer {
    private String name;

    public RealCustomer(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }
}

class NullCustomer implements Customer {
    @Override
    public String getName() {
        return "occupant";
    }
}

class CustomerService {
    public Customer findCustomer(String criteria) {
        // 검색 로직, 결과에 따라 NullCustomer 또는 RealCustomer 반환
        for (Customer customer : customers) {
            if (customer.getName().equalsIgnoreCase(criteria)) {
                return customer; // 실제 고객 객체 반환
            }
        }
        // 검색 결과가 없는 경우 NullCustomer를 반환
        return new NullCustomer(); 
    }

    public void processCustomer(String criteria) {
        Customer customer = findCustomer(criteria);
        // Null 검사 없이 곧바로 getName을 호출합니다.
        String name = customer.getName();

        // Null 검사를 제거합니다. NullCustomer의 getName은 "occupant"를 반환하므로 안전합니다.
        System.out.println("Processing " + name);
    }
}

 

6. 상태 변환 메서드와 값 반환 메서드를 분리

getTotalOutstandingAndSetReadyForSummaries() 는 2가지 기능을 가지므로

  • getTotalOutStanding()
  • setReadyForSummaries()

로 쪼갠다

7. 매개변수 집합을 객체로 전환

같이 있는 것이 자연스러운 파라미터는 객체로 묶는다.

  • amountInvoicedIn(Date start, Date end);
  • amountOverdueIn(Date start, Date end);

와 같은 메서드가 있다면

start와 end를 하나의 객체로 묶어 amountInvoicedIn(DateRange dateRange); 로 수정

 

8. 여러 개의 조건문을 감시절로 전환

정상 수행 경로를 명확히 만들지 않는조건을 가진 메서드가 있다면 모든 특수 케이스에는 가드 조건을 사용할 것!

double getAmount() {
    double result;
    if (_isDead) {
        result = deadAmount();
    } else if (_isSeparated) {
        result = separatedAmount();
    } else if (_isRetired) {
        result = retiredAmount();
    } else {
        result = normalAmount();
    }
    return result;
}
double getAmount () {
    if (_isDead) return deadAmount ( ) ;
    if (_isSeparated) return separatedAmount (); 
    if (_isRetired) return retiredAmount (); 
    return normalAmount () ;
}

특수 조건을 식별하는 모든 코드는 한 줄로 변한다. 조건이 적용되는지 결정해 만족하면 이를 처리해 가드 조건이라고 부른다. 비록 4개의 리턴이 있지만 이해하기가 더 쉽다.