리팩토링 27. 필드 옮기기

    들어가기 전

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


    리팩토링 27. 필드 옮기기 (Move Field)

    • 좋은 데이터 구조를 가지고 있다면, 해당 데이터에 기반한 어떤 행위를 코드로 (메서드나 함수) 옮기는 것도 간편하고 단순해진다.
    • 처음에는 타당해 보였던 의사 결정도 프로그램이 다루고 있는 도메인과 데이터 구조에 대해 더 많이 익혀나가면서 틀린 의사 결정으로 바뀌는 경우도 있다. 
      • 즉, 처음에는 옳은 결정이었으나 나중에는 틀린 결정이 될 수 있음. 
    • 필드를 옮기는 단서 : 
      • 어떤 데이터를 항상 다른 레코드와 함께 전달하는 경우
      • 어떤 레코드를 변경할 때, 다른 레코드에 있는 필드를 변경해야 하는 경우
      • 여러 레코드에 동일한 필드를 수정해야 하는 경우
      • 여기서 언급한 레코드는 클래스 / 객체로 대체할 수도 있음. 

     

    함수를 작성하는데 필요한 데이터가 여러 곳에 흩어져 있으면 함수가 해야하는 일이 복잡해 질 수 밖에 없다. 왜냐하면 필요한 데이터를 불러오기 위한 작업들을 계속 해야하기 때문이다. 따라서 함수를 덜 복잡하게 만들기 위해 필요한 필드를 적재적소로 옮기는 작업을 해야한다.

    그렇다면 어떤 경우가 필드를 옮겨야 하는 냄새로 생각해 볼 수 있을까? 

    • 어떤 데이터를 항상 다른 레코드와 함께 전달하는 경우
    • 어떤 레코드를 변경할 때, 다른 레코드에 있는 필드를 변경해야 하는 경우
    • 여러 레코드에 동일한 필드를 수정해야 하는 경우
    • 여기서 언급한 레코드는 클래스 / 객체로 대체할 수도 있음. 

    다음 경우가 될 수 있다. 하나의 예시를 들어보자. 어떤 메서드에 매개변수가 2개 있고, 각각 레코드 / 데이터 하나라고 가정해보자. 어쩌면 이렇게 함께 전달되는 매개변수는 데이터의 의미를 따져보았을 때, 하나의 클래스나 레코드로 묶어서 사용해야한다는 것으로 볼 수도 있다. 

    또한 어떤 레코드를 변경할 때, 다른 레코드에 있는 필드를 함께 변경해야하는 경우도 변경이 필요할 수 있다. 여러 레코드 클래스에 동일/비슷한 필드가 존재한다면, 해당 필드를 한 곳으로 옮겨서 변경점을 줄이는 방법이 될 수 있다. 

    중요한 부분은 '처음에 한 결정이 항상 옳은 것'은 아니라는 것이다. 처음 의사 결정과정에서 선택된 데이터 구조는 어플리케이션이 변해가면서, 틀린 의사결정이 될 수 있다. 따라서 필요하다면 데이터의 위치를 옮기는 것이 좋다.


    코드

    Customer / CustomerContract 클래스가 각각 존재한다. 현재는 Customer 클래스에서 할인율을 계산해서 변경하고 있다. 그런데 추후 CustomerContract에서 할인율을 계산해야한다고 가정해보자. 그렇다면 discountRate 같은 값들은 CustomerContract로 넘어가야한다. 만약 이런 변경사항을 고려하지 않고, 필드를 그대로 둔다고 가정하면 추후에 하나의 기능을 수정하기 위해서 수많은 코드를 수정해야 할 수 있다. 이 작업은 코드를 응집시키는 작업으로 볼 수 있다.

    // 추후 DiscountRate는 CustomerContract에서 계산되는 것이 더 타당하다.
    public class Customer {
    
        private String name;
        private double discountRate;
        private CustomerContract contract;
    
        public Customer(String name, double discountRate) {
            this.name = name;
            this.discountRate = discountRate;
            this.contract = new CustomerContract(dateToday());
        }
    
        public double getDiscountRate() {
            return discountRate;
        }
    
        public void becomePreferred() {
            // 필드에 직접 접근하는 부분을 모두 getter / setter로 감싼다
            this.discountRate += 0.03;
            // 다른 작업들
        }
    
        public double applyDiscount(double amount) {
            BigDecimal value = BigDecimal.valueOf(amount);
            // 필드에 직접 접근하는 부분을 모두 getter / setter로 감싼다.
            return value.subtract(value.multiply(BigDecimal.valueOf(this.discountRate))).doubleValue();
        }
    
        private LocalDate dateToday() {
            return LocalDate.now();
        }
    }

    다음부터는 필드를 옮겨가는 절차를 살펴본다.

    1. 옮겨가야 하는 필드가 있는 경우, 현재 클래스에서 필드에 직접 접근하는 코드를 getter / setter로 감싸서 접근하도록 바꿔준다. 그 이후에 필드를 옮겨야 안전하게 옮길 수 있기 때문이다. 
    2. Customer 클래스에서 CustomerContract 인스턴스를 생성하는 코드에 discountRate를 추가한다. 그러면 컴파일 에러가 발생하고, 이를 통해 CustomerContract 생성자 코드에 discountRate를 추가할 수 있다. 
    3. CustomerContract에 discountRate 필드를 생성한다. 그리고 discountRate를 위한 Getter / Setter를 생성한다.
    4. Customer 클래스에서 discountRate 필드를 삭제해준다. 
    5. Customer 클래스에서 사용하고 있던 getter / setter 메서드에 컴파일 에러가 발생하는데, 이 부분을 수정해준다. 

    이런 순서대로 코드를 수정해주면, 필드 옮기기 리팩토링을 완성할 수 있게 된다.

    // 리팩토링 완료
    public class Customer {
    
        private String name;
        private CustomerContract contract;
    
        public Customer(String name, double discountRate) {
            this.name = name;
            this.contract = new CustomerContract(dateToday(), discountRate);
        }
    
        public double getDiscountRate() {
            return contract.getDiscountRate();
        }
    
        public void setDiscountRate(double discountRate) {
            this.contract.setDiscountRate(discountRate);
        }
    
        public void becomePreferred() {
            // 필드에 직접 접근하는 부분을 모두 getter / setter로 감싼다
            setDiscountRate(this.getDiscountRate() + 0.03);
            // this.discountRate += 0.03;
            // 다른 작업들
        }
    
        public double applyDiscount(double amount) {
            BigDecimal value = BigDecimal.valueOf(amount);
            // 필드에 직접 접근하는 부분을 모두 getter / setter로 감싼다.
            return value.subtract(value.multiply(BigDecimal.valueOf(this.getDiscountRate()))).doubleValue();
        }
    
        private LocalDate dateToday() {
            return LocalDate.now();
        }
    }
    
    
    public class CustomerContract {
    
        private double discountRate;
        private LocalDate startDate;
    
        public CustomerContract(LocalDate startDate, double discountRate) {
            this.startDate = startDate;
            this.discountRate = discountRate;
        }
    
        public double getDiscountRate() {
            return discountRate;
        }
    
        public void setDiscountRate(double discountRate) {
            this.discountRate = discountRate;
        }
    
        public LocalDate getStartDate() {
            return startDate;
        }
    
        public void setStartDate(LocalDate startDate) {
            this.startDate = startDate;
        }
    }

    FuthreMore

    리팩토링이 완료되었는데, Customer의 applyDiscount()의 위치가 살짝 아쉽다. 왜냐하면 applyDiscount가 참조하는 변수는 Customer 클래스에 있는 값이 전혀 없고, CustomerContract에 있는 discountRate를 참조하기 때문이다. 이것은 applyDiscount의 위치가 CustomerContract 클래스에 있는게 더 맞다는 것을 암시한다. 

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

    리팩토링 29. 클래스 인라인  (0) 2023.05.10
    리팩토링 28. 함수 인라인  (0) 2023.05.10
    냄새 8. 산탄총 수술  (0) 2023.05.10
    리팩토링 26. 함수 옮기기  (0) 2023.05.10
    리팩토링 25. 함수 옮기기  (0) 2023.05.10

    댓글

    Designed by JB FACTORY