Java 리팩토링 - 특이 케이스 추가하기 (Introduce Special Case)
Clean Code

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

1. 알고자 하는 것

여러 메서드에서 특정 데이터를 기준으로 계속 로직이 분기된다면, 이를 아예 클래스로 분리해 코드의 길이를 대폭 줄일 수 있다.

그리고 그 중심에는 다형성이 있다. 

무궁무진한 다형성의 활용도를 익히기 위해 더 열심히 공부해야겠다는 괜한 동기부여가 되는 주제인 것 같다.

 

  • 특이 케이스 추가하기 (Introduce Special Case)

 

 

 

2. 알게 된 것

  • 위에서 말한 "특정 데이터를 기준으로 계속 로직이 분기" 되는 상황을 특이 케이스 (Special Case)라고 정의할 수 있다.
  • 가령, 다음과 같은 형식이다.
public class Customer {
    private String name;
    private BillingPlan billingPlan;
    private PaymentHistory paymentHistory;
    ...
}


public class CustomerService {

    public String customerName(Site site) {
        Customer customer = site.getCustomer();

        String customerName;
        // Customer의 Name이 unknown인지에 따른 분기 (특이 케이스)
        if (customer.getName().equals("unknown")) {
            customerName = "occupant";
        } else {
            customerName = customer.getName();
        }

        return customerName;
    }

    public BillingPlan billingPlan(Site site) {
        Customer customer = site.getCustomer();
        // Customer의 Name이 unknown인지에 따른 분기 (특이 케이스)
        return customer.getName().equals("unknown") ? new BasicBillingPlan() : customer.getBillingPlan();
    }

    public int weeksDelinquent(Site site) {
        Customer customer = site.getCustomer();
        // Customer의 Name이 unknown인지에 따른 분기 (특이 케이스)
        return customer.getName().equals("unknown") ? 0 : customer.getPaymentHistory().getWeeksDelinquentInLastYear();
    }

}

 

  • Customer의 이름이 'unknown' 인지를 지겹도록 확인한다.
  • 3개 메서드 모두 'unknown' 여부에 따라 반환하는 값이 다르다.
  • 이 때, 'unknown'에 해당하는 Customer를 Customer를 상속받는 UnknownCustomer 클래스로 분리해서 unknown일 때 반환하는 값들을 필드로 가지게 할 수 있다.

 

public class UnknownCustomer extends Customer {

    public UnknownCustomer() {
        super("unknown", null, null);
    }

    @Override
    public boolean isUnknown() {
        return true;
    }

    @Override
    public String getName() {
        return "occupant";
    }

    @Override
    public BillingPlan getBillingPlan() {
        return new BasicBillingPlan();
    }
}
  • 이렇게 하면, 이제 Site에서는 다형성을 활용해 'unknown' 여부에 따라 Customer를 가지고 있을 수 있다.
public class Site {

    private Customer customer;

    public Site(Customer customer) {
        // UnknownCustomer를 만들었으므로 분기해서 저장 가능
        this.customer = customer.isUnknown() ? new UnknownCustomer() : customer;
    }

    public Customer getCustomer() {
        return customer;
    }
}
  • 이제 CustomerService는 다형성을 통해 단순히 Site가 가지는 Customer의 데이터를 반환하면 된다.
    • 이미 Site가 가지는 Customer에서 unknown / 그 외일 때 데이터를 각각 구분지어 가지고 있다.
public class CustomerService {

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

//        String customerName;
//        if (customer.getName().equals("unknown")) {
//            customerName = "occupant";
//        } else {
//            customerName = customer.getName();
//        }
//
//        return customerName;
    }

    public BillingPlan billingPlan(Site site) {
        return site.getCustomer().getBillingPlan();
//        return customer.getName().equals("unknown") ? new BasicBillingPlan() : customer.getBillingPlan();
    }

    public int weeksDelinquent(Site site) {
        // NPE 발생!!!!
        return site.getCustomer().getPaymentHistory().getWeeksDelinquentInLastYear();
//        return customer.getName().equals("unknown") ? 0 : customer.getPaymentHistory().getWeeksDelinquentInLastYear();
    }

}

 

  • 이 때, 문제가 되는 부분은 Customer가 가지고 있는 필드인 PaymentHistory인데, 'unknown'의 경우에는 PaymentHistory를 가지고 있지 않다(null).
  • 따라서 마지막 메서드에서도 다른 메서드와 같이 getCustomer().getPaymentHistory().getWeeks..() 를 반환하려고 하면 NPE가 발생한다.
  • 이 때에는 null인 경우를 또 다른 특이 케이스로 판단하고 null에 대응하는 클래스를 분리할 수 있는데, 이를 Null Object 패턴이라고 한다.
  • PaymentHistory를 상속받는 NullPaymentHistory 클래스를 만들고, 해당 클래스는 weeksDelinquentInLastYear의 값을 0으로 설정해주면 된다.
public class NullPaymentHistory extends PaymentHistory {

    public NullPaymentHistory() {
        super(0);
    }
}

public class UnknownCustomer extends Customer {

    public UnknownCustomer() {
        super("unknown", null, new NullPaymentHistory()); // NullPaymentHistory 설정
    }
    ...
}
  • 이제 Customer가 'unknown' 이라도 안전하고 쉽게 CustomerService에서 weeksDelinquentInLastYear를 찾을 수 있다.
    public int weeksDelinquent(Site site) {
        // OK
        return site.getCustomer().getPaymentHistory().getWeeksDelinquentInLastYear();
//        return customer.getName().equals("unknown") ? 0 : customer.getPaymentHistory().getWeeksDelinquentInLastYear();
    }

 

 

 

 

 

3. 정리

  • 여러 메서드에서 특정 데이터를 기준으로 계속 로직이 분기된다면(특이 케이스), 이를 아예 클래스로 분리하고 상속과 다형성을 활용해 코드의 길이를 대폭 줄일 수 있다.
  • 분기에 따라 가지는 값을 새로 분리한 클래스의 필드로 가지게 한다.
  • 이를 사용하는 로직에서는 단순히 다형성을 활용해 클래스가 가지는 값을 반환하게 하면 된다.
  • 분기에 따라 특정 필드가 null인 경우, 이 역시도 특이 케이스로 생각하고 null 조건에 해당하는 클래스를 분리할 수 있다. (Null Object 패턴)

 


Reference

강의 - 코딩으로 학습하는 리팩토링 (백기선 강사님)