본문 바로가기
공부 기록

[리팩토링 1판] Chapter 07. 객체 간의 기능 이동

by 매트(Mat) 2024. 4. 1.

Chapter 07. 객체 간의 기능 이동

객체 설계에서 원칙은 아니지만 가장 중요한 일 중 하나가 바로 '기능을 어디에 넣을지 판단'하는 것이다.

기능을 넣을 적절한 위치를 찾는 문제는 메서드 이동과 필드 이동을 하면 된다.

클래스가 방대해지는 원인은 대개 기능이 너무 많기 때문이다. 이럴 때는 클래스 추출을 하면 된다.

메서드 이동 (Move Method)

메서드가 자신이 속한 클래스보다 다른 클래스의 기능을 더 많이 이용할 땐

  • 그 메서드가 제일 많이 이용하는 클래스 안에서 비슷한 내용의 새 메서드를 작성하자.
  • 기본 메서드는 간단한 대리 메서드로 전환하든지 아예 삭제하자.

메서드 이동은 빛과 소금 같은 리팩토링 기법이다. 클래스에 기능이 너무 많거나 클래스가 다른 클래스와 과하게 연동되어 의존성이 지나칠 때는 메서드를 옮기는 것이 좋다. 메서드를 옮기면 클래스가 간결해지고 여러 기능을 더 명확히 구현할 수 있다.

public class Account {
    private AccountType type;
    private int daysOverdrawn;

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

    public double bankCharge() {
        double result = 4.5;
        if (daysOverdrawn > 0) {
            result += overdraftCharge();
        }
        return result;
    }
}
  • 몇 가지 새 계좌 유형을 추가할 예정이며, 각 계좌 유형마다 당좌대월 금액을 계산하는 공식이 다르다고 가정하자.
  • 그래서 overdraftCharge 메서드를 AccountType 클래스로 옮기려 한다.
public class AccountType {
    public double overdraftCharge(int daysOverdrawn) {
        if (isPremium()) {
            double result = 10;
            if (daysOverdrawn > 7) {
                result += (daysOverdrawn - 7) * 0.85;
            }
            return result;
        } else {
            return daysOverdrawn * 1.75;
        }
    }

    public boolean isPremium() {
        // 로젝 생략
        return true;
    }

}
  • 적절한 수정을 통해 AccountType 클래스의 overdraftCharge 메서드에서 type을 삭제하고 daysOverdrawn 변수를 매개변수로 전달한다.
  • Account 클래스는 위임 코드로 바꿀 수 있게 된다.
  • 또한 AccountType 클래스에서 매개변수로 daysOverdrawn를 넘기지 않고, Account 클래스 자체를 넘기면 Account 클래스의 다른 변수나 메서드를 호출할 수 있다.
public class Account {
    private AccountType type;
    private int daysOverdrawn;

    public double overdraftCharge() {
        return type.overdraftCharge(daysOverdrawn);
    }

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

필드 이동 (Move Field)

어떤 필드가 자신이 속한 클래스보다 다른 클래스에서 더 많이 사용될 때는

  • 대상 클래스 안에 새 필드를 선언하고 그 필드 참조 부분을 전부 새 필드 참조로 수정하자.

  • 한 클래스에서 다른 클래스로 기능을 옮기는 것은 리팩토링의 기본이다.
  • 시스템이 발전할수록 새 클래스가 필요해지고 기능을 여기저기로 옮겨야 할 필요성도 생긴다.
public class Account {
    private AccountType type;
    private double interestRate;

    public double interestForAmountDays(double amount, int days) {
        return interestRate * amount * days / 365;
    }
}
  • interestRate 필드를 AccountType 클래스로 옮기려 한다.
public class AccountType {
    private double interestRate;

    public double getInterestRate() {
        return interestRate;
    }

    public void setInterestRate(double interestRate) {
        this.interestRate = interestRate;
    }
}
  • 옮긴 후 getter/setter를 만든다.
  • 변경된 Account 클래스는 아래와 같다.
  • interestRate 필드는 private으로 선언하여 캡슐화로 숨겨야 한다.
public class Account {
    private AccountType type;

    public double interestForAmountDays(double amount, int days) {
        return type.getInterestRate() * amount * days / 365;
    }
}
  • getter 메서드를 이용하여 값을 가져온다.

클래스 추출 (Extract Class)

두 클래스가 처리해야 할 기능이 하나의 클래스에 들어 있을 땐

  • 새 클래스를 만들고 기존 클래스의 관련 필드와 메서드를 새 클래스로 옮기자.

클래스는 확실하게 추상화되어야 하며, 두세 가지의 명확한 기능을 담당해야 한다. 실제로 클래스는 시간이 갈수록 방대해지기 마련이다. 개발자는 클래스에 점증적으로 어떤 기능이나 데이터를 추가하기 때문이다.

  • 클래스 추출은 두 결과 클래스에 따로 락을 걸 수 있어서 병렬 실행 프로그램의 생동감을 향상시키는 용도로 흔히 사용되는 기법이다.
  • 두 객체에 락을 걸 필요가 없다면 이 기법을 사용하지 않아도 무방하다.

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

클래스에 기능이 너무 적을 땐 그 클래스의 모든 기능을 다른 클래스로 합쳐 넣고 원래의 클래스는 삭제하자.

  • 클래스 내용 직접 삽입은 클래스 추출과는 반대이다.
  • 클래스 내용 직접 삽입은 클래스가 더 이상 제 역할을 수행하지 못하여 존재할 이유가 없을 때 사용한다.
  • 이런 상황은 주로 클래스의 기능 대부분을 다른 곳으로 옮기는 리팩토링을 실시해서 남은 기능이 거의 없어졌을 때 나타난다.

이 기법의 장점은 필요없는 클래스를 줄여나갈 수 있어서 더 간결해지고 명확해진다.

대리 객체 은폐 (Hide Delegate)

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

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

  • 클라이언트가 서버 객체의 필드 중 하나에 정의된 메서드를 호출할 때 그 클라이언트는 이 대리 객체에 관해 알아야 한다.
  • 대리 객체가 변경될 때 클라이언트도 변경해야 할 가능성이 있기 때문이다.
  • 이런 의존성을 없애려면, 대리 객체를 감추는 간단한 위임 메서드를 서버에 두면 된다.
  • 변경은 서버에만 이뤄지고 클라이언트에는 영향을 주지 않는다.

즉, 대리 객체에 getter를 사용하여 은폐시키고 서버 객체에는 대리 객체의 getter 메서드를 호출한다. 그리고 클라이언트 객체는 서버의 기능을 그대로 사용하면 문제 없다.

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

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

  • 앞서 대리 객체 은폐의 장점을 알아보았는데 단점 역시 존재한다.
  • 클라이언트가 대리 객체의 새 기능을 사용해야 할 때마다 서버에 간단한 위임 메서드를 추가해야 한다는 점이다.
  • 이럴 때는 클라이언트가 대리 객체를 직접 호출하게 해야 한다.

즉, 서버 객체를 통해 기능을 사용하는 것이 아닌 바로 대리 객체를 통해 메서드를 호출한다.

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

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

public class Sample1 {
    private Previous previous;

    public void methodA() {
        LocalDate newStart = LocalDate.of(previous.getYear(), previous.getMonth(), previous.getDays());
    }
}

리팩토링 후

public class Sample1 {
    private Previous previous;

    public LocalDate methodA() {
        return nextDay(previous);
    }

    private LocalDate nextDay(Previous previous) {
        return LocalDate.of(previous.getYear(), previous.getMonth(), previous.getDays());
    }
}
  • 원본 클래스가 수정 가능하다면 원본 클래스에 메서드를 추가한다.
  • 하지만 수정할 수 없다면 클라이언트 클래스 안에 메서드를 추가해야 한다.

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

사용중인 서버 클래스에 여러 개의 메서드를 추가해야 하는데 클래스를 수정할 수 없을 땐

  • 새 클래스를 작성하고 그 안에 필요한 여러 개의 메서드를 작성하자.
  • 이 상속확장 클래스를 원본 클래스의 하위 클래스나 래퍼 클래스로 만들자.

필요한 메서드들을 적당한 곳에 모아둬야 하는데, 확실한 방법은 보편적인 객체지향 기법인 하위클래스화와 래퍼화를 실시하는 것이다. 이렇게 만든 하위클래스와 래퍼클래스를 국소적 상속확장 클래스라고 부른다.

  • 국소적 상속확장 클래스는 별도의 클래스지만 상속확장하는 클래스의 하위 타입이다.
  • 따라서 국소적 상속확장 클래스는 원본 클래스의 모든 기능도 사용 가능하면서 추가 기능도 들어 있다.
  • 원본 클래스를 사용할 것이 아니라 국소적 상속확장 클래스를 인스턴스화해서 사용하자.

주의할 점이 있는데 원본 클래스를 LocalDate라 할 때 국소적 상속확장 클래스에서 equals 메서드를 재정의 할 경우 메서드 이름을 다르게해서 메서드를 추가해야 한다.

그렇지 않으면 equals 메서드의 원래 본연의 기능이 아닌 내가 재정의한 메서드로 실행되기 때문이다. 따라서 내가 정의한 equals 메서드로 추가하려면 equalsLocalDate 처럼 메서드 이름을 다르게 해서 추가 작성하면 된다.

정리

  • 기능을 재정의하면 메서드를 검색할 때 애먹게 된다.
  • 국소적 상속확장 클래스를 사용한다면 메서드를 재정의하지 않고 그냥 메서드를 추가하면 된다.

느낀 점

이번 챕터는 이전에 배웠던 부분과 겹치는 것이 있다.

  • 메서드 이동
  • 필드 이동

그 밖에 왜 객체를 은폐(캡슐화)시켜야 하는지, 클래스 추출과 직접 내용 삽입하는 방법과 국소적 상속확장 클래스라는 용어를 알게 되었다.

어떻게 보면 스프링을 공부하면서 배웠던 패턴들이 지금 공부하고 있는 리팩토링의 기법들을 당연하게 적용하고 있는 것이다.

이를 통해 우리는 스프링의 편리한 기능들을 사용할 수 있는 것을 보면 객체 간의 기능 이동이 얼마나 잘 되어 있으며, 캡슐화가 잘 되어있는지 알 수가 있었다.

우리는 스프링을 사용하는 클라이언트니까 잘 알 수 있다.ㅎㅎ

References

댓글