Clean Code

Java 리팩토링 - 기본형을 객체로 바꾸기 (Replace Primitive with Object)

1. 알고자 하는 것

기본은 중요하다. 하지만 코드에서 기본형(Primitive Type)은 언제나 좋지만은 않다.

필요한 기능들이 많아지면 기본형만으로는 부족한 때가 온다.

기본형만으로는 버거울 때, 어떻게 해야할까

 

  • 기본형을 객체로 바꾸기 (Replace Primitive with Object)

 

 

 

2. 알게 된 것

  • 특정 데이터는 초기의 개발에는 기본형(Primitive Type) 만으로 충분히 표현 가능할 수 있다.
  • 하지만 기능이 변경되고 추가됨에 따라, 이러한 데이터를 기반으로 수행해야 하는 기능이 추가될 수 있다. (ex. type에 올바른 값이 들어오는지 validation / type의 우선순위 비교)
  • 이 경우 기본형을 그대로 사용해 추가된 기능을 구현할 수 있지만, 코드가 직관적이지 못하고 기본형 필드를 가지는 클래스가 이러한 기능을 모두 가지고 있는 기능 편애(Feature Envy)를 야기할 수 있다.
  • 데이터의 validation, 데이터의 우선순위 비교는 사실 데이터를 필드로 가지는 클래스의 책임보다는 데이터 스스로의 책임으로 두는 것이 적절하다.
  • 이 때, 기본형으로 두었던 데이터를 클래스로 감싸고 해당 클래스에서 기능을 구현한다면 직관적으로 기능을 구현하여 사용할 수 있고, 책임을 해당 클래스로 적절하게 옮겨줄 수 있다.

 

  • 주문(Order)에는 우선순위(Priority)가 존재한다.
  • 우선순위는 "low", "normal", "high", "rush" 순이다.
  • 현재 이러한 우선순위 데이터는 기본형 (String)으로 존재한다.
public class Order {

    private String priority;

    public Order(String priority) {
        this.priority = priority;
    }

    public String getPriority() {
        return priority;
    }
}

 

  • 단순히 다음과 같이 주문의 우선순위를 가져와 출력하는 기능만이 존재한다면 기본형만으로도 충분하다.
public void printOrderPriority(Order order) {
        System.out.println("priority is " + order.getPriority());
}

 

  • 하지만 order list에서 normal보다 높은 우선순위를 갖는 order의 개수를 조회하는 기능이 추가된다면?
  • 다음과 같이 우선순위를 기반으로 한 기능임에도 불구하고 priority가 기본형(String)이므로 메서드 단에서 직접 stream과 equals를 통해 로직을 처리해야 한다.
  • 또한, filter 내에서 직접 equals와 논리연산자로 우선순위를 비교하는 로직이 썩 직관적이지도 않다. (normal보다 높은 우선순위를 가진 order만을 필터링하는 로직)
public long numberOfHighPriorityOrders(List<Order> orders) {
        return orders.stream()
                .filter(o -> o.getPriority().equals("high") || o.getPriority().equals("rush"))
                .count();
}

 

  • String 타입의 Priority 필드를 객체로 만들어보자.
public class Priority {
    
    private String value;

    // 우선순위 순서 && 유효한 우선순위 value 목록
    private List<String> legalValues = List.of("low", "normal", "high", "rush");

    public Priority(String value) {
        if (legalValues.contains(value)) { // validation
            this.value = value;
        } else {
            throw new IllegalArgumentException(value);
        }
    }

    @Override
    public String toString() {
        return this.value;
    }

    private int index() {
        return legalValues.indexOf(this.value);
    }

    // Priority 간 우선순위 비교
    public boolean higherThan(Priority other) {
        return this.index() > other.index();
    }
}

 

  • 기능 1 : 생성자에서 legalValues.contains()를 통해 올바른 우선순위 값을 validation 할 수 있다.
  • 기능 2 : toString()을 오버라이딩해 기존 String 기본형처럼 priority.toString()으로 value를 반환할 수도 있다.
  • 기능 3 : higherThan() 라는 명확한 의도를 내포하는 이름을 가진 메서드를 통해 다른 Priority 객체와 우선순위를 비교할 수 있다.
  • 기능 1, 2, 3은 모두 Priority 데이터와 관련한 기능이며, 이제는 이러한 기능의 책임을 Priority가 올바르게 가진다.
public class Order {

    private Priority priority;

    public Order(String priorityValue) {
        this.priority = new Priority(priorityValue);
    }

    public Priority getPriority() {
        return this.priority;
    }
}
  • 주문 (Order) 클래스는 이제 기본형(String)의 Priority가 아닌 객체(Object)의 Priority를 필드로 가진다.
// Before
public long numberOfHighPriorityOrders(List<Order> orders) {
        return orders.stream()
                .filter(o -> o.getPriority().equals("high") || o.getPriority().equals("rush"))
                .count();
}

// After
public long numberOfHighPriorityOrders(List<Order> orders) {
        return orders.stream()
                .filter(o -> o.getPriority().higherThan(new Priority("normal")))
                .count();
}
  • 이제 Priority를 비교해 normal보다 높은 우선순위를 가진 order를 count하는 로직이 변경되었다.
  • 코드도 훨씬 짧아졌으며, higherThan(new Priority("normal"))이라는 코드 하나만으로 의도를 명확하게 표현한다.
  • 이러한 Priority 데이터를 활용한 기능은 모두 Priority라는 클래스가 온전하게 책임을 갖는다.

 

 

 

3. 정리

  • 개발 초기에는 기본형(Primitive Type)만으로 충분히 표현 가능한 데이터가 기능이 추가됨에 따라 표현이 어려워질 수 있다.
    • 데이터의 validation, 여러 데이터 간 복잡한 비교연산, 데이터의 수학적 연산 등
  • 기본형만으로 구현이 불가능하지는 않지만, 제한적인 메서드로 코드가 복잡해지고 가독성이 떨어진다.
  • 또한, 이러한 데이터를 기반으로 구성된 기능(메서드)은 해당 데이터가 책임을 가져야 하는데, 데이터를 가지는 클래스가 책임을 가지는 기능 편애(Feature Envy)를 야기할 수 있다.
  • 이럴 때, 기본형을 별도의 클래스로 감싸서 해당 클래스에서 필요한 기능을 의도가 분명한 메서드로 감싸서 구현해 가독성을 높일 수 있다.
  • 또한, 기능의 책임(=위치)을 올바르게 옮겨줄 수 있다.

 


Reference

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