냄새 9. 기능 편애

    들어가기 전

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


    냄새 9. 기능 편애(Feature Envy)

    • 어떤 모듈에 있는 함수가 다른 모듈에 있는 데이터나 함수를 더 많이 참조하는 경우에 발생한다.
      • 이것은 특정 함수가 기능을 너무 욕심내서, 특정 함수에 많은 기능이 있는 것이다.
      • 예) 다른 객체의 getter를 여러 개 사용하는 메서드
    • 관련 리팩토링 기술
      • '함수 옮기기'를 사용해서 함수를 적절한 위치로 옮긴다. (참조하는 데이터/메서드가 많은 클래스 쪽)
      • 함수 일부분만 다른 곳의 데이터와 함수를 많이 참조한다면 '함수 추출하기'로 함수를 나눈 다음에 함수를 옮길 수 있다. 
    • 만약에 여러 모듈(클래스)를 참조하고 있다면
      1. 가장 많은 데이터를 참조하는 곳으로 옮김.
      2. 함수를 여러 개로 쪼개서, 각 클래스로 분산 
    • 데이터와 해당 데이터(필드)를 참조하는 행동(메서드)을 같은 곳(클래스)에 두도록 하자.
    • 이 기준의 예외는 데이터와 행동을 분리한 디자인 패턴 (전략 패턴, 방문자 패턴)을 적용할 수도 있다. 

     

    어떤 함수가 욕심을 내다보면, 다른 모듈이 가지고 있어야 할 기능 조차 특정 함수가 가지고 있는 경우가 있다. 이런 함수의 특징은 다른 클래스의 변수 / 함수를 많이 참조하는 경우다. 이 함수는 아마도 적절한 위치에 있는 것이 아닐 수 있다. 혹은 함수의 특정 부분만 다른 클래스를 많이 참조한다면, 그 부분을 함수로 추출한 후, 다른 클래스로 이동시켜주는 것이 좋을 수 있다. 

     

    기본적인 처리 방법

    만약에 어떤 함수가 다른 여러 모듈(클래스)를 참조한다면, 두 가지 방법으로 처리할 수 있다.

    1. 함수를 통째로 가장 많이 참조하는 클래스로 옮김.
    2. 함수를 여러 개로 나눈 다음, 각 함수를 관련된 클래스로 옮김.

    옮기는 기준은 참조하는 함수 / 데이터가 된다. 모듈화를 할 때 가장 기본적인 룰은 관련있는 데이터와 행동(함수)는 같은 곳에 두는 것이다. 그런데 항상 적용되지는 않는데, 행동이 여러 조건에 따라 바뀌어야 한다면 행동만을 데이터로부터 따로 분리할 수 있다. (Strategy, Visitor 패턴는 예외 경우임.)


    코드

    아래 코드를 살펴보면 Bill 클래스가 기능에 욕심을 내고 있다.

    • 참조 관점에서 봤을 때, Bill 클래스는 calculateBill() 안에서 자신의 필드 / 메서드를 아무것도 참조하지 않고 다른 클래스의 메서드만 참조하고 있다. 따라서 이 관점에서 calculateBill()은 기능 편애의 냄새를 풍긴다.
    • 문맥 관점에서도 계산지 클래스에서 전기세 / 가스세를 직접 구하고 있다. 전기세 / 가스세는 전기 사용량 / 가스 사용량 클래스에서 직접 구하는 것이 더욱 문맥적으로 맞을 수 있다. 

    따라서 해당 클래스는 기능 편애의 냄새가 나기 때문에 리팩토링이 필요하다. 리팩토링은 다음과 같이 진행한다.

    1. 각 계산하는 메서드는 각기 다른 클래스에 있어야 한다. 일차적으로 Bill 클래스에서 메서드로 추출한다.
    2. 추출한 메서드를 각 클래스로 옮긴다.
    3. 문맥을 점검한다. 
    public class Bill {
    
        private ElectricityUsage electricityUsage;
    
        private GasUsage gasUsage;
    
        // Bill 클래스가 electiryUsage / GasUsage의 메서드를 많이 참조한다. (심지어 자기것은 없음)
        // 기능 편애의 냄새가 난다.
        public double calculateBill() {
            var electicityBill = electricityUsage.getAmount() * electricityUsage.getPricePerUnit();
            var gasBill = gasUsage.getAmount() * gasUsage.getPricePerUnit();
            return electicityBill + gasBill;
        }
    
    }

    리팩토링의 중간 단계는 다음과 같다. 코드를 그대로 옮기기보다는 관련된 부분만 '함수로 추출하기'를 한 후에, 해당 함수를 클래스로 옮겨주는 것이다. 

    public class Bill {
    
        private ElectricityUsage electricityUsage;
    
        private GasUsage gasUsage;
    
        // Bill 클래스가 electiryUsage / GasUsage의 메서드를 많이 참조한다. (심지어 자기것은 없음)
        // 기능 편애의 냄새가 난다.
        // 각각을 메서드로 추출한 후, 해당하는 클래스로 옮긴다.
        public double calculateBill() {
            double electicityBill = getElecticityBill();
            double gasBill = getGasBill();
            return electicityBill + gasBill;
        }
    
        private double getGasBill() {
            var gasBill = gasUsage.getAmount() * gasUsage.getPricePerUnit();
            return gasBill;
        }
    
        private double getElecticityBill() {
            var electicityBill = electricityUsage.getAmount() * electricityUsage.getPricePerUnit();
            return electicityBill;
        }
    
    }
    

    최종 수정

    최종 수정의 결과로 다음과 같이 코드가 변경된다. 

    public class Bill {
    
        private ElectricityUsage electricityUsage;
        private GasUsage gasUsage;
    
        public double calculateBill() {
            return electricityUsage.getElecticityBill() + gasUsage.getGasBill();
        }
    }
    
    
    public class ElectricityUsage {
    
        private double amount;
    
        private double pricePerUnit;
    
        public ElectricityUsage(double amount, double pricePerUnit) {
            this.amount = amount;
            this.pricePerUnit = pricePerUnit;
        }
    
        public double getElecticityBill() {
            return this.getAmount() * this.getPricePerUnit();
        }
        public double getAmount() {
            return amount;
        }
    
        public double getPricePerUnit() {
            return pricePerUnit;
        }
    }
    
    public class GasUsage {
    
        private double amount;
    
        private double pricePerUnit;
    
        public GasUsage(double amount, double pricePerUnit) {
            this.amount = amount;
            this.pricePerUnit = pricePerUnit;
        }
    
        public double getGasBill() {
            return this.getAmount() * this.getPricePerUnit();
        }
        public double getAmount() {
            return amount;
        }
    
        public double getPricePerUnit() {
            return pricePerUnit;
        }
    }

     

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

    냄새 11. 기본형 집착  (0) 2023.05.10
    냄새 10. 데이터 뭉치 (Data Clumps)  (0) 2023.05.10
    리팩토링 29. 클래스 인라인  (0) 2023.05.10
    리팩토링 28. 함수 인라인  (0) 2023.05.10
    리팩토링 27. 필드 옮기기  (0) 2023.05.10

    댓글

    Designed by JB FACTORY