본문 바로가기
공부 기록

[리팩토링 1판] Chapter 10. 메서드 호출 단순화

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

Chapter 10. 메서드 호출 단순화

객체에서 가장 중요한 것은 인터페이스다. 이해와 사용이 쉬운 인터페이스를 작성하는 기술이야 말로 좋은 객체지향 소프트웨어 개발에 꼭 필요하다.

인터페이스를 더 쉽게 만드는 리팩토링

  • 가장 간단하면서도 중요한 것은 메서드명을 변경하는 일이다.
  • 매개변수 자체는 인터페이스를 다루는데 상당한 역할을 한다.
  • ...

좋은 인터페이스는 보여줘야 할 만큼만 딱 보여준다.
공개할 필요가 없는 것들을 감추는 것만으로 인터페이스를 개선할 수 있다.

메서드명 변경

메서드명을 봐도 기능을 알 수 없을 땐

  • 메서드명을 직관적인 이름으로 바꾸자.

public class Sample1 {
    public int getInvcdtlmt() {
        return 0;
    }
}

리팩토링 후

public class Sample1 {
    public int getInvoiceableCreditLimit() {
        return 0;
    }
}
  • 메서드명만 봐도 그 메서드의 의도를 한눈에 알 수 있어야 한다.
  • 메서드 기능을 설명하기 위해 주석을 떠올린 후 그 주석을 메서드명으로 바꾸면 쉽게 정할 수 있다.

매개변수 추가

메서드가 자신을 호출한 부분의 정보를 더 많이 알아야할 땐

  • 객체에 그 정보를 전달할 수 있는 매개변수를 추가하자.

public class Sample2 {
    public String getContact() {
        Customer customer = new Customer();
        return customer.getContact();
    }

    static class Customer {
        String contact;

        public String getContact() {
            return contact;
        }
    }
}

리팩토링 후

public class Sample2 {
    public String getContact(Customer customer) {
        return customer.getContact();
    }

    static class Customer {
        String contact;

        public String getContact() {
            return contact;
        }
    }
}
  • 사실 이 리팩토링은 하지 말아야 한다.
  • 매개변수를 추가하는 대신 다른 방법이 있을 수 있다.
  • 매개변수를 추가하면 매개변수 세트가 더 길어지기 때문이다.

매개변수 제거

메서드가 어떤 매개변수를 더 이상 사용하지 않을 땐

  • 그 매개변수를 삭제하자.

public class Sample3 {
    public String getContact(Customer customer) {
        return "Contact Me";
    }

    static class Customer {
        String contact;

        public String getContact() {
            return contact;
        }
    }
}

리팩토링 후

public class Sample3 {
    public String getContact() {
        return "Contact Me";
    }

    static class Customer {
        String contact;

        public String getContact() {
            return contact;
        }
    }
}
  • 매개변수를 제거하지 않으면 그 메서드를 사용하는 모든 곳이 불필요한 추가 작업을 수행하게 된다.

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

값 반환 기능과 객체 상태 변경 기능이 한 메서드에 들어 있을 땐

  • 질의 메서드와 변경 메서드로 분리하자.

public class Sample4 {
    public void getTotalOutstandingAndSetReadyForSummaries() {
        // 값 반환과 객체 상태 변경 동시에..
    }
}

리팩토링 후

public class Sample4 {
    public void getTotalOutstanding() {
        // 값 반환 ..
    }

    public void setReadyForSummaries() {
        // 객체 상태 변경 ..
    }
}

메서드를 매개변수로 전환

여러 메서드가 기능은 비슷하고 안에 든 값만 다를 땐

  • 서로 다른 값을 하나의 매개변수로 전달받는 메서드를 하나 작성하자.

public class Sample5 {
    public String fivePercentRaise() {
        return "5%";
    }

    public String tenPercentRaise() {
        return "10%";
    }
}

리팩토링 후

public class Sample5 {
    public String raise(String percentage) {
        return percentage;
    }
}
  • 비슷한 메서드가 여러 개 있을 때 각 메서드를 전달된 매개변수에 따라 다른 작업을 처리하는 하나의 메서드로 만들면 편리하다.

매개변수를 메서드로 전환

매개변수로 전달된 값에 따라 메서드가 다른 코드를 실행될 땐

  • 그 매개변수로 전달될 수 있는 모든 값에 대응하는 메서드를 각각 작성하자.

public class Sample6 {
    private int  height;
    private int width;

    public void setValue(String name, int value) {
        if (name.equals("height")) {
            height = value;
            return;
        }
        if (name.equals("width")) {
            width = value;
            return;
        }
    }
}

리팩토링 후

public class Sample6 {
    private int  height;
    private int width;

    public void setHeight(int height) {
        this.height = height;
    }

    public void setWidth(int width) {
        this.width = width;
    }
}

객체를 통째로 전달

객체에서 가져온 여러 값을 메서드 호출에서 매개변수로 전달할 땐

  • 그 객체를 통째로 전달하게 수정하자.

public class Sample7 {
    private int low;
    private int high;
    private int withinPlan;

    public void methodA() {
        DaysTempRange daysTempRange = new DaysTempRange();
        low = daysTempRange.getLow();
        high = daysTempRange.getHigh();

        Plan plan = new Plan();
        withinPlan = plan.withinRange(low, high);
    }

    static class Plan {
        public int withinRange(int low, int high) {
            return low * high;
        }
    }

    static class DaysTempRange {
        private int low;
        private int high;

        public int getLow() {
            return low;
        }

        public int getHigh() {
            return high;
        }
    }
}

리팩토링 후

public class Sample7 {
    private int low;
    private int high;
    private int withinPlan;

    public void methodA() {
        DaysTempRange daysTempRange = new DaysTempRange();
        Plan plan = new Plan();
        withinPlan = plan.withinRange(daysTempRange);
    }

    static class Plan {
        public int withinRange(DaysTempRange daysTempRange) {
            return daysTempRange.getLow() * daysTempRange.getHigh();
        }
    }

    static class DaysTempRange {
        private int low;
        private int high;

        public int getLow() {
            return low;
        }

        public int getHigh() {
            return high;
        }
    }
}
  • 객체를 통째로 전달하면 매개변수 세트 변경의 편의성뿐 아니라 코드를 알아보기도 쉬워진다.

매개변수 세트를 메서드로 전환

객체가 A 메서드를 호출해서 그 결과를 B 메서드에 매개변수로 전달하는데,
결과를 매개변수로 받는 B 메서드도 직접 A 메서드를 호출할 수 있을 땐

  • 매개변수를 없애고 A 메서드를 B 메서드가 호출하게 하자.

public class Sample8 {
    public void methodA() {
        int basePrice = quantity * itemPrice;
        discountLevel = getDiscountLevel();
        double finalPrice = discountPrice(basePrice, discountLevel);
    }
}

리팩토링 후

public class Sample8 {
    public void methodA() {
        int basePrice = quantity * itemPrice;
        double finalPrice = discountPrice(basePrice);
    }
}

매개변수 세트를 객체로 전환

여러 개의 매개변수가 항상 붙어 다닐 땐

  • 그 매개변수들을 객체로 바꾸자.

public class Sample9 {
    static interface Customer {
        int amountInvoicedIn(Date start, Date end);
        int amountReceivedIn(Date start, Date end);
        int amountOverdueIn(Date start, Date end);
    }
}

리팩토링 후

public class Sample9 {
    static interface Customer {
        int amountInvoicedIn(DateRange range);
        int amountReceivedIn(DateRange range);
        int amountOverdueIn(DateRange range);
    }
}
  • 새 객체에 정의된 속성 접근 메서드로 인해 코드의 일관성도 개선되고, 결과적으로 코드를 알아보거나 수정하기도 쉬워진다.

쓰기 메서드 제거

생성할 때 지정한 필드값이 절대로 변경되지 말아야할 땐

  • 그 필드를 설정하는 모든 쓰기 메서드를 삭제하자.

public class Sample10 {
    int price;

    public void setPrice(int price) {
        this.price = price;
    }
}

리팩토링 후

public class Sample10 {
    private int price;

    public int getPrice() {
        return price;
    }
}
  • 읽기의 경우 필드는 private으로 선언하여 외부에서 접근하는 것을 막고, 쓰기는 삭제한다.
  • 의도에 맞게 필드가 수정될 가능성을 차단할 수 있다.
  • 만약 쓰기가 필요한 경우 의도에 맞는 메서드명을 잘 지어서 만드는 것이 최선이다.
public class Sample10 {
    private int price;

    public void changePrice(int price) {
        this.price = price;
    }

    public int getPrice() {
        return price;
    }
}

메서드 은폐

메서드가 다른 클래스에 사용되지 않을 땐

  • 그 메서드의 반환 타입을 private으로 만들자.

public class Sample11 {
    public void methodA() {
        System.out.println("Sample11.methodA");
    }
}

리팩토링 후

public class Sample11 {
    private void methodA() {
        System.out.println("Sample11.methodA");
    }
}
  • 필드도 그랬듯이 메서드 역시 외부에서 사용하지 않는다면 은폐시키는 것이 좋다.
  • 이유도 없이 메서드를 개방 해놓으면 협업시 의도하지 않은 코드가 생긴다.
  • 이것은 유지보수에 치명적이다.

생성자를 팩토리 메서드로 전환

객체를 생성할 때 단순한 생성만 수행하게 해야 할 땐

  • 생성자를 팩토리 메서드로 교체하자.

public class Sample12 {
    static class Employee {
        int type;

        Employee(int type) {
            this.type = type;
        }
    }
}

리팩토링 후

public class Sample12 {
    static class Employee {
        int type;

        private Employee(int type) {
            this.type = type;
        }

        // 팩토리 메서드
        static Employee create(int type) {
            return new Employee(type);
        }
    }
}
  • 생성자를 팩토리 메서드로 전환하는 경우는 타입(Enum)을 하위클래스로 바꿀 때 사용한다.

하향 타입 변환을 캡슐화

메서드가 반환하는 객체를 호출 부분에서 하향 타입 변환해야 할 땐

  • 하향 타입 변환 기능을 메서드 안으로 옮기자.

public class Sample13 {
    List<Object> readings = new ArrayList<>();
    public Object lastReading() {
        return readings.getLast();
    }

    static class Reading {

    }
}

리팩토링 후

public class Sample13 {
    List<Object> readings = new ArrayList<>();
    public Reading lastReading() {
        return (Reading) readings.getLast();
    }

    static class Reading {

    }
}
  • 하향 타입 변환은 왠만하면 사용하지 말아야 한다.
  • 가능하면 항상 가장 구체적인 타입을 작성해야 한다.

에러 부호를 예외 코드로 교체

메서드가 에러를 나타내는 특수한 부호를 반환할 땐

  • 그 부호 반환 코드를 예외 코드로 바꾸자.

public class Sample14 {
    private int balance;
    public int withdraw(int amount) {
        if (amount > balance) {
            return -1;
        } else {
            balance -= amount;
            return 0;
        }
    }
}

리팩토링 후

public class Sample14 {
    private int balance;
    public void withdraw(int amount) {
        if (amount > balance) throw new BalanceException();
        balance -= amount;
    }

    static class BalanceException extends RuntimeException {}
}
  • 에러 코드를 만났을 때 프로그램을 중단되게 할 수 있다.
    • 하지만 이것은 비행기를 놓쳤다고 자살하는 것이나 다름 없다.
  • 자바에는 예외라는 것이 있고, 이 예외를 던져서 예외처리를 수행할 수 있다.

예외 처리를 테스트로 교체

호출 부분에 사전 검사 코드를 넣으면 될 상황인데 예외 코드를 사용했을 땐

  • 호출 부분이 사전 검사를 실시하게 수정하자.

public class Sample15 {
    int[] values;
    public double getValueForPeriod(int periodNumber) {
        try {
            return values[periodNumber];
        } catch (ArrayIndexOutOfBoundsException e) {
            return 0;
        }
    }
}

리팩토링 후

public class Sample15 {
    int[] values;
    public double getValueForPeriod(int periodNumber) {
        if (periodNumber >= values.length) return 0;
        return values[periodNumber];
    }
}
  • 예외처리 덕분에 복잡한 코드를 작성하지 않아도 되었다.
  • 하지만 좋다고 해서 예외를 과다하게 사용하면 더 이상 좋은 것이 아니게 된다.

느낀 점

메서드 호출시 리팩토링하는 방버에 대해 알아보았다.

  • 가장 간단하면서 좋은 것은 메서드명을 잘 짓는 것이다.
  • 매개변수를 전달할 수 있다.
  • 비슷한 매개변수를 객체로 전환할 수 있다.
  • 하향 타입을 메서드 안에서 전환한다.
  • 예외처리를 할 수 있고, 과잉은 좋지 않다.

위 카테고리에서 공통점은 뭐든지 과하면 안 좋다는 것이다. 메서드명을 명확하게 잘 짓는다고 과하게 길게 지으면 오히려 보기 불편하다.

뭐든지 적당한 선이 있고, 이를 지키는 것이 우리의 의무이다. 또한 그러한 선을 지키기 위해 우리는 공부하고 실무를 경혐해야 한다.

References

댓글