리팩터링 2판 정리 - 조건부 로직 간소화(10)
1. 조건문 분해하기
- 복잡한 조건부 로직은 프로그램을 복잡하게 만드는 가장 흔한 원흉에 속한다.
- 긴 함수는 그 자체로 읽기가 어렵지만, 조건문은 그 어려움을 한층 가중시킨다.
- 거대한 코드 블록이 주어지면 코드를 부위벼롤 분해한 다음 해체된 코드 덩어리들을 각 덩어리의 의도를 살린 이름의 함수 호출로 바꿔주자. 그러면 전체적인 의도가 더 확실히 드러난다.
절차
- 조건식과 그 조건식에 딸린 조건절 각각을 함수로 추출한다.
2. 조건식 통합하기
- 비교하는 조건은 다르지만 그 결과로 수행하는 동작은 똑같은 코드들이 더러 있는데, 어차피 같은 일을 할 거라면 조건 검사도 하나로 통합하는 게 낫다.
- 조건부 코드를 통합하는 게 중요한 이유는 두가지다.
- 첫째, 여러 조각으로 나뉜 조건들을 하나로 통합함으로써 내가 하려는 일이 더 명확해진다.
- 두번째, 이 작업이 함수 추출하기까지 이어질 가능성이 높기 때문이다.
절차
- 해당 조건식들 모두에 부수효과가 없는지 확인한다.
- 조건문 두 개를 선택하여 두 조건문의 조건식들을 논리 연산자로 결합한다.
- 테스트한다.
- 조건이 하나만 남을 때까지 2~3 과정을 반복한다.
- 하나로 합쳐진 조건식을 함수로 추출할지 고려해본다.
3. 중첩 조건문을 보호 구문으로 바꾸기
- 조건문은 주로 두 가지 형태로 쓰인다. 참인 경로와 거짓인 경로 모두 정상 동작으로 이어지는 형태와, 한쪽만 정상인 형태다.
- 두 형태는 의도하는 바가 서로 다르므로 그 의도가 코드에 드러나아 한다.
- 두 경로 모두 정상 동작이라면 if와 else절 사용, 한쪽만 정상이라면 비정상 조건을 if에서 검사한 다음, 조건이 참이면 함수에서 빠져나온다. 두 번째 검사 형태를 흔히 보호 구문이라고 한다.
- 중첩 조건문을 보호 구문으로 바꾸기 리팩터링의 핵심은 의도를 부각하는 데 있다.
- 코드에서는 명확함이 핵심이다.
절차
- 교체해야할 조건 중 가장 바깥 것을 선택하여 보호 구문으로 바꾼다.
- 테스트한다.
- 1~2 과정을 필요한 만큼 반복한다.
- 모든 보호 구문이 같은 결과를 반환한다면 보호 구문들의 조건식을 통합한다.
4. 조건부 로직을 다형성으로 바꾸기
- 클래스와 다형성을 이용하면 복잡한 조건부 로직을 확실하게 분리할 수도 있다.
절차
- 다형적 동작을 표현하는 클래스들이 아직 없다면 만들어준다. 이왕이면 적합한 인스턴스를 알아서 만들어 반환하는 팩터리 함수도 함께 만든다.
- 호출하는 코드에서 팩터리 함수를 사용하게 한다.
- 조건부 로직 함수를 슈퍼클래스로 옮긴다.
- 서브클래스 중 하나를 선택한다. 서브클래스에서 슈퍼클래스의 조건부 로직 메서드를 오버라이드한다. 조건부 문장 중 선택된 서브클래스에 해당하는 조건절을 서브클래스 메서드로 복사한 다음 적절히 수정한다.
- 같은 방식으로 각 조건절을 해당 서브클래스에서 메서드로 구현한다.
- 슈퍼클래스 메서드에는 기본 동족 부분만 남긴다. 혹은 슈퍼클래스가 추상 클래스여야 한다면, 이 메서드를 추상으로 선언하거나 서브클래스에서 처리해야 함을 알리는 에러를 던진다.
5. 특이 케이스 추가하기
- 코드베이스에서 특정 값에 대해 똑같이 반응하는 코드가 여러 곳이라면 그 반응들을 한 데로 모으는게 효율적이다.
- 특이 케이스 객체는 이를 캡슐화한 클래스가 반환하도록 만들 수도 있고, 변환을 거쳐 데이터 구조에 추가시키는 형태도 될 수 있다.
절차
- 컨테이너에 특이 케이스인지를 검사하는 속성을 추가하고, false를 반환하게 한다.
- 특이 케이스 객체를 만든다. 이 객체는 특이 케이스인지를 검사하는 속성만 포함이며, 이 속성은 true를 반환하게 한다.
- 클라이언트에서 특이 케이스인지를 검사하는 코드를 함수로 추출한다. 모든 클라이언트가 값을 직접 비교하는 대신 방금 추출한 함수를 사용하도록 고친다.
- 코드에 새로운 특이 케이스 대상을 추가한다. 함수의 반환 값으로 받거나 변환 함수를 적용하면 된다.
- 특이 케이스를 검사하는 함수 본문을 수정하여 특이 케이스 객체의 속성을 사용하도록 한다.
- 테스트한다.
- 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 적용하여 특이 케이스를 처리하는 공통 동작을 새로운 요소로 옮긴다.
- 아직도 특이 케이스 검사 함수를 이용하는 곳이 남아 있다면 검사 함수를 인라인한다.
6. 어서션 추가하기
- 특정 조건이 참일 때만 제대로 동작하는 코드 영역이 있을 수 있다. 객체로 눈을 돌리면 여러 필드 중 최소 하나에는 값이 들어 있어야 동작하는 경우를 생각할 수 있다.
- 이런 가정이 코드에 항상 명시적으로 기술되어 있지는 않아서 알고리즘을 보고 연역해서 알아내야 할 때도 있다. 주석에라도 적혀 있다면 그나마 형편이 좀 낫다. 더 나은 방법은 어서션을 이용해서 코드 자체에 삽입해놓은 것이다.
- 어서션은 항상 참이라고 가정하느 ㄴ조건부 문장으로, 어서션이 실패했다는 건 프로그래머가 잘 못했다는 뜻이다.
- 어서션 실패는 시스템의 다른 부분에서는 절대 검서하지 않아야 하며, 어서션이 있고 없고가 프로그램 기능의 정상 동작에 아무런 영향을 주지 않도록 작성돼야 한다.
- 테스트 코드가 있다면 어서션의 디버깅 용도로서의 효용은 줄어든다.
- 단위 테스트를 꾸준히 추가하여 사각을 좁히면 어서션보다 나을 때가 많다. 하지만 소통 측면에서는 어서션이 여전히 매력적이다.
- 어서션은 오류의 출처를 특정하기 어려울 때 특히 제값을 한다.
- 어서션은 반드시 참이어야 하는 것만 검사한다.
- 프로그래머가 일으킬만한 오류에만 어서션을 사용한다. 데이터를 외부에서 읽어온다면 그 값을 검사하는 작업은 가정이 아니라 프로그램 로직의 일부로 다뤄야 한다.
절차
- 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가한다.
7. 제어 플래그를 탈출문으로 바꾸기
- 제어 플래그란 코드의 동작을 변경하는 데 사용되는 변수를 말하며, 어딘가에서 값을 계산해 제어 플래그에 설정한 후 다른 어딘가의 조건문에서 검사하는 형태로 쓰인다. 나는 이런 코드를 항상 악취로 본다.
- 제어 플래그의 주 서식지는 반복문 안이다. brake문이나 continue문 활용에 익숙하지 않은 사람이 심언호기도 하고, 함수의 return문을 하나로 유지하고자 노력하는 사람이 심기도 한다.
절차
- 제어 플래그를 사용하는 코드를 함수로 추출할지 고려한다.
- 제어 플래그를 갱신하는 코드 각각을 적절한 제어문으로 바꾼다. 하나 바꿀 때마다 테스트한다.
- 모두 수정했다면 제어 플래그를 제거한다.
출처
리팩터링 2판