리팩토링 36. 특이 케이스 추가하기

    들어가기 전

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


    리팩토링 36. 특이 케이스 추가하기 (Introduce Special Case)

    • 어떤 필드의 특정한 값에 따라 다른 동작을 하는 코드가 반복적으로 나타난다면, 해당 필드를 감싸는 '특별한 케이스'를 만들어 해당 조건을 표현할 수 있다. 
      • 위의 내용은 '임시 변수'에도 해당된다. 
    • 이런 메커니즘을 '특이 케이스 패턴'이라고 부르고 'Null Object 패턴'을 이러한 패턴의 특수한 형태라고 볼 수 있다. 
    • '임시 변수 냄새'를 제거하는 리팩토링이다. 

     

    어떤 특정한 경우에만 값이 설정되는 임시 필드가 존재하며, 임시 필드에 따라 로직이 계속 바뀌는 것이 반복되어서 코드가 지저분해지는 경우가 있다. 굳이 임시 필드가 아니더라도, 특정 값을 만족하는 경우에 다른 동작을 하는 것이 반복되는 경우가 있을 수 있다. 이 경우라면 '특이 케이스' 자체를 별도의 클래스 / 엘리먼트로 추출하는 리팩토링을 할 수 있다. 이것을 '특이 케이스 추가하기'라고 한다. 

    어떤 특정한 경우에만 값이 설정되고, 이것에 따라 바뀌는 로직이 여러 번 반복하는 경우라면 '특이 케이스' 자체를 별도의 클래스 / 엘리먼트로 추출해내는 것이고, 이런 것을 '특이 케이스 추가하기' 리팩토링이다. 


    코드

    CustomerService를 주로 리팩토링 할 것이다. 

    • customerName() 메서드를 보면 unknown이면 occupant라고 표시하고, 아니면 커스터머의 이름을 반환한다. 
    • billingPlan() 메서드도 비슷하게 동작한다. 
    • weeksDelinquent() 메서드도 살펴보면 비슷한 형태로 동작한다. 

    아래 코드를 살펴보면 위와 같은 작업을 하는 것을 알 수 있다. 결과적으로는 Customer의 이름이 'unknown'인 경우에만 특이한 작업을 하도록 유도되고 있다. 이것을 '특이 케이스 추가하기'를 통해서 리팩토링 할 수 있고, CustomerService의 코드를 보다 깔끔하게 가져갈 수 있게 된다.

    public class CustomerService {
    
        public String customerName(Site site) {
            Customer customer = site.getCustomer();
    
            String customerName;
            if (customer.getName().equals("unknown")) {
                customerName = "occupant";
            } else {
                customerName = customer.getName();
            }
    
            return customerName;
        }
    
        public BillingPlan billingPlan(Site site) {
            Customer customer = site.getCustomer();
            return customer.getName().equals("unknown") ? new BasicBillingPlan() : customer.getBillingPlan();
        }
    
        public int weeksDelinquent(Site site) {
            Customer customer = site.getCustomer();
            return customer.getName().equals("unknown") ? 0 : customer.getPaymentHistory().getWeeksDelinquentInLastYear();
        }
    }

    리팩토링 0. UnknownCustomer 클래스 생성하기

    이름이 Unknown인 경우 Customer 클래스는 다른 동작을 한다. 따라서 UnknownCustomer 클래스로 해당 특이 케이스를 추출하는 것이 좋다. 처음에 Customer의 이름이 "unknown"인 경우에는 occupant라는 이름이 된다. 따라서 생성자에서 "occupant"라는 이름을 가지도록 작성한다.

    public class UnknownCustomer extends Customer{
        public UnknownCustomer() {
            super("occupant",null,null);
        }
    }

    그리고 나머지 값들은 우선은 아무것도 제공하지 않는다고 가정하고 null로 설정한다. 이 null은 리팩토링 하는 과정에서 구체적인 값으로 변경될 것이다.


    리팩토링1. CustomerService에서 unknown 반복확인 빼기

    CustomerService에서 unknown인지를 확인하는 조건문이 반복된다. 이것을 아래와 같이 메서드로 뺄 수 있다. CustomerService 클래스의 isUnknown() 메서드를 살펴보면 Customer의 필드만 계속 참조하고 있다. 따라서 이 메서드는 Customer 클래스에 있는 것이 더 적절하기 때문에 이동한다.  인텔리제이의 Refactor → move Instance Method를 이용한다.

    // CustomerService.java
    private boolean isUnknown(Customer customer) {
        return customer.getName().equals("unknown");
    }

    이동하고 보면 isUnknown()은 Customer 클래스에서는 항상 False를 반환하도록 하고, UnknownCustomer 클래스에서는 항상 True를 반환하도록 한다.

    public class UnknownCustomer extends Customer{
    	
        ...
        
        @Override
        boolean isUnknown() {
            return true;
        }
    }
    
    
    public class Customer {
    	
        ...
    
        boolean isUnknown() {
            return false;
        }
    }

    리팩토링 2. Site에서 Customer 결정

    CustomerService는 Site가 가지고 있는 Customer 필드를 참조해서 값을 처리한다. 따라서 Site 클래스에서 Customer 필드를 어떻게 저장할지에 따라 값이 달라진다. 따라서 Site에서 Customer의 이름에 따라 Customer / UnknwonCustomer를 선택하는 코드를 추가한다.

    public class Site {
    
        private Customer customer;
    
        public Site(Customer customer) {
            this.customer = customer.getName().equals("unknown") ?
                    new UnknownCustomer() : customer;
        }
    
        ...
    }

    리팩토링 3. CustomerService의 customerName() 메서드 리팩토링

    Unknown인지 아닌지를 확인하는 로직은 이제 Site / Customer / UnknownCustomer로 넘어갔다. CustomerService에서는 지금부터는 unknown인지를 확인하지 않아도 된다. 단순히 Site 인스턴스에서 Customer를 불러와서 이름을 전달해주면 된다. 

    public String customerName(Site site) {
        return site.getCustomer().getName();
    }

    리팩토링 4. CustomerService의 billingPlan() 메서드 리팩토링

    UnknownCustomer로 특이케이스가 나누어졌다. 따라서 현재 billingPlan()의 아래 로직을 Customer / UnknownCustomer로 각각 넘겨줘야한다. 

    public BillingPlan billingPlan(Site site) {
        Customer customer = site.getCustomer();
        return customer.isUnknown() ? new BasicBillingPlan() : customer.getBillingPlan();
    }

    이 로직은 간단하게 넘길 수 있는데, UnknownCustomer가 생성될 때 BasicBillingPlan()을 가지고 생성되도록 하면 된다. 일반 Customer의 경우에는 수정할 사항이 없다. 다음과 같이 옮겨주면 된다. 

    public class UnknownCustomer extends Customer{
        public UnknownCustomer() {
            super("occupant", new BasicBillingPlan(), null);
        }
    
    	...
    }

    이 로직을 UnknownCustomer로 넘겨주면 billingPlan() 메서드는 다음과 같이 수정된다.

    public BillingPlan billingPlan(Site site) {
        return site.getCustomer().getBillingPlan();
    }

    리팩토링 5. CustomerService의 weeksDelinquent() 메서드 리팩토링

    일반적으로 PaymentHistory가 가진 객체의 getWeeksDelinquentInLastYear()를 호출해야하는데, 이 때 UnknownCustomer는 이 값이 항상 0이어야 한다.

    public int weeksDelinquent(Site site) {
        Customer customer = site.getCustomer();
        return customer.getName().equals("unknown") ? 0 : customer.getPaymentHistory().getWeeksDelinquentInLastYear();
    }

    이 때는, PaymentHistory에 Null Object 패턴을 적용해 줄 수 있다. Null Object 패턴을 적용하면 NullPaymentHistory로 생성될 수 있다.

    public class NullPaymentHistory extends PaymentHistory{
        public NullPaymentHistory() {
            super(0);
        }
    }

    그리고 UnknownCustomer는 이 NullObject를 이용해서 weeksDelinquent()에 있던 로직을 다음과 같이 옮길 수 있다.

    public class UnknownCustomer extends Customer{
        public UnknownCustomer() {
            super("occupant", new BasicBillingPlan(), new NullPaymentHistory());
        }
    }

    그리고 CustomerService의 weeksDelinquent() 메서드는 아래와 같이 단순하게 변경할 수 있다. 

    public int weeksDelinquent(Site site) {
        return site.getCustomer().getPaymentHistory().getWeeksDelinquentInLastYear();
    }

    특이케이스 추가하기 결론 

    • 특정 클래스가 특정 상태일 때마다 다른 행동을 해야하는 코드가 많다면, 해당 클래스를 상속 구조로 만들어 특이 케이스로 빼줄 수 있다.
    • 특이 케이스로 빼주면, 해당 코드의 복잡도가 특이 케이스 클래스로 넘어가기 때문에 훨씬 좋아진다. 

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

    냄새 17. 메세지 체인  (0) 2023.05.10
    냄새 16. 임시 필드  (0) 2023.05.10
    리팩토링 35. 죽은 코드 제거하기  (0) 2023.05.10
    냄새 15. 추측성 일반화  (0) 2023.05.10
    리팩토링 34. 계층 합치기  (0) 2023.05.10

    댓글

    Designed by JB FACTORY