[Refactoring] 기본형 집착 (Primitive Obsession)

서준교·2023년 11월 20일
0

Refactoring

목록 보기
1/8
post-thumbnail

👉 이 게시글은 inflearn의 코딩으로 학습하는 리팩토링 강의 내용을 바탕으로 작성되었습니다.

애플리케이션에서 프로그램을 만들 때 int, double, string과 같은 프로그래밍 언어가 제공하는 원시 타입(primitive type)을 사용하는 경우가 대부분입니다. 하지만 이러한 타입만으로는 다양한 도메인에서 필요로 하는 특수한 표기법(전화번호, 좌표, 돈, 범위, 수량 등)을 모두 표현하기 어렵습니다. 이러한 경우에는 어떻게 해야 할까요?

이 smell은 다음과 같은 리팩토링 기술을 적용하여 해결할 수 있습니다.

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

개발 초기에는 기본형으로 표현한 데이터가 나중에는 해당 데이터와 관련있는 다양한 기능을 필요로 하는 경우가 발생합니다. 숫자로 표현하던 온도의 단위를 변환하는 경우, 문자열로 표현하던 전화번호의 지역 코드가 필요하거나 다양한 포맷을 지원하는 경우가 예입니다. 기본형을 사용한 데이터를 감싸 필요로 하는 기능을 추가하는 리팩토링을 통해 해결해봅시다.

public class Order {

    private String priority;

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

    public String getPriority() {
        return priority;
    }
}
public class OrderProcessor {

    public long numberOfHighPriorityOrders(List<Order> orders) {
        return orders.stream()
                .filter(o -> o.getPriority() == "high" || o.getPriority() == "rush")
                .count();
    }
}

기존 코드를 보면 Order 클래스 내부의 priority 변수를 바탕으로 "high", "rush" 문자열과 일치하는 우선 순위만을 count하고 있습니다. 해당 코드의 문제점은 우선 순위와 관련없는 문자열의 입력을 허용하여 type safety가 보장되지 않고 있고, 원하는 우선 순위를 필터링하는 로직이 직관적이지 않다는 점입니다. 원시 타입의 priority 변수를 클래스로 정의하여 코드를 개선해봅시다.

public class Priority {
    private String value;

    private List<String> legalValues = List.of("low", "normal", "high", "rush");
    
    public Priority(String value) {
        if (this.legalValues.contains(value))
            this.value = value;
        else
            throw new IllegalArgumentException("illegal value for priority " + value);
    }

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

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

    public boolean higherThan(Priority other) {
        return this.index() > other.index();
    }
}

새로 정의한 Priority 클래스입니다. 리스트에 입력을 허용하고자 하는 문자열을 미리 담아두고, 생성자에서 유효한 문자열인지 검증하는 로직을 예외처리를 통해 추가하여 type safety를 보장합니다. 리스트의 값을 통해 얻어온 인덱스를 바탕으로 우선 순위를 결정하는 메소드를 추가로 정의하여 보다 직관적으로 우선 순위를 판단하는 코드를 작성할 수 있게 되었습니다.


public class Order {
    private Priority priority;

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

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

    public Priority getPriority() {
        return priority;
    }
}
public class OrderProcessor {

    public long numberOfHighPriorityOrders(List<Order> orders) {
        return orders.stream()
                .filter(o -> o.getPriority().higherThan(new Priority("normal")))
                .count();
    }
}

Priority 클래스가 새로 정의됨에 따라 기존의 priority 변수가 클래스로 대체되고, OrderProcessor 클래스에서 higherThan 메소드를 사용하여 보다 직관적으로 우선 순위를 비교할 수 있게 되었습니다.

조건부 로직을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)

상속 구조를 활용하여 조건 로직을 하위 클래스로 나누는 리팩토링입니다. 해당 리팩토링을 거치면 복잡한 조건식을 상속과 다형성을 사용해 코드를 보다 명확하게 분리할 수 있어 보다 가독성있는 코드를 만들 수 있게 됩니다.

기본 동작과 특수한 기능이 섞여있는 경우, 상속 구조를 만들어서 기본 동작을 상위 클래스에 두고 특수한 기능을 하위 클래스로 옮겨서 각 타입에 따른 차이점을 강조할 수 있다는 장점이 있습니다.

기존의 Employee 클래스 내부 메소드는 employee의 종류(full-time, part-time, temporal)에 따른 조건문을 통해 값을 반환합니다. 기존의 클래스를 추상 클래스로 변경하고, employee의 종류를 표현하는 FullTimeEmployee, PartTimeEmployee, TemporalEmployee 클래스를 각각 생성하여 추상 클래스를 상속받도록 합니다.
또한, 자식 클래스가 추상 클래스의 변수를 상속받아야 하기 때문에 접근제어자를 private -> protected로 변경합니다.

추가된 클래스는 다음과 같습니다.


public class FullTimeEmployee extends Employee {
    public FullTimeEmployee(List<String> availableProjects) {
        super(availableProjects);
    }

    @Override
    public int vacationHours() {
        return 120;
    }

    @Override
    public boolean canAccessTo(String project) {
        return true;
    }
}
public class PartTimeEmployee extends Employee {
    public PartTimeEmployee(List<String> availableProjects) {
        super(availableProjects);
    }

    @Override
    public int vacationHours() {
        return 80;
    }
}
public class TemporalEmployee extends Employee {
    public TemporalEmployee(List<String> availableProjects) {
        super(availableProjects);
    }

    @Override
    public int vacationHours() {
        return 32;
    }
}

추상클래스의 메소드를 오버라이딩하여 조건에 따른 반환값을 각각의 클래스 메소드 내부에 직접 설정하는 방식으로 리팩토링을 진행할 수 있습니다.

특정 조건에 해당하는 로직을 처리해야 하는 경우에도 Factory 클래스를 통해 리팩토링을 진행할 수 있습니다.

ChinaExperiencedVoyageRating 클래스를 생성하여 특정 조건이 충족되었을 때 실행해야 하는 로직을 메소드 오버라이딩을 통해 해당 클래스로 모두 옮겼고, RatingFactory 클래스에서 해당 조건을 기반으로 어떠한 클래스를 생성할 지 여부를 결정하도록 하였습니다.

public class ChinaExperiencedVoyageRating extends VoyageRating {
    public ChinaExperiencedVoyageRating(Voyage voyage, List<VoyageHistory> history) {
        super(voyage, history);
    }

    @Override
    protected int captainHistoryRisk() {
        int result = super.captainHistoryRisk() - 2;
        return Math.max(result, 0);
    }

    @Override
    protected int voyageProfitFactor() {
        return super.voyageProfitFactor() + 3;
    }

    @Override
    protected int voyageLengthFactor() {
        int result = 0;
        result += 3;
        if (this.voyage.length() > 12) result += 1;
        if (this.voyage.length() > 18) result -= 1;
        return result;
    }

    @Override
    protected int historyLengthFactor() {
        return (this.history.size() > 10) ? 1 : 0;
    }
}
public class RatingFactory {
    public static VoyageRating createRating(Voyage voyage, List<VoyageHistory> history) {
        if (voyage.zone().equals("china") && hasChinaHistory(history)) {
            return new ChinaExperiencedVoyageRating(voyage, history);
        } else {
            return new VoyageRating(voyage, history);
        }
    }

    private static boolean hasChinaHistory(List<VoyageHistory> history) {
        return history.stream().anyMatch(v -> v.zone().equals("china"));
    }
}

해당 리팩토링을 통해서 특정 조건에만 생성해야 할 클래스를 명확히 구분할 수 있어 코드의 가독성을 향상시킬 수 있고, 해당 클래스의 목적에 부합하는 메소드로만 구성했기 때문에 응집도 또한 향상되었습니다.

profile
매일 성장하는 개발자가 되고 싶습니다. 😊

0개의 댓글