24장 리팩터링
목차
- 24.1 소프트웨어 진화의 종류
- 24.2 리팩터링 소개
- 24.3 구체적인 리팩터링
- 24.4 안전한 리팩터링
- 24.5 리팩터링 전략
이번 장은 프로그래밍의 꽃(?)이라 할 수 있는 리팩터링에 대해 다룬다. 그동안 나는 기존 코드를 변경하고, 이를 리팩터링 했다고 생각해왔는데 해킹을 해왔었다는 것을 오늘 알았다...
24.1 소프트웨어 진화의 종류
코드 변경을 통해 소프트웨어는 진화할 수도 있고, 퇴보할 수도 있다. 어떻게 코드를 변경하는지가 소프트웨어의 품질을 크게 좌우한다.
코드를 작성할 때, 이후에 이를 쉽게 변경할 수 있도록 작성해야 한다. 일단 지금 동작하는 코드를 작성한 것이 아닌지 의심을 하는 습관을 갖고, 추후에 이 기능을 수정해야 할 때 이 부분만 수정하면 되는지에 대해서도 생각하고 코드를 작성해야겠다.
24.2 리팩터링 소개
리팩터링 하는 이유
- 코드가 중복되어 있다.
- 루틴이 너무 길다.
- 루프가 너무 길거나 깊이 중첩되어 있다.
- 클래스의 응집력이 약하다.
- 클래스 인터페이스가 일관된 추상화 수준을 제공하지 않는다.
- 매개변수가 너무 많다.
- 클래스 내의 변경 사항이 상호 관계를 고려하지 않고 구분되는 경향이 있다.
- 변경할 때 여러 개의 클래스를 동시에 수정해야 한다.
- 상속 계층 구조가 병렬로 변경되어야 한다.
- case 문이 병렬로 변경되어야 한다.
- 함께 사용되는 연관된 데이터 항목이 클래스로 구성되지 않았다.
- 루틴이 자신이 포함된 클래스보다 다른 클래스의 기능을 더 많이 사용한다.
- 클래스가 많은 일을 수행하지 않는다.
- 일련의 루틴이 뜨내기 데이터를 전달한다.
- 중개 역할을 하는 객체가 아무것도 하지 않는다.
- 한 클래스가 지나치게 다른 클래스를 참견한다.
- 루틴의 이름이 엉성하다.
- 공개 데이터 멤버다.
- 서브 클래스가 부모 클래스 루틴의 일부만을 사용한다.
- 주석을 이용해 어려운 코드를 설명한다.
- 전역 변수를 사용한다.
- 루틴이 루틴을 호출하기 전에 설정 코드를 사용하거나 루틴을 호출한 다음에 분해 코드를 사용한다.
- 프로그램이 언젠가 필요할 것 같은 코드를 포함하고 있다.
24.3 구체적인 리팩터링
여기서 설명하는 많은 리팩터링은 마틴 파울러의 <<리팩토링: 코드 품질을 개선하는 객체지향 사고법>>에서 설명한 내용을 요약한 것이다.
데이터 수준 리팩터링
- 매직 넘버를 이름 상수로 대체한다.
- 변수 이름을 더 분명하고 많은 정보를 제공하는 이름으로 다시 짓는다.
- 표현식을 인라인화한다.
- 표현식을 루틴으로 대체한다.
- 중간 변수를 사용한다.
- 여러 목적으로 사용되는 변수를 단일 목적을 갖는 변수 여러 개로 변환한다.
- 로컬에서 사용할 목적이라면 매개변수 대신 지역 변수를 사용한다.
- 기본형 데이터를 클래스로 변환한다.
- 형 선언 코드 집합을 클래스나 열거형으로 변환한다.
- 형 선언 코드 집합을 서브 클래스를 갖는 클래스로 변환한다.
- 배열을 객체로 변경한다.
- 컬렉션을 캡슐화한다.
- 전형적인 레코드를 데이터 클래스로 대체한다.
명령문 수준 리팩터링
- 복잡한 불린 표현식을 명확한 이름의 불린 함수로 옮긴다.
- 서로 다른 조건문 내에 중복으로 사용된 코드를 결합한다.
- 루프 제어 변수 대신 break나 return을 사용한다.
- 중첩된 if-then-else 명령문 내에서 리턴 값을 할당하는 대신, 답을 알았을 때 곧바로 리턴한다.
- 조건문(특히 반복되는 case 문)을 다형성으로 대체한다.
- 널 값을 테스트하는 대신 널 객체를 생성하여 사용한다.
루틴 수준 리팩터링
- 루틴을 추출한다/메서드를 추출한다.
- 루틴의 코드를 인라인화한다.
- 긴 루틴을 클래스로 변환한다.
- 복잡한 알고리즘 대신 간단한 알고리즘을 사용한다.
- 매개변수를 추가한다.
- 매개변수를 제거한다.
- 변경 연산과 쿼리 연산을 구분한다.
- 매개변수를 이용하여 유사한 루틴을 결합한다.
- 전달되는 매개변수에 따라 행동하는 루틴을 분리한다.
- 특정한 필드 대신 전체 객체를 전달한다.
- 전체 객체 대신 특정한 필드만 전달한다.
- 다운 캐스팅을 캡슐화한다.
클래스 구현 리팩터링
- 값 객체를 참조 객체로 변경한다.
- 참조 객체를 값 객체로 변경한다.
- 가상 루틴을 데이터 초기화로 대체한다.
- 멤버 루틴이나 데이터의 위치를 변경한다.
- 특화된 코드를 서브클래스로 추출한다.
- 유사한 코드를 슈퍼클래스로 결합한다.
클래스 인터페이스 리팩터링
- 루틴을 다른 클래스로 이동시킨다.
- 한 클래스를 두 개로 변환한다.
- 클래스를 제거한다.
- 위임을 숨긴다.
- 중개자를 제거한다.
- 상속을 위임으로 대체한다.
- 위임을 상속으로 대체한다.
- 외부 루틴을 도입한다.
- 확장 클래스를 도입한다.
- 노출된 멤버 변수를 캡슐화한다.
- 변경할 수 없는 필드에 대한 Set() 루틴을 제거한다.
- 클래스 외부에서 사용하면 안되는 루틴을 숨긴다.
- 사용되지 않는 루틴을 캡슐화한다.
- 슈퍼 클래스와 서브 클래스의 구현이 매우 유사하다면 이 둘을 결합한다.
시스템 수준 리팩터링
- 제어할 수 없는 데이터에 대해 명확한 참조 소스를 생성한다.
- 단방향 클래스 관계를 양방향 클래스 관게로 바꾼다.
- 양방향 클래스 관게를 단방향 클래스 관계로 바꾼다.
- 간단한 생성자 대신 팩토리 메서드를 제공한다.
- 오류 코드를 예외로 대체하거나 그 반대로 한다.
24.4 안전한 리팩터링 방법
리팩터링을 안전하게 하기 위한 가장 중요한 첫 번째는 초기 코드를 저장하는 것이다. 또 이후 변경할 때마다 돌아갈 수 있는 체크 포인트들을 만들어, 문제가 발생할 경우 돌아갈 수 있도록 해야한다. 또한 한 번에 하나만 수행하고, 수행할 목표를 명확하게 해야 한다.
24.5 리팩터링 전략
리팩터링을 할 좋은 타이밍
- 루틴을 추가할 때 리팩터링한다.
- 클래스를 추가할 때 리팩터링한다.
- 결함을 수정할 때 리팩터링한다.
- 오류를 유발할 가능성이 있는 모듈을 대상으로 삼는다.
- 복잡도가 높은 모듈을 대상으로 삼는다.
- 유지보수 환경에서는 자신이 맡은 부분을 개선한다. -> 자신이 맡기 전보다 더 좋은 상태가 되도록 한다.
- 정돈된 코드와 엉성한 코드 사이으이 인터페이스를 정의한 후 인터페이스를 통해 코드를 이동한다.
어제 다른 사람이 작성하던 코드를 내가 새로 기능을 넣고, 디자인을 조금 손봐야 했다. 내가 작성한 코드가 아니라서 어느 부분을 손대야 하는지, 그리고 내가 변경한 부분이 다른 기능에 영향을 줄지에 대해 고민해야 했다. 내가 발견하기로 다른 부분에 영향을 주지는 않았지만, 이는 여러번 다시 확인해 봐야 한다.
일단 기능을 추가하는 것에는 성공했고, 디자인을 바꾸는 것도 어느 정도는 완성했다. 하지만 내가 한 것은 리팩터링이 아니라, 해킹이었다. 기존의 코드보다 더 더러워졌고, 당장 이 기능을 추가하기 위한 코드를 삽입했다. 나만의 리팩터링 원칙을 세우고, 이전보다 코드의 품질을 향상시키기 위한 코드를 작성해야겠다. 이전에 내가 맡았던 보고서 페이지도 지금 보니 리팩터링 해야 할 부분이 산더미다. 일단 기존 배열의 인덱스로 접근하던 것을 객체로 바꿔 변경을 더 쉽게 할 수 있도록 해야 한다. 또 0보다 작거나 100보다 큰 부분을 별도의 루틴으로 두면 보기도 더 쉽고, 다른 부분에서도 사용할 수 있을 것이다. 월요일에 출근하면 보고서 페이지를 리팩터링하는 시간을 가져야겠다.