본문 바로가기
공부 기록

[리팩토링 1판] Chapter 06. 메서드 정리

by 매트(Mat) 2024. 3. 31.

Chapter 06. 메서드 정리

리팩토링의 주된 작업은 코드를 포장하는 메서드를 적절히 정리하는 것이다.

  • 거의 모든 문제점은 장황한 메서드로 인해 생긴다.
  • 장황한 메서드에는 많은 정보가 들어있는데, 마구 얽힌 복잡한 로직에 정보들이 묻혀버린다.
  • 핵심적인 리팩토링 기법은 코드 뭉치를 별도의 메서드로 빼내는 메서드 추출이다.

메서드 추출에서 가장 힘든 작업은 지역변수를 처리하는 것인데, 그건 주로 임시변수 때문이다.

  • 메서드 정리 작업을 할 때 임시변수를 메서드 호출로 전환을 실시해서 없어도 되는 임시변수를 전부 제거한다.

메서드 추출 (Extract Method)

어떤 코드를 그룹으로 묶어도 되겠다고 판단될 때 그 코드를 빼내어 목적을 잘 나타내는 직관적 이름의 메서드로 만들자.

public class Sample1 {
    public void printOwing(double amount) {
        printBanner();

        // 세부 정보 출력
        System.out.println("name: " + "Mike");
        System.out.println("amount: " + 3000);
    }

    private void printBanner() {
        System.out.println("printBanner()");
    }
}

메서드 추출 후

public class Sample1 {
    public void printOwing(double amount) {
        printBanner();

        printDetails(amount);
    }

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

    private void printBanner() {
        System.out.println("printBanner()");
    }
}
  • 메서드가 너무 길거나 코드에 주석을 달아야만 의도를 이해할 수 있을 때, 해당 코드를 빼내어 별도의 메서드로 만든다.
  • 이처럼 직관적인 이름의 간결한 메서드는 재활용할 수 있으며, 주석 사용하지 않아도 파악할 수 있으며, 재정의하기도 훨씬 수월하다.
  • 지역변수가 있을 경우엔 메서드 추출시 생각을 조금 해야하는데, 만약 지역변수가 읽히기만 하고 변경되지 않을 경우엔 그냥 매개변수로 전달하면 된다.

메서드 추출을 하지 않아도 코드가 직관적이고 간결하다면 메서드 추출을 하지 않는 것이 좋다. 과한 메서드 추출은 오히려 역효과를 일으킬 수 있다.

지역변수가 변경되는 경우

만약 지역변수가 변경되는 경우를 살펴보자.

  1. 임시변수가 추출한 코드 안에서만 사용되는 경우 간단히 임시변수를 추출한 코드로 옮기면 된다.
  2. 임시변수가 추출한 코드 밖에서 사용되는 경우에는 변경된 임시변수의 값을 반환시키면 된다.
public class Sample1 {
    public void printOwing(double amount) {
        printBanner();
        for (int i = 1; i <= 5; i++) {
            amount *= i;
        }
        printDetails(amount);
    }
    ...
}
  • printOwing 메서드는 간단하게 반복문을 돌면서 amount에 i값을 계속 곱해나가는 메서드이다.
  • 반복문에서 계산하는 부분을 추출해보자.
public class Sample1 {
    public void printOwing(double amount) {
        printBanner();
        amount = getAmount(amount);
        printDetails(amount);
    }

    private double getAmount(double amount) {
        for (int i = 1; i <= 5; i++) {
            amount *= i;
        }
        return amount;
    }
}
  • getAmount 메서드를 보면 amount 변수를 반환시키는 것을 볼 수 있다.
  • 정리하면 임시변수가 너무 많으면 임시변수를 메서드 호출로 전환 기법을 실시해서 줄이자.
  • 메서드 추출이 어렵다면 메서드를 메서드 객체로 전환 기법을 실시

메서드 내용 직접 삽입 (Inline Method)

메서드 기능이 너무 단순해서 메서드명만 봐도 너무 뻔할 땐 그 메서드의 기능을 호출하는 메서드에 넣어버리고 그 메서드는 삭제하자.

이 부분이 바로 메서드 추출을 굳이 할 필요가 없는 경우이다.

public class Sample2 {
    int numberOfLateDeliveries;
    public int getRating() {
        return moreThanFiveLateDeliveries() ? 2 : 1;
    }

    private boolean moreThanFiveLateDeliveries() {
        return numberOfLateDeliveries > 5;
    }
}

메서드 삭제 후

public class Sample2 {
    int numberOfLateDeliveries;
    public int getRating() {
        return numberOfLateDeliveries > 5 ? 2 : 1;
    }
}
  • 불필요한 메서드를 삭제하니 훨씬 깔끔해졌다.
  • 리팩토링의 핵심은 의도한 기능을 한눈에 파악할 수 있는 직관적인 메서드명을 사용하는 것과 메서드를 간결하게 만드는 것이다.
  • 하지만 메서드명에 모든 기능이 반영될 정도로 메서드 기능이 지나치게 단순할 때가 있는데, 이때는 그 메서드를 제거해야 한다.

임시변수 내용 직접 삽입 (Inline Temp)

간단한 수식을 대입받는 임시변수로 인해 다른 리팩토링 기법 적용이 힘들 땐 그 임시변수를 참조하는 부분을 전부 수식으로 치환하자.

public class Sample3 {
    public boolean isBasePrice() {
        int basePrice = getBasePrice();
        return basePrice > 1000;
    }
    private int getBasePrice() {
        return 1000;
    }
}

메서드 삭제 후

public class Sample3 {
    public boolean isBasePrice() {
        return getBasePrice() > 1000;
    }
    private int getBasePrice() {
        return 1000;
    }
}
  • 이러한 리팩토링은 요즘 IDE가 자동으로 해준다..

임시변수를 메서드 호출로 전환 (Replace Temp with Query)

수식의 결과를 저장하는 임시변수가 있을 땐 그 수식을 빼내어 메서드로 만든 후, 임시변수 참조 부분을 전부 교체하자. 새로 만든 메서드는 다른 메서드에서도 호출 가능하다.

public class Sample4 {
    public double calcBasePrice(double quantity, double itemPrice) {
        double basePrice = quantity * itemPrice;
        if (basePrice > 1000) {
            return basePrice * 0.95;
        } else {
            return basePrice * 0.98;
        }
    }
}

리팩토링 후

public class Sample4 {
    public double calcBasePrice(double quantity, double itemPrice) {
        double basePrice = basePrice(quantity, itemPrice);
        if (basePrice > 1000) {
            return basePrice * 0.95;
        } else {
            return basePrice * 0.98;
        }
    }

    private double basePrice(double quantity, double itemPrice) {
        return quantity * itemPrice;
    }
}
  • basePrice 메서드는 다른 곳에서 재활용 할 수 있다.

리팩토링 후 2

public class Sample4 {
    public double calcBasePrice(double quantity, double itemPrice) {
        double basePrice = basePrice(quantity, itemPrice);
        return discountFactor(basePrice);
    }

    private static double discountFactor(double basePrice) {
        if (basePrice > 1000) {
            return basePrice * 0.95;
        } else {
            return basePrice * 0.98;
        }
    }

    private double basePrice(double quantity, double itemPrice) {
        return quantity * itemPrice;
    }
}
  • 이번에는 메서드 추출을 이용하여 if-else문 코드를 메서드로 빼내었다.
  • 최종적으로 아래와 같이 리팩토링 할 수 있다.
public class Sample4 {
    public double calcBasePrice(double quantity, double itemPrice) {
        return discountFactor(basePrice(quantity, itemPrice));
    }

    private static double discountFactor(double basePrice) {
        if (basePrice > 1000) {
            return basePrice * 0.95;
        } else {
            return basePrice * 0.98;
        }
    }

    private double basePrice(double quantity, double itemPrice) {
        return quantity * itemPrice;
    }
}
  • 임시 변수 내용을 직접 삽입한 경우이다.
  • 메서드가 한 줄로 표현된다.

직관적 임시변수 사용 (Introduce Explaining Variable)

사용된 수식이 복잡할 땐 수식의 결과나 수식의 일부분을 용도에 부합하는 직관적 이름의 임시변수에 대입하자.

public class Sample5 {
    public void methodA() {
        if ((platform.toUpperCase().indexOf("MAC") > -1) &&
                browser.toUpperCase().indexOf("IE") > -1 &&
                wasInitialized() && resize > 0) {
            // 기능 코드
        }
    }
}

리팩토링 후

public class Sample5 {
    public void methodA() {
        final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
        final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
        final boolean wasResized = resize > 0;

        if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
            // 기능 코드
        }
    }
}
  • 코드를 보면 계산하는 수식을 if문안에서 하는 것이 아닌 따로 변수를 만들어 선언하고, 해당 변수를 if문에서 사용하였다.
  • 이렇게하면 훨씬 직관적으로 기능 코드를 확인할 수 있다.

임시변수 분리 (Split Temporary Variable)

루프 변수나 값 누적용 임시변수가 아닌 임시변수에 여러번 값이 대입될 땐 각 대입마다 다른 임시변수를 사용하자.

public class Sample6 {
    double height = 3.5;
    double width = 2.5;
    public void splitTemporaryVariable() {
        double temp = 2 * (height + width);
        System.out.println("temp = " + temp);
        temp = height * width;
        System.out.println("temp = " + temp);
    }
}

리팩토링 후

public class Sample6 {
    double height = 3.5;
    double width = 2.5;
    public void splitTemporaryVariable() {
        final double perimeter = 2 * (height + width);
        System.out.println("perimeter = " + perimeter);
        final double area = height * width;
        System.out.println("area = " + area);
    }
}
  • 임시변수는 불변으로 만들고, 각 대입마다 다른 임시변수를 사용했다.
  • 여러 용도로 사용되는 변수는 각 용도별로 다른 변수를 사용하게 분리해야 한다.

매개변수로의 값 대입 제거 (Remove Assignments to Parameters)

매개변수로 값을 대입하는 코드가 있을 때 매개변수 대신 임시변수를 사용하게 수정하자.

public class Sample7 {
    public int discount(int inputVal, int quantity, int yearToDate) {
        if (inputVal > 50) inputVal -= 2;
    }
}

리팩토링 후

public class Sample7 {
    public int discount(int inputVal, int quantity, int yearToDate) {
        int result = inputVal;
        if (inputVal > 50) result -= 2;
    }
}
  • 자바 Call By Value (값을 통한 전달), 즉 값을 복사해서 전달한다.
  • 이는 어떠한 매개변수 값 변화도 호출한 루틴에 반영되지 않음을 의미한다.
  • 또한 메서드 안의 코드 자체도 혼동된다. 전달받은 객체를 나타내는 용도로만 매개변수를 사용해야지 용도의 일관성으로 인해 코드가 훨씬 이해하기 쉬워진다.
  • 하지만 매개변수를 변경하면 일관성이 깨지고, 원래의 용도를 파악하기 어려워진다.
  • 자바에선 매개변수에 갑을 대입해서는 안된다.

메서드를 메서드 객체로 전환 (Replace Method with Method Object)

지역변수 때문에 메서드 추출을 적용할 수 없는 긴 메서드가 있을 때
그 메서드 자체를 객체로 전환해서 모든 지역변수를 객체의 필드로 만들자.
그런 다음 그 메서드를 객체 안의 여러 메서드로 쪼개면 된다.

public class Order {
    public double price() {
        double primaryBasePrice;
        double secondaryBasePrice;
        double tertiaryBasePrice;
        // 긴 계산 코드;

        ...
    }
}

리팩토링 후

public class Order {
    public double price() {
        return new PriceCalculator().compute();
    }

    static class PriceCalculator {
        double primaryBasePrice;
        double secondaryBasePrice;
        double tertiaryBasePrice;

        public double compute() {
            // 계산 로직 ...
        }
    }
}

알고리즘 전환 (Substitute Algorithm)

알고리즘을 더 분명한 것으로 교체해야 할 땐 해당 메서드의 내용을 새 알고리즘으로 바꾸자.

public class Sample8 {
    public String foundPerson(String[] people) {
        for (int i = 0; i < people.length; i++) {
            if (people[i].equals("Don")) {
                return "Don";
            }
            if (people[i].equals("John")) {
                return "John";
            }
            if (people[i].equals("Kent")) {
                return "Kent";
            }
        }
        return "";
    }
}

리팩토링 후

public class Sample8 {
    public String foundPerson(String[] people) {
        List<String> candidates = Arrays.asList(new String[]{"Don", "John", "Kent"});
        for (String candidate : candidates) {
            if (candidates.contains(candidate)) {
                return candidate;
            }
        }
        return "";
    }
}
  • 라이브러리를 활용하여 더 정확하고, 간결하게 교체하였다.
  • 위 코드는 Stream을 이용하면 더 간결해질 수 있다. (넘어가겠다..)

느낀 점

메서드를 정리하는 방법들에 대해 알아보았다. 대표적으로 메서드 추출 방법이 있고, 그에 따른 임시변수를 처리한다거나 코드를 더욱 간결하게 변환하는 것도 있고.. 다양한 방법을 알아보았다.

확실히 예제를 통해 리팩토링이 어떻게 진행되는 것인지 알아보는 것이 더 이해하기가 편하다.

내용중에 잘못된 부분이 있다면 댓글 부탁드립니다!

References

댓글