본문 바로가기
공부 기록

[리팩토링 1판] Chapter 09. 조건문 간결화

by 매트(Mat) 2024. 4. 4.

Chapter 09. 조건문 간결화

조건문은 복잡해질 가능성이 높은 만큼, 리팩토링 기법이 다양하다.

그 중 핵심적인 기법은 조건문을 여러 개로 나누는 조건문 쪼개기다. 이 기법은 세부 기능에서 스위칭 로직을 분리하므로 중요하다.

조건문 쪼개기

복잡한 조건문이 있을 땐

  • if, else-if, else 부분을 각각 메서드로 뺴자.

public class Sample1 {
    public void methodA() {
        if (data.before(SUMMER_START) || date.after(SUMMER_END)) {
            charge = quantity + winterRate + winterServiceCharge;
        } else {
            charge = quantity + summerRate;
        }
    }
}

리팩토링 후

public class Sample1 {
    public void methodA() {
        if (notSummer(date)) {
            charge = winterCharge(quantity);
        } else {
            charge = sumerCharge(quantity);
        }
    }
}
  • 복잡한 다양한 조건문은 메서드가 길어지면서 알아보기가 힘들다.
  • 따라서 긴 코드들은 잘 쪼개야 하며, 메서드 분리를 통해 더 코드가 더 간결해질 수 있다.

중복 조건식 통합

여러 조건 검사식의 결과가 같을 땐

  • 하나의 조건문으로 합친 후 메서드로 빼내자.

public class Sample2 {
    public double disabilityAmount() {
        if (seniority < 2) return 0;
        if (monthsDisabled > 12) return 0;
        if (isPartTime) return 0;

        //...
    }
}

리팩토링 후

public class Sample2 {
    public double disabilityAmount() {
        if (isNotEligableForDisability()) {
            return 0;
        }
        //...
    }
}

서로 다른 여러 개의 조건식을 합쳐야 하는 이유

  • 여러 검사를 OR 연산자로 연결해서 실제로 하나의 검사 수행을 표현해서 무엇을 검사하는지 더 확실히 이해할 수 있다.
  • 메서드 추출을 적용할 수 있는 기반이 마련된다.

조건문의 공통 실행 코드 빼내기

조건문의 모든 절에 같은 실행 코드가 있을 땐

  • 같은 부분을 조건문 밖으로 빼자.

public class Sample3 {
    public void methodA() {
        if (isSpecialDeal()) {
            total = price * 0.95;
            send();
        } else {
            total = price * 0.98;
            send();
        }
    }
}

리팩토링 후

public class Sample3 {
    public void methodA() {
        if (isSpecialDeal()) {
            total = price * 0.95;
        } else {
            total = price * 0.98;
        }
        send();
    }
}
  • 조건문 밖으로 빼내야 각 절이 공통적으로 실행할 기능과 서로 다르게 실행할 기능을 한눈에 알 수 있다.

제어 플래그 제거

논리 연산식의 제어 플래그 역할을 하는 변수가 있을 땐

  • 그 변수를 break 문이나 return 문으로 바꾸자.

  • 제어 플래그는 유용함을 능가하는 단점이 있다.
  • 진입점과 이탈점이 하나씩 있는 루틴을 호출하는 구조적 프로그래밍의 문법적 잔재다.
  • 진입점이 하나인 것엔 문제가 없지만, 이탈점을 하나만 사용해도 코드 안의 각종 특이한 플래그로 인해 조건문이 복잡해진다.

👻 본인은 코딩테스트 작성할 때 제어 플래그를 많이 쓴다. 확실히 제어 플래그를 많이 쓰면 코드가 복잡해지고, 알아보기도 힘들어진다. 따라서 이를 지양하는 것이 좋겠따.

public class Sample4 {
    public void checkSecurity(String[] people) {
        boolean found = false;
        for (int i = 0; i < people.length; i++) {
            if (!found) {
                if (people[i].equals("Don")) {
                    sendAlert();
                    found = true;
                }

            }
        }
    }
}

리팩토링 후 break

public class Sample4 {
    public void checkSecurity(String[] people) {
        for (int i = 0; i < people.length; i++) {
            if (people[i].equals("Don")) {
                sendAlert();
                break;
            }
        }
    }
}

리팩토링 후 return

public class Sample4 {
    public String checkSecurity(String[] people) {
        for (int i = 0; i < people.length; i++) {
            if (people[i].equals("Don")) {
                sendAlert();
                return "Don";
            }
        }
        return "";
    }
}

여러 겹의 조건문을 감시 절로 전환

메서드에 조건문이 있어서 정상적인 실행 경로를 파악하기 힘들 땐

  • 모든 특수한 경우에 감시절을 사용하자.

public class Sample5 {
    public double getPayAmount() {
        double result;
        if (isDead) result = deadAmount();
        else {
            if (isSeparated) result = separatedAmoun();
            else {
                if (isRetired) result = retiredAmount();
                else result = normalPayAmount();
            }
        }
        return result;
    }
}

리팩토링 후

public class Sample5 {
    public double getPayAmount() {
        if (isDead) return deadAmount();
        if (isSeparated) return separatedAmoun();
        if (isRetired) return retiredAmount();
        return normalPayAmount();
    }
}
  • 역시 훨씬 간결해졌다.
  • 그래서 조건문을 사용하면 항상 더 간결해질 수는 없는지 리팩토링 해보고, 테스트까지 진행하여 정상 동작하는지 확인해야 한다.

조건문을 재정의로 전환

객체 타입에 따라 다른 기능을 실행하는 조건문이 있을 땐

  • 조건문의 각 절을 하위클래스의 재정의 메서드 안으로 옮기고, 원본 메서드는 abstract 타입으로 수정하자.

  • 이전에 한번 알아보았으므로 넘어가겠다.
  • 만약 switch 문에 더이상 조건이 추가될 것이 없고, 이미 case가 몇개 없다면 재정의로 전환하는 것을 해야하는지 고려해볼 필요가 있다.
  • 이미 변하지 않는 기능을 복잡하게 리팩토링하는 것이 오버가 아닌지 판단할 수 있어야 한다.

Null 검사를 널 객체에 위임

null 값을 검사하는 코드가 계속 나올 땐

  • null 값을 널 객체로 만들자.

public class Sample6 {
    public void methodA() {
        if (customer == null) plan = BillingPlan.basic();
        else plan = customer.getPlan();
    }
}
  • 재정의 본질은 어떤 종류인지를 객체에 일일이 물어서 그 응답에 따라 실행할 기능을 호출하는 것이 아니라. 묻지도 따지지도 않고 기능을 곧바로 호출하는 것이다.
  • 객체는 타입에 따라 그에 맞는 기능을 수행한다.
  • null 값이 저장된 필드가 있을 땐 재정의하면 비교적 이해하기 힘들다.

하지만 널 객체를 만드는 과정을 복잡하다. 또한 요즘은 Spring 에서 제공해주는 ObjectUtils나 StringUtils 같은 유틸성 클래스가 null 체크할 수 있으도록 라이브러리를 제공해준다. 따라서 "아, 이런게 있었구나." 하고 넘어가도 좋을 것 같다.

어설션 넣기

일부 코드가 프로그램의 어떤 상태를 전제할 땐

  • 어설션을 넣어서 그 전제를 확실하게 코드로 작성하자.

public class Sample7 {
    public double getExpenseLimit() {
        return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject.getMemberExpenseLimit();
    }
}

리팩토링 후

public class Sample7 {
    public double getExpenseLimit() {
        Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);
        return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject.getMemberExpenseLimit();
    }
}
  • 어셜션이란 테스트코드처럼 검증하는 것이다.
  • 어설션을 넣으면 적어도 널 포인터 같은 에러는 막을 수 있다.
  • 실무에서 사용하는지는 잘 모르겠다..
  • 다만 어셜션을 너무 남발하면 유지하기 힘들어진다.
  • 따라서 어셜션을 안쓸 수 있다면 안쓰는 것이 좋겠다.

느낀 점

이번 챕터에서는 조건문이 복잡한 경우 어떻게 해야 간결해질 수 있는지 리팩토링하는 법에 대해 알아보았다.

  • 기본적으로 조건문을 쪼개거나
  • 조건문을 통합하거나
  • 공통 메서드는 조건문 밖에 빼낸다거나
  • 등등..

위의 기본적인 내용들은 알아두는 것이 좋겠다.
조건문은 복잡해질 수도 있고, 리팩토링하면 복잡한 코드를 간결하게 변경시킬 수도 있다.

유지보수하는 입장에서 조건문이 복잡하면 "분명 리팩토링 할 수 있을 것 같은데.." 하는 생각이 든다. 그만큼 조건문을 작성할 경우 더 간결해질 수 없는지 생각하고 코딩해야 한다.

References

댓글