리팩토링 21. 파생 변수를 질의 함수로 바꾸기

    들어가기 전

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


    리팩토링 21. 파생 변수를 질의 함수로 바꾸기 (Replace Derived Variable with Query)

    • 변경할 수 있는 데이터를 최대한 줄이도록 노력해야 한다. (사이드 이펙트를 가져오기 때문)
    • 계산해서 알아낼 수 있는 변수는 제거할 수 있다.
      • 계산 자체가 데이터의 의미를 잘 표현하는 경우도 있다. 
      • 변수가 제거되면, 해당 변수가 어디선가 잘못된 값으로 수정될 수 있는 가능성을 제거할 수 있다.  
    • 계산에 필요한 소스 데이터가 불변이라면, 계산 결과 역시 불변이기 때문에 해당 변수는 그대로 유지해도 괜찮음.
    • 요지는 가변 파생 변수를 필드, 로컬 등에서 모두 제거해야 한다는 것이다.

     

    파생 변수는 소스 데이터로부터 계산되어 만들어지는 변수다. 일반적으로 계산으로 만들어지는 변수를 하나의 변수에 할당하는데, 그렇게 만들어진 것이 파생 변수다. 여기서 이야기 하는 리팩토링은 파생 변수 자체도 변경 지점이 될 수 있기 때문에 변수를 함수로 추출하고, 그 변수를 사용하던 코드는 함수를 사용하도록 하자는 것이다. 

    가변 변수가 하나 줄어들면 변경되는 부분이 하나 줄어들기 때문에 사이드 이펙트가 발생할 가능성이 줄어든다. 따라서 이 방법을 통해서 변수를 줄이는 것이 좋은 방법이 된다. 

    이런 리팩토링을 적용하지 않아도 되는 경우는 소스 데이터가 불변인 경우다. 소스 데이터가 불변이면, 파생 변수도 불변이다. 불변 변수는 변경 가능성이 없으며, 따라서 사이드 이펙트가 발생하지 않을 것이기 때문에 굳이 리팩토링을 적용할 필요가 없다. 


    실습1

    아래 코드에서 discountedTotal 변수를 살펴보자. discountedTotal은 baseTotal / discount 변수에서 계산되어 만들어지는 파생 변수다. 이 때, discount는 Setter가 열려있어서 가변 변수이며, 따라서 discountedTotal 역시 가변 파생 변수다. 가변 파생 변수는 메서드를 통해서 접근하도록 하는 것이 좋다. 왜냐하면 변수 자체가 변경점이 될 수 있기 때문이다. 

    public class Discount {
    
        private double discountedTotal;
        private double discount;
        private double baseTotal;
    
        public Discount(double baseTotal) {
            this.baseTotal = baseTotal;
        }
    
        public double getDiscountedTotal() {
            return this.discountedTotal;
        }
    
        // discountedTotal은 baseTotal, discount를 이용한 계산을 통해 생성되는 값이다. → 파생변수.
        // 또한 discount가 가변 값이기 때문에 가변 파생변수인 discountedTotal을 제거해야한다.
        public void setDiscount(double number) {
            this.discount = number;
            this.discountedTotal = this.baseTotal - this.discount;
        }
    }

    먼저 메서드로 추출하려고 할 때, 아래와 같이 assert 문을 활용해서 점진적으로 변경해나가는 부분이 좋다.

    • calculatedgetDiscountedTotal() 메서드를 생성해서 수식을 제공한다.
    • getDiscountedTotal() 메서드에서 수식과 기대하는 값이 같은지 assert 문을 이용해서 확인한다. 
    public double getDiscountedTotal() {
        assert this.discountedTotal == calculatedGetDiscountedTotal();
        return this.discountedTotal;
    }
    
    private double calculatedGetDiscountedTotal() {
        return this.baseTotal - this.discount;
    }
    
    // discountedTotal은 baseTotal, discount를 이용한 계산을 통해 생성되는 값이다. → 파생변수.
    // 또한 discount가 가변 값이기 때문에 가변 파생변수인 discountedTotal을 제거해야한다.
    public void setDiscount(double number) {
        this.discount = number;
        this.discountedTotal = this.baseTotal - this.discount;
    }

    검증이 완료되면 assert문과 calculatedGetDiscountedTotal() 메서드를 제거해주고, inline 리팩토링을 이용해서 getDiscountedTotal()이 직접 수식을 나타내도록 하면 된다.  리팩토링 결과 다음과 같아 진다.

    1. 필드로 존재하던 가변 파생 변수 discountedTotal이 제거됨. (이 리팩토링의 목적)
    2. getDiscountedTotal()에서 요청할 때 마다 계산해서 값을 제공. 
    public class Discount {
    
        private double discount;
        private double baseTotal;
    
        public Discount(double baseTotal) {
            this.baseTotal = baseTotal;
        }
    
        public double getDiscountedTotal() {
            // 수식과 기대값이 같은지 assert 문으로 먼저 검증.
            // assert this.discountedTotal == calculatedGetDiscountedTotal();
            return this.baseTotal - this.discount;
        }
    
        /*private double calculatedGetDiscountedTotal() {
            return this.baseTotal - this.discount;
        }*/
    
        // discountedTotal은 baseTotal, discount를 이용한 계산을 통해 생성되는 값이다. → 파생변수.
        // 또한 discount가 가변 값이기 때문에 가변 파생변수인 discountedTotal을 제거해야한다.
        public void setDiscount(double number) {
            this.discount = number;
        }
    
    
    }

     

     

     

    그런데 이 코드 자체는 버그가 있다. 왜냐하면 setDiscount를 할 때만 discountedTotal이 계산된다. 따라서 초기값이 없다는 것을 알 수 있다. 이걸 개선하는 방법은 리팩토링 하는 과정에서 assert 문을 넣는 것이다. 예를 들어 assert this.discountedTotal == this.baseTotal - this.discount;를 사용하는 것이다. 같지 않다면 assert 문에서 코드가 실패하게 될 것이다. 그리고 getDiscountedTotal에 수식인 this.baseTotal - this.discount;를 넣어주면 된다.  Set에 있던 파생변수를 메서드로 빼버리는 것이다. 그러면 내부적으로 사용되고 있는 필드인 discountedTotal이 삭제된다. 


    실습2

    아래 코드에서 producution은 adjustment에 쌓여있는 값들의 총 합계를 내는 변수다. adjustment의 있는 값을 계산해서 만들어지기 때문에 production은 adjustment의 파생 변수다. 또한 adjustment가 추가될 때마다 변하기 때문에 production은 가변 파생 변수다. 따라서 해당 변수를 필드에서 제거하는 것이 더 좋은 방향이 될 것이다. 

    public class ProductionPlan {
    
        private double production;
        private List<Double> adjustments = new ArrayList<>();
    
        // production 변수는 미리 계산할 필요가 없다. 
        // adjustment에 모두 포함되어 있는 값이며, 거기서 파생되는 가변 파생 변수이기 때문이다.  
        public void applyAdjustment(double adjustment) {
            this.adjustments.add(adjustment);
            this.production += adjustment;
        }
    
        public double getProduction() {
            return this.production;
        }
    }

    앞서 리팩토링 하던 것과 동일하게 진행한다. 따라서 리팩토링 중간의 코드는 아래와 같이 변경된다. 

    1. 수식 검증을 위한 내부 메서드를 생성한다.
    2. assert 문을 이용해서 기대값과 수식 검증이 일치하는지 확인한다.
    3. 일치한다면 assert 문을 제거하고, 인라인 리팩토링으로 값을 리턴하도록 한다.
    public double getProduction() {
        assert this.production == calculatedProduction(); // 수식 검증 후, 맞으면 제거
        return this.production;
    }
    
    // 수식 검증을 위한 메서드
    private double calculatedProduction() {
        return adjustments.stream().mapToDouble(value -> value).sum();
    }

    정상적으로 동작하는 것을 확인한다면, 이제 다음 작업을 진행한다.

    1. 수식 검증 위한 메서드 제거
    2. 가변 파생 변수 제거 
    public class ProductionPlan {
    
        // 가변 파생 인수 제거
        // private double production;
        private List<Double> adjustments = new ArrayList<>();
    
        public void applyAdjustment(double adjustment) {
            this.adjustments.add(adjustment);
        }
    
        public double getProduction() {
            // assert this.production == calculatedProduction(); // 수식 검증 후, 맞으면 제거
            return adjustments.stream().mapToDouble(value -> value).sum();
        }
    
        // 수식 검증을 위한 메서드
        /*private double calculatedProduction() {
            return adjustments.stream().mapToDouble(value -> value).sum();
        }*/
    }

    댓글

    Designed by JB FACTORY