1. 소프트웨어 진화의 종류
•
여러 소프트웨어 진화의 가장 중요한 차이점은 변경 시 프로그램의 품질이 향상되느냐 떨어지느냐에 있다.
•
두 번재 차이점은 변경이 구현 중에 발생하는지, 유지보수 중에 이뤄지는지 있다.
◦
구현 시 변경은 일반적으로 개발자가 작성한 기억을 잊기 전 수행한다. 일정또한 유지보수 시기보다 더 자유롭다.
소프트웨어 진화의 철학
•
기본 원칙은 진화가 프로그램의 내적인 품질을 향상시켜야 한다는 것이다.
2. 리팩토링 소개
•
소프트웨어 진화의 기본 원칙을 지키는 핵심 전략은 리팩토링이다.
◦
마틴 파울러는 리팩토링을 소프트웨어를 더 쉽게 이해하고 적은 비용으로 수정할 수 있도록 동작 변화 없이 내부 구조를 변경하는 것이라고 정의했다.
◦
리팩토링이란 단어는 래리 콘스탄틴이 구조적 프로그램에서 팩토링이라는 단어를 사용한 것에서 유래했다. 기존의 팩토링은 프로그램을 가능한 많은 구성 요소로 분해하는 것을 의미한다.
리팩토링하는 이유
•
리팩토링이 필요함을 암시하는 징후들을 코드 스멜이라고 한다. 다음이 코드 스멜의 대표적인 예들이다.
•
코드가 중복되어 있다.
◦
중복된 코드는 설계를 완전하게 나누는데 실패했다는 것을 의미한다. 이때 코드를 변경하게 되면 다른 곳도 병렬적으로 변경해주어야 한다.
◦
앤드류 헌트와 데이브 토마스가 언급한 DRY 법칙을 위반한다.
▪
저자는 개인적으로 데이비드 파르나스의 복사해서 봍여넣기는 설계 오류다라는 말이 이러한 상황을 가장 잘 표현한다고 생각한다.
•
메소드가 너무 길다.
◦
메소드가 너무 길다는 것은 너무 많은 책임이 메소드에게 지어져 있을 가능성이 있다.
◦
이럴 때, 한 가지 기능만 잘 처리하는 잘 정의되고 이름이 좋은 메소드의 수를 늘려 모듈화를 늘리는 것이 좋다.
•
루프가 너무 길거나 깊이 중첩되어 있다.
•
클래스의 응집력이 약하다.
◦
내부 연산들이 서로 연관성 없는 기능을 책임지고 있다면 나누는 것이 좋다.
•
인터페이스가 일관된 추상화 수준을 제공하지 않는다.
•
매개변수가 너무 많다.
•
클래스 내의 변경 사항이 상호 관계를 고려하지 않고 구분되는 경향이 있다.
◦
클래스가 두 개 이상의 분명한 책임을 지는 경우도 있다. 클래스의 한 부분이나 다른 부분을 변경했지만 그러한 변경 사항이 클래스 양쪽에 모두 영향을 미치는 경우가 발생한다.
◦
이는 클래스를 책임별로 나누기에 적절한 시기가 되었음을 뜻한다.
•
변경할 때 여러 개의 클래스를 동시에 수정해야 한다.
•
상속 계층 구조가 병렬로 변경되어야 한다.
•
case 문이 병렬로 변경되어야 한다.
◦
여러 곳에서 case 문을 병렬로 수행하고 있다면 상속이 더 좋은 접근 방법은 아닌지 고려해보아야 한다.
•
함께 사용되는 연관 데이터 항목이 클래스로 구성되지 않았다.
•
메소드가 자신이 포함된 클래스보다 다른 클래스의 기능을 더 많이 사용한다.
◦
해당 메소드를 해당 다른 클래스로 옮기고 이전 클래스에서 호출하도록 하자.
•
클래스가 많은 일을 수행하지 않는다.
◦
이 경우, 해당 클래스의 작은 책임을 다른 클래스에게 넘기고 삭제하자.
•
일련의 메소드가 뜨내기 데이터를 전달한다.
◦
데이터를 전달하는 메소드가 단순히 다른 메소드로 데이터를 전달하기만 할 때 그 데이터를 뜨내기 데이터라 한다.
•
중개 역할을 하는 객체가 아무것도 하지 않는다.
•
한 클래스가 지나치게 다른 클래스에 참견한다.
•
메소드의 이름이 좋지 않다.
•
공개 데이터 멤버다.
◦
인터페이스와 구현 사이의 구분을 흐리게 하며 본질적으로 캡슐화를 위반하고 향후 유연성을 제한한다.
•
서브클래스는 부모 클래스 메소드의 일부만을 사용한다.
◦
포함 관계로 전환하여 캡슐화를 달성하도록 하는 것을 고려해본다.
•
주석을 이용해 어려운 코드를 설명한다.
◦
나쁜 코드를 주석으로 설명하기 보다 좋은 코드로 만들어라.
•
전역 변수를 사용한다.
•
프로그램이 언젠가 필요할 것 같은 코드를 포함하고 있다.
◦
미리 설계하는 방식은 매우 많은 문제를 일으킬 수 있다.
◦
현재 필요한 코드를 분명하고 직관적으로 작성하여 미래의 개발자가 그 코드의 사이드 이펙트를 정확하게 이해할 수 있도록 하는 것이 낫다.
리팩토링하면 안 되는 이유
•
본질적으로 리팩토링은 종종 코드를 변경하는 것과 동의어로 사용되는데, 이 용어의 의미는 유지보수 시 소프트웨어의 품질을 향상시키기 위한 중요한 전략이다.
3. 구체적인 리팩토링
데이터 수준 리팩토링
•
매직 넘버를 상수로 대체한다.
•
변수 이름을 더 분명하고 많은 정보를 제공하는 이름으로 다시 작성한다.
•
표현식을 인라인화한다.
◦
표현식의 결과를 할당하는 중간 변수를 표현식 자체로 대체한다.
•
표현식을 메소드로 대체한다.
•
중간 변수를 사용한다.
•
여러 목적으로 사용되는 변수를 단일 목적을 갖는 변수 여러 개로 변환한다.
•
로컬에서 사용할 목적이라면 매개변수 대신 지역 변수를 사용하라.
•
기본형 데이터를 클래스로 변환한다.
◦
기본형 데이터에 추가적인 작업이나 데이터가 필요하다면 이를 객체로 변환하라.
•
형 선언 코드 집합을 클래스나 열거형으로 변환한다.
◦
독립적인 상수 대신 클래스를 생성해 더 엄격하게 형을 검사하고 필요할 때 더 풍부한 의미를 제공하게 하라.
◦
열거를 생성하는 것도 클래스 생성에 대한 좋은 대안 중 하나다.
•
형 선언 코드 집합을 서브클래스를 갖는 클래스로 변환한다.
•
배열을 객체로 변경한다.
•
컬렉션을 캡슐화한다.
•
전형적인 레코드를 데이터 클래스로 대체한다.
명령문 수준의 리팩토링
•
불린 표현식을 분해한다.
•
복잡한 불린 표현식을 명확한 이름의 불린 함수로 대체한다.
•
서로 다른 조건문 내에 중복된 코드를 결합한다.
•
루프 제어 변수 대신 break, return을 사용한다.
•
중첩된 if-then-else 문 내에서 리턴 값을 할당하는 대신 답을 알게되었을때 바로 return한다.
•
조건문을 다형성으로 대체한다.
◦
반복되는 case 문에 유용하다. 상속 계층으로 구성되어 다형성 메소드 호출을 통해 구현될 수 있다.
•
널 값을 테스트하는 대신 널 객체를 생성하여 사용한다.
메소드 수준의 리팩토링
•
메소드를 추출한다.
•
메소드의 코드를 인라인화한다.
•
긴 메소드를 클래스로 변환한다.
•
복잡한 알고리즘 대신 간단한 알고리즘을 사용한다.
•
더 많은 정보가 필요하다면 매개변수를 추가한다.
•
사용되지 않는 매개변수를 제거한다.
•
조회와 명령 연산을 구분한다.
•
매개변수를 이용하여 유사한 메소드를 결합한다.
•
전달되는 매개변수에 따라 행동하는 메소드를 분리한다.
•
특정한 필드 대신 전체 객체를 전달한다.
◦
같은 객체에 있는 여러 개의 값을 한 메소드에 전달하고 있다면 전체 객체를 전달하도록 매개변수를 변경하라.
•
전체 객체 대신 특정한 필드만 전달한다.
◦
단순히 메소드에 전달하려고 객체를 만들었다면 전체 객체 대신 특정한 필드만 받아들이도록 변경하라.
•
다운캐스팅을 캡슐화한다.
클래스 리팩토링
•
값 객체를 참조 객체로 변경한다.
◦
크고 복잡한 객체의 복사본을 많이 생성해 관리하고 있다면 하나의 값 객체만 존재하도록 객체의 사용법을 변경하고 나머지 코드는 해당 객체애 다한 참조만 사용하라.
•
참조 객체를 값 객체로 변경한다.
◦
작고 간단한 객체를 수많은 참조 코드가 참조하고 있다면 모든 객체가 값 객체가 되도록 객체의 사용법을 변경한다.
•
가상 메소드를 데이터 초기화로 대체한다.
•
멤버 메소드나 데이터의 위치를 변경한다.
•
특화된 코드를 서브클래스로 추출한다.
•
유사한 코드를 슈퍼클래스로 결합한다.
인터페이스 리팩토링
•
메소드를 다른 클래스로 이동시킨다.
•
한 클래스를 두 개로 변환한다.
•
클래스를 제거한다.
•
위임을 숨긴다.
•
중개자를 제거한다.
•
상속을 위임으로 대체한다.
◦
클래스가 다른 클래스를 사용해야 하지만 인터페이스를 제어하고 싶은 경우, 슈퍼클래스를 서브클래스의 필드로 만들고 메소드를 노출한다.
•
위임을 상속으로 대체한다.
◦
클래스가 위임 클래스의 모든 공개 메소드를 노출하고 있다면 사용하는 대신 위임 클래스로부터 상속받는다.
•
외부 메소드를 도입한다.
•
확장 클래스를 도입한다.
•
노출된 멤버 변수를 캡슐화한다.
•
변경할 수 없는 필드에 대한 세터를 제거한다.
•
클래스 외부에서 사용하면 안 되는 메소드를 숨긴다.
•
사용되지 않은 메소드를 캡슐화한다.
•
슈퍼클래스와 서브클래스의 구현이 매우 유사하다면 이 둘을 결합한다.
시스템 수준 리팩토링
•
제어할 수 없는 데이터에 대해 명확한 참조 소스를 생성한다.
•
단방향 클래스 관계를 양방향 클래스 관계로 바꾼다.
◦
서로의 기능을 사용해야하지만 오직 하나의 클래스만 다른 클래스에 대해 알고 있는 경우, 서로 알게하는 것이 좋다.
•
양방향 클래스 관계를 단뱡항 클래스 관계로 바꾼다.
•
간단한 생성자 대신 팩토리 메소드를 제공한다.
•
오류 코드를 예외로 대체하거나 그 반대로 한다.
◦
오류 처리 전략의 일관성을 지키자.
4. 안전한 리팩토링 방법
•
리팩토링을 시작하기 전 코드를 백업한다.
•
리팩토링을 작게 실행한다.
•
리팩토링은 한 번에 하나만 수행한다.
•
수행할 단계에 대한 목록을 작성한다.
•
체크포인트를 자주 설정한다.
•
컴파일러 경고를 활용한다.
•
회귀 테스트를 수행한다.
•
테스트 케이스를 추가한다.
•
변경사항을 검토한다.
•
리팩토링의 위험 수준에 따라서 우선순위를 결정한다.
리팩토링이 적합하지 않은 경우
•
코드를 작성하고 수정하는 것을 감추는 용도로 사용하지 않는다.
•
코드를 재작성하는 대신 리팩토링하지 않는다.
5. 리팩토링 전략
•
메소드를 추가할 때 리팩토링한다.
•
클래스를 추가할 때 리팩토링한다.
•
결함을 수정할 때 리팩토링한다.
•
오류를 유발할 가능성이 있는 복잡한 모듈을 대상으로 한다.
•
유지보수 환경에서는 자신이 맡은 부분을 개선한다.
•
정돈된 코드와 엉성한 코드 사이의 인터페이스를 정의한 후 인터페이스를 통해 코드를 이동한다.
요점 정리
•
프로그램이 초기 개발 시와 유지보수 시 변경 가능성은 가능성이 아니라 확신이다.
•
소프트웨어는 변경될 때 향상되거나 손상될 수 있다. 진화의 기본 원칙은 향상되어야 한다는 것이다.
•
리팩토링을 성공적으로 수행하기 위해 다양한 경고 표시나 리팩토링이 필요하다는 암시인 코드 스멜에 주의를 기울인다.
◦
외에도 다양한 사례들을 학습하라.
◦
리팩토링을 안전하게 수행하기 위한 접근법을 가진다.
•
구현 중에 리팩토링을 수행하는 것은 유지보수 시보다 더 낫다. 적극적으로 구현 중에 수행하자.