리팩토링 25. 함수 옮기기

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다. 


    리팩토링 25. 함수 옮기기 (Move Function)

    • 함수 옮기기의 필요성.
      • 모듈화가 잘 된 소프트웨어는 최소한의 지식만으로 프로그램을 변경할 수 있다.
      • 관련있는 함수나 필드가 모여있어야 더 쉽게 찾고 이해할 수 있다. (모듈화가 잘 되어있음)
      • 하지만 관련있는 함수나 필드가 항상 고정적인 것은 아니기 때문에 때에 따라 옮겨야 할 필요가 있다. 
    • 함수를 옮겨야 하는 경우
      • 해당 함수가 다른 문맥 (클래스)에 있는 데이터 (필드)를 더 많이 참조하는 경우.
      • 보조하는 함수가 있을 때, 이 함수를 다른 클래스에서도 사용하는 경우 다른 클라이언트 (클래스)에서도 필요로 하는 경우.
    • 함수를 옮겨갈 새로운 문맥 (클래스)이 필요한 경우에는 '여러 함수를 클래스로 묶기' 또는 '클래스 추출하기'를 사용한다.
    • 함수를 옮길 적당한 위치를 찾기가 어렵다면, 그대로 두어도 괜찮다. 언제든 나중에 옮길 수 있다. 
    • 함수를 옮길 때 매개변수를 어떻게 넘길지 고민해야한다.
      • 각각을 그대로 넘김
      • 객체 전체를 넘김 → 순환참조 고려. (순환참조는 코드가 읽기 어려워짐)

    좋은 모듈화

    특정 기능을 추가할 때, 여러 클래스 및 함수를 많이 살펴봐야만 한다면 이것은 모듈화가 잘 되어있지 않은 것을 의미한다.  응집도가 낮다는 것을 의미하는데, 응집도가 높으면 한 곳에 필요한 문맥들이 잘 모여있기 때문에 빠르게 파악할 수 있고, 변경 사항을 금방 만들 수 있게 된다. 


    함수를 옮겨야 하는 경우?

    좋은 모듈화를 하기 위해서는 때에 따라서는 함수를 적절한 곳으로 옮겨주어야 한다. 

    1. 어떤 함수가 사용하는 데이터가 현재 클래스보다 다른 클래스의 데이터를 더 많이 사용할 때. 
    2. 보조하는 함수가 있을 때, 이 함수를 다른 클래스에서도 사용하는 경우 → 외부에서 공통으로 사용할 클래스로 빼내기 

    함수를 옮길 때는 두 가지 선택지가 있을 것이다.

    • 다른 클래스로 옮김.
    • 여러 함수를 클래스로 묶어서 처리 

    만약 함수를 옮기려고 할 때 고민이 많이 된다면, 지금 당장 옮기지 않아도 된다. 왜냐하면 그런 종류의 고민이라면 이동을 고려하는 메서드는 사실 어떤 클래스에 있어도 무방하다고 볼 수 있기 때문이다. 필요한 시점에 나중에 옮겨도 된다. 


    코드 보기

    Account 클래스의 overdraftCharge() 메서드를 살펴보자. 해당 메서드의 참조를 살펴보면 다음과 같다.

    • AccountType의 메서드 1개
    • Account 클래스의 필드 1개 

    이런 상태이기 때문에 overdraftCharge() 메서드는 Account / AccountType 클래스 어디에 있어도 무방하다. 하지만 필요성이 생겨 해당 메서드를 AccountType으로 옮긴다고 가정해보자. 

    AccountType으로 메서드를 옮기게 되면 overdraftCharge() 메서드는 daysOverDrawn이라는 값을 전달받아야 한다. 이 때 고려해야할 부분은 daysOverDrawn이라는 변수를 어떻게 넘길 것인지다. 

    • daysOverDrawn만 넘긴다
    • Account 인스턴스를 넘긴다. 

    그런데 현재 Account가 AccountType을 참조하고 있기 때문에 AccountType에 Account를 넘기면 순환참조가 발생한다. 순환참조는 코드를 읽기 어렵게 만든다. 따라서 Account 객체를 통째로 넘기기보다는 daysOverDrawn이라는 매개변수를 넘기는 것이 좋다. 

     

    public class Account {
    
        private int daysOverdrawn;
        private AccountType type;
    
        public Account(int daysOverdrawn, AccountType type) {
            this.daysOverdrawn = daysOverdrawn;
            this.type = type;
        }
    
        public double getBankCharge() {
            double result = 4.5;
            if (this.daysOverdrawn() > 0) {
                result += this.overdraftCharge();
            }
            return result;
        }
    
        private int daysOverdrawn() {
            return this.daysOverdrawn;
        }
    
        // type의 메서드를 참조한다. 아래의 경우는 반반이지만, 이런 경우라도 메서드 이동을 고려해볼 수 있음.
        // type으로 메서드를 옮기게 되면 daysOverdrawn 매개변수를 받아야 한다.
        // 이 때 daysOrverdrawn 매개변수를 받을지, Account 타입을 받을지를 고려해야함. Account 타입은 새로운 의존성이 생기는 것임.
        // Account -> Account Type을 참조하는데, 만약 Account Type도 Account를 참조한다면, 이것은 순환참조가 될 수 있음. 
        // 따라서 daysOverDrawn 값만 넘기는 것이 좋다. 
        private double overdraftCharge() {
            if (this.type.isPremium()) {
                final int baseCharge = 10;
                if (this.daysOverdrawn <= 7) {
                    return baseCharge;
                } else {
                    return baseCharge + (this.daysOverdrawn - 7) * 0.85;
                }
            } else {
                return this.daysOverdrawn * 1.75;
            }
        }
    }

    리팩토링 결과

    리팩토링을 완료하면 다음과 같이 코드가 변경된다.

    public class Account {
    
        private int daysOverdrawn;
    
        private AccountType type;
    
        public Account(int daysOverdrawn, AccountType type) {
            this.daysOverdrawn = daysOverdrawn;
            this.type = type;
        }
    
        public double getBankCharge() {
            double result = 4.5;
            if (this.daysOverdrawn() > 0) {
                result += type.overdraftCharge(this.daysOverdrawn);
            }
            return result;
        }
    
        private int daysOverdrawn() {
            return this.daysOverdrawn;
        }
    
        // type의 메서드를 참조한다. 아래의 경우는 반반이지만, 이런 경우라도 메서드 이동을 고려해볼 수 있음.
        // type으로 메서드를 옮기게 되면 daysOverdrawn 매개변수를 받아야 한다.
        // 이 때 daysOrverdrawn 매개변수를 받을지, Account 타입을 받을지를 고려해야함. Account 타입은 새로운 의존성이 생기는 것임.
        // Account -> Account Type을 참조하는데, 만약 Account Type도 Account를 참조한다면, 이것은 순환참조가 될 수 있음.
        // 따라서 daysOverDrawn 값만 넘기는 것이 좋다.
    }
    
    
    public class AccountType {
        private boolean premium;
    
        public AccountType(boolean premium) {
            this.premium = premium;
        }
    
        public boolean isPremium() {
            return this.premium;
        }
    
        public double overdraftCharge(double daysOverdrawn) {
            if (this.isPremium()) {
                final int baseCharge = 10;
                if (daysOverdrawn <= 7) {
                    return baseCharge;
                } else {
                    return baseCharge + (daysOverdrawn - 7) * 0.85;
                }
            } else {
                return daysOverdrawn * 1.75;
            }
        }
    }

    'etc > 리팩토링' 카테고리의 다른 글

    냄새 8. 산탄총 수술  (0) 2023.05.10
    리팩토링 26. 함수 옮기기  (0) 2023.05.10
    리팩토링 24. 단계 쪼개기  (0) 2023.05.10
    냄새 7. 뒤엉킨 변경  (0) 2023.05.10
    리팩토링 23. 참조를 값으로 바꾸기  (0) 2023.05.02

    댓글

    Designed by JB FACTORY