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
'Clean Code' 카테고리의 다른 글
Java 리팩토링 - 상속을 위임으로 바꾸기 (Replace Inherit with Delegate) (1) | 2024.01.21 |
---|---|
Java 리팩토링 - 특이 케이스 추가하기 (Introduce Special Case) (0) | 2024.01.16 |
Java 리팩토링 - 함수 인라인, 클래스 인라인 (Function Inline, Class Inline) (0) | 2024.01.14 |
Java 리팩토링 - 함수 옮기기 (Move Function) (0) | 2023.12.25 |
Java 리팩토링 - 변수 캡슐화하기 (with. Getter/Setter) (0) | 2023.12.17 |