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
'공부 기록' 카테고리의 다른 글
[리팩토링 1판] Chapter 11. 일반화 처리 (0) | 2024.04.09 |
---|---|
[리팩토링 1판] Chapter 10. 메서드 호출 단순화 (1) | 2024.04.06 |
[리팩토링 1판] Chapter 08. 데이터 체계화 (0) | 2024.04.03 |
[리팩토링 1판] Chapter 07. 객체 간의 기능 이동 (0) | 2024.04.01 |
[리팩토링 1판] Chapter 06. 메서드 정리 (0) | 2024.03.31 |
댓글