본문 바로가기
Refactoring

리팩토링 - 객체 간의 기능 이동

by jayden-lee 2019. 6. 6.
728x90

메서드 이동 (Move Method)

메서드가 자신의 클래스에 있는 기능보다 다른 클래스의 기능을 더 많이 사용하는 경우에 메서드가 많이 사용하는 클래스에 비슷한 내용의 새 메서드를 작성하자. 기존 메서드는 대리 메서드로 전환 또는 삭제하자.

수정전 코드

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;
    }
  }

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

수정후 코드

class Account {
     private AccountType type;
  private int daysOverdrawn;

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

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메서드는 Account 클래스보다 AccountType 클래스로 메서드를 이동하는 것이 바람직하다.

 

bankCharge 메서드는 AccountType 클래스의 overdraftCharge 메서드를 호출하고 있다. 매개변수로 daysOverdrawn 값을 넘기고 있다. 만약, Account 클래스의 다른 메서드를 호출해야 한다면 원본 객체를 전달할 수 있다.

class Account {
  // 생략
  double bankCharge() {
    double result = 4.5;
    if (daysOverdrawn > 0) {
      result += type.overdraftCharge(this);
    }
    return result;
  }
}

class AccountType {
  double overdraftCharge(Account account) {
    double result = 0;
    // 생략
    return result;
  }
}

필드 이동 (Move Field)

어떤 필드가 자신이 속해 있는 클래스보다 다른 클래스에서 많이 사용될 때는 필드를 대상 클래스로 이동시키자

수정전 코드

class Account {
  private AccountType type;
  private double interestRate;

  double interestForAmountDays(double amount, int days) {
    return interestRate * amount * days / 365;
  }
}

수정후 코드

class Account {
  private AccountType type;

  double interestForAmountDays(double amount, int days) {
    return getInterestRate() * amount * days / 365;
  }

  private void setInterestRate(double arg) {
    type.setInterestRate(arg);
  }

  private double getInterestRate() {
    return type.getInterestRate();
  }
}

class AccountType {
  private double interestRate;

  private void setInterestRate(double arg) {
    interestRate = arg;
  }

  private double getInterestRate() {
    return interestRate;
  }
}

설명

어떤 필드가 자신이 속한 클래스보다 다른 클래스에 있는 메서드를 더 많이 참조해서 정보를 이용한다면 필드를 옮기는 것을 고려해보자.

클래스 추출 (Extract Class)

두 클래스가 처리해야 할 기능이 하나의 클래스에 있다면, 새 클래스를 만들고 기존 클래스의 관련 필드와 메서드를 새 클래스로 옮기자

수정전 코드

class Person {

  private String name;
  private String officeAreaCode;
  private String officeNumber;

  public String getName() {
    return name;
  }

  public String getTelephoneNumber() {
    return "(" + officeAreaCode + ")" + officeNumber;
  }

  public String getOfficeAreaCode() {
    return officeAreaCode;
  }

  public void setOfficeAreaCode(String args) {
    this.officeAreaCode = arg;
  }

  public String getOfficeNumber() {
    return officeNumber;
  }

  public void setOfficeNumber(String arg) {
    this.officeNumber = arg;
  }
}

수정후 코드

class Person {

  private TelephoneNumber officeTelephone = new TelephoneNumber();
  private String name;

  public String getName() {
    return name;
  }

  public String getTelephoneNumber() {
    return officeTelephone.getTelephoneNumber();
  }

  public TelephoneNumber getOfficeTelephone() {
    return officeTelephone;
  }
}

class TelephoneNumber {

  private String number;
  private String areaCode;

  public String getTelephoneNumber() {
    return "(" + areaCode + ")" + number;
  }

    public String getAreaCode() {
    return areaCode;
  }  

  public setAreaCode(String arg) {
    this.areaCode = arg;
  }

  public String getNumber() {
    return number;
  }

  public void setNumber(String arg) {
        this.number = arg;
  }
}

설명

클래스는 추상화되어야 하며, 두 세가지의 기능을 담당해야 한다. 시간이 갈수록 요구사항이 변함에 따라 클래스에 기능이 추가되고 여러 기능을 담당하게 된다. 이렇게 기능을 추가하다 보면, 클래스가 방대해진다. 이런 경우에 데이터와 메서드 일부분을 떼어내어 다른 클래스로 이동할 수 없는지 고민해보자.

클래스 내용 직접 삽입 (Inline Class)

클래스에 기능이 너무 적을 땐 해당 클래스의 모든 기능을 다른 클래스로 모두 옮기고 원래 클래스는 삭제하자

수정전 코드

class Person {

  private TelephoneNumber officeTelephone = new TelephoneNumber();
  private String name;

  public String getName() {
    return name;
  }

  public String getTelephoneNumber() {
    return officeTelephone.getTelephoneNumber();
  }

  public TelephoneNumber getOfficeTelephone() {
    return officeTelephone;
  }
}

class TelephoneNumber {

  private String number;
  private String areaCode;

  public String getTelephoneNumber() {
    return "(" + areaCode + ")" + number;
  }

    public String getAreaCode() {
    return areaCode;
  }  

  public setAreaCode(String arg) {
    this.areaCode = arg;
  }

  public String getNumber() {
    return number;
  }

  public void setNumber(String arg) {
        this.number = arg;
  }
}

수정후 코드

class Person {

  private String name;
  private String officeAreaCode;
  private String officeNumber;

  public String getName() {
    return name;
  }

  public String getTelephoneNumber() {
    return "(" + officeAreaCode + ")" + officeNumber;
  }

  public String getOfficeAreaCode() {
    return officeAreaCode;
  }

  public void setOfficeAreaCode(String args) {
    this.officeAreaCode = arg;
  }

  public String getOfficeNumber() {
    return officeNumber;
  }

  public void setOfficeNumber(String arg) {
    this.officeNumber = arg;
  }
}

설명

이번에 다루는 클래스 내용 직접 삽입클래스 추출과 반대이다. 클래스 내용 직접 삽입은 클래스가 기능이 너무 적고 제 역할을 하기에 충분하지 않을 때 적용하는 방법이다. 리팩토링 작업을 하고 나서 클래스에 남는 기능이 없어질 때 나타난다. 이런 경우에는 기능이 적은 클래스를 가장 많이 사용하는 클래스에 필드와 메서드를 모두 옮긴다.

대리 객체 은폐 (Hide Delegate)

클라이언트가 객체의 대리 클래스를 호출할 땐, 대리 클래스를 감추는 메서드를 서버에 작성하자

수정전 코드

class Person {
  private Department department;

  public Department getDepartment() {
    return department;
  }

  public void setDepartment(Department arg) {
    department = arg;
  }
}

class Department {
  private String chargeCode;
  private Person manager;

  public Department(Person manager) {
    this.manager = manager;
  }

  public Person getManager() {
    return manager;
  }
}

public class Main {
  public static void main(String[] args) {
    Person manager = jayden.getDepartment().getManager();
  }
}

수정후 코드

class Person {
  private Department department;

  public Person(Department department) {
    this.department = department;
  }

  public Person getManager() {
    return department.getManager();
  }
}

class Department {
  private String chargeCode;
  private Person manager;

  public Department(Person manager) {
    this.manager = manager;
  }

  public Person getManager() {
    return manager;
  }
}

public class Main {
  public static void main(String[] args) {
    Person manager = jayden.getManager();
  }
}

설명

객체에서 핵심 개념 중 하나는 캡슐화이다. 캡슐화는 객체가 시스템의 다른 부분에 대한 정보의 일부만 볼 수 있게 은폐하는 것을 말한다. 객체를 캡슐화하면 무언가를 변경할 때 변화를 전달해야 할 객체가 줄어들므로 변경하기 쉬워진다.

 

클라이언트가 서버 객체의 필드 중 하나에 정의된 메서드를 호출하려면 그 클라이언트는 대리 객체에 대한 존재를 알아야 한다. 대리 객체가 변경될 때마다 클라이언트도 변경할 가능성이 있기 때문이다. 이런 의존성을 제거하기 위해서는 대리 객체를 감추는 위임 메서드를 서버에 두면 된다.

 

수정전 코드에서 클라이언트가 특정 인원이 속해 있는 팀장이 누구인지 조회하기 위해서 읽기 메서드(getDepartment)를 통해서 Department 클래스에 접근한다. 클라이언트가 Department 존재 자체를 알 수 없게 의존성을 제거하기 위해서는 Person 클래스에 다음과 같이 간단한 위임 메서드를 작성하면 된다.

public Person getManager() {
    return department.getManager();
}

과잉 중개 메서드 제거 (Remove Middle Man)

클래스에 자잘한 위임이 너무 많을 땐 대리 객체를 클라이언트가 직접 호출하게 하자

수정전 코드

class Person {
  private Department department;

  public Person(Department department) {
    this.department = department;
  }

  public Person getManager() {
    return department.getManager();
  }
}

class Department {
  private String chargeCode;
  private Person manager;

  public Department(Person manager) {
    this.manager = manager;
  }

  public Person getManager() {
    return manager;
  }
}

public class Main {
  public static void main(String[] args) {
    Person manager = jayden.getManager();
  }
}

수정후 코드

class Person {
  private Department department;

  public Department getDepartment() {
    return department;
  }

  public void setDepartment(Department arg) {
    department = arg;
  }
}

class Department {
  private String chargeCode;
  private Person manager;

  public Department(Person manager) {
    this.manager = manager;
  }

  public Person getManager() {
    return manager;
  }
}

public class Main {
  public static void main(String[] args) {
    Person manager = jayden.getDepartment().getManager();
  }
}

설명

대리 객체 은폐에서 클라이언트가 대리 객체의 존재를 감추기 위해서 캡슐화를 적용했다. 이 방법은 장점도 있지만 단점도 동시에 존재한다. 클라이언트가 대리 객체의 새로운 기능을 사용할 때마다 서버에 위임 메서드를 추가해야 한다는 점이다. 서버 클래스는 중개자에 불과하게 된다. 이런 경우에는 클라이언트가 대리 객체를 직접 호출하게 해야 한다.

 

모든 위임 메서드를 제거할 필요는 없다. 대리 객체를 일부 클라이언트에게만 감추고 나머지 클라이언트에게 공개해야 하는 경우도 있다. 그럴 때는 간단한 위임 메서드 중 일부를 그대로 내버려두면 된다.

외래 클래스에 메서드 추가 (Introduce Foreign Method)

사용중인 서버 클래스에 메서드를 추가해야 하는데 그 클래스를 수정할 수 없을 땐, 클라이언트 클래스 안에 서버 클래스를 인자로 받는 메서드를 작성하자

수정전 코드

Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);

수정후 코드

Date newStart = nextDay(previousEnd);

private static Date nextDay(Date arg) {
  return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}

설명

원본 클래스를 수정할 수 있다면 원하는 기능을 추가할 수 있다. 다만, 수정할 수 없는 경우에는 클라이언트 클래스에서 새로운 기능 메서드를 구현해야 한다. 그리고 추가한 기능이 한 번만 사용한다면 상관없지만 여러 번 사용한다면 클래스마다 해당 기능 역할을 하는 메서드를 추가하게 된다. 동일한 기능의 중복 메서드를 만드는 것은 관리 포인트를 늘리는 일이기 때문에 중복 코드를 작성하지 않도록 주의해야 한다.

국소적 상속확장 클래스 사용 (Introduce Local Extension)

사용중인 클래스에 여러 개의 메서드를 추가해야 하는데 클래스를 수정할 수 없을 땐, 새 클래스를 작성하고 그 안에 필요한 메서드를 정의하자

하위 클래스 사용 코드

public class CustomDate extends Date {

  public CustomDate (String dateString) {
    super (dateString);
  }

  public CustomDate (Date arg) {
    super (arg.getTime());
  }

  public Date nextDay() {
    return new Date(getYear(), getMonth(), getDate() + 1);
  }

}

래퍼 클래스 사용 코드

public class CustomDate {
  private Date original;

  public CustomDate (String dateString) {
    original = new Date(dateString);
  }

  public CustomDate (Date arg) {
    this.original = arg;
  }

  public int getYear() {
    return original.getYear();
  }

  // 위임 메서드 추가

  public Date nextDay() {
    return new Date(getYear(), getMonth(), getDate() + 1);
  }  

  public boolean equals(Object arg) {
    if (this == arg) return true;
    if (!(arg instanceof CustomDate)) return false;
    CustomDate other = ((CustomDate) arg);
    return original.equals(other.original);
  }

}

설명

필요한 기능이 있어서 클래스를 추가하려는데 수정할 수 없을 땐 외래 클래스에 메서드 추가 기법을 사용하면 된다. 하지만, 필요한 메서드 수가 세 개 이상이면 새로운 클래스를 작성하고 그 안에 필요한 메서드를 정의하는 것이 좋다. 새로 작성하는 클래스는 기존 클래스를 확장하거나 래퍼화를 한다. 이렇게 만든 하위클래스와 래퍼 클래스를 국소적 상속확장 클래스라고 부른다.

 

'Refactoring' 카테고리의 다른 글

리팩토링 - 메서드 정리  (0) 2019.05.16
리팩토링 - 리팩토링 개론  (0) 2019.05.04
리팩토링 - 코드의 구린내  (0) 2019.04.28

댓글