////
Search
Duplicate
🌛

24. 리팩토링

1. 소프트웨어 진화의 종류

여러 소프트웨어 진화의 가장 중요한 차이점은 변경 시 프로그램의 품질이 향상되느냐 떨어지느냐에 있다.
두 번재 차이점은 변경이 구현 중에 발생하는지, 유지보수 중에 이뤄지는지 있다.
구현 시 변경은 일반적으로 개발자가 작성한 기억을 잊기 전 수행한다. 일정또한 유지보수 시기보다 더 자유롭다.

소프트웨어 진화의 철학

기본 원칙은 진화가 프로그램의 내적인 품질을 향상시켜야 한다는 것이다.

2. 리팩토링 소개

소프트웨어 진화의 기본 원칙을 지키는 핵심 전략은 리팩토링이다.
마틴 파울러는 리팩토링을 소프트웨어를 더 쉽게 이해하고 적은 비용으로 수정할 수 있도록 동작 변화 없이 내부 구조를 변경하는 것이라고 정의했다.
리팩토링이란 단어는 래리 콘스탄틴이 구조적 프로그램에서 팩토링이라는 단어를 사용한 것에서 유래했다. 기존의 팩토링은 프로그램을 가능한 많은 구성 요소로 분해하는 것을 의미한다.

리팩토링하는 이유

리팩토링이 필요함을 암시하는 징후들을 코드 스멜이라고 한다. 다음이 코드 스멜의 대표적인 예들이다.
코드가 중복되어 있다.
중복된 코드는 설계를 완전하게 나누는데 실패했다는 것을 의미한다. 이때 코드를 변경하게 되면 다른 곳도 병렬적으로 변경해주어야 한다.
앤드류 헌트와 데이브 토마스가 언급한 DRY 법칙을 위반한다.
저자는 개인적으로 데이비드 파르나스의 복사해서 봍여넣기는 설계 오류다라는 말이 이러한 상황을 가장 잘 표현한다고 생각한다.
메소드가 너무 길다.
메소드가 너무 길다는 것은 너무 많은 책임이 메소드에게 지어져 있을 가능성이 있다.
이럴 때, 한 가지 기능만 잘 처리하는 잘 정의되고 이름이 좋은 메소드의 수를 늘려 모듈화를 늘리는 것이 좋다.
루프가 너무 길거나 깊이 중첩되어 있다.
클래스의 응집력이 약하다.
내부 연산들이 서로 연관성 없는 기능을 책임지고 있다면 나누는 것이 좋다.
인터페이스가 일관된 추상화 수준을 제공하지 않는다.
매개변수가 너무 많다.
클래스 내의 변경 사항이 상호 관계를 고려하지 않고 구분되는 경향이 있다.
클래스가 두 개 이상의 분명한 책임을 지는 경우도 있다. 클래스의 한 부분이나 다른 부분을 변경했지만 그러한 변경 사항이 클래스 양쪽에 모두 영향을 미치는 경우가 발생한다.
이는 클래스를 책임별로 나누기에 적절한 시기가 되었음을 뜻한다.
변경할 때 여러 개의 클래스를 동시에 수정해야 한다.
상속 계층 구조가 병렬로 변경되어야 한다.
case 문이 병렬로 변경되어야 한다.
여러 곳에서 case 문을 병렬로 수행하고 있다면 상속이 더 좋은 접근 방법은 아닌지 고려해보아야 한다.
함께 사용되는 연관 데이터 항목이 클래스로 구성되지 않았다.
메소드가 자신이 포함된 클래스보다 다른 클래스의 기능을 더 많이 사용한다.
해당 메소드를 해당 다른 클래스로 옮기고 이전 클래스에서 호출하도록 하자.
클래스가 많은 일을 수행하지 않는다.
이 경우, 해당 클래스의 작은 책임을 다른 클래스에게 넘기고 삭제하자.
일련의 메소드가 뜨내기 데이터를 전달한다.
데이터를 전달하는 메소드가 단순히 다른 메소드로 데이터를 전달하기만 할 때 그 데이터를 뜨내기 데이터라 한다.
중개 역할을 하는 객체가 아무것도 하지 않는다.
한 클래스가 지나치게 다른 클래스에 참견한다.
메소드의 이름이 좋지 않다.
공개 데이터 멤버다.
인터페이스와 구현 사이의 구분을 흐리게 하며 본질적으로 캡슐화를 위반하고 향후 유연성을 제한한다.
서브클래스는 부모 클래스 메소드의 일부만을 사용한다.
포함 관계로 전환하여 캡슐화를 달성하도록 하는 것을 고려해본다.
주석을 이용해 어려운 코드를 설명한다.
나쁜 코드를 주석으로 설명하기 보다 좋은 코드로 만들어라.
전역 변수를 사용한다.
프로그램이 언젠가 필요할 것 같은 코드를 포함하고 있다.
미리 설계하는 방식은 매우 많은 문제를 일으킬 수 있다.
현재 필요한 코드를 분명하고 직관적으로 작성하여 미래의 개발자가 그 코드의 사이드 이펙트를 정확하게 이해할 수 있도록 하는 것이 낫다.

리팩토링하면 안 되는 이유

본질적으로 리팩토링은 종종 코드를 변경하는 것과 동의어로 사용되는데, 이 용어의 의미는 유지보수 시 소프트웨어의 품질을 향상시키기 위한 중요한 전략이다.

3. 구체적인 리팩토링

데이터 수준 리팩토링

매직 넘버를 상수로 대체한다.
변수 이름을 더 분명하고 많은 정보를 제공하는 이름으로 다시 작성한다.
표현식을 인라인화한다.
표현식의 결과를 할당하는 중간 변수를 표현식 자체로 대체한다.
표현식을 메소드로 대체한다.
중간 변수를 사용한다.
여러 목적으로 사용되는 변수를 단일 목적을 갖는 변수 여러 개로 변환한다.
로컬에서 사용할 목적이라면 매개변수 대신 지역 변수를 사용하라.
기본형 데이터를 클래스로 변환한다.
기본형 데이터에 추가적인 작업이나 데이터가 필요하다면 이를 객체로 변환하라.
형 선언 코드 집합을 클래스나 열거형으로 변환한다.
독립적인 상수 대신 클래스를 생성해 더 엄격하게 형을 검사하고 필요할 때 더 풍부한 의미를 제공하게 하라.
열거를 생성하는 것도 클래스 생성에 대한 좋은 대안 중 하나다.
형 선언 코드 집합을 서브클래스를 갖는 클래스로 변환한다.
배열을 객체로 변경한다.
컬렉션을 캡슐화한다.
전형적인 레코드를 데이터 클래스로 대체한다.

명령문 수준의 리팩토링

불린 표현식을 분해한다.
복잡한 불린 표현식을 명확한 이름의 불린 함수로 대체한다.
서로 다른 조건문 내에 중복된 코드를 결합한다.
루프 제어 변수 대신 break, return을 사용한다.
중첩된 if-then-else 문 내에서 리턴 값을 할당하는 대신 답을 알게되었을때 바로 return한다.
조건문을 다형성으로 대체한다.
반복되는 case 문에 유용하다. 상속 계층으로 구성되어 다형성 메소드 호출을 통해 구현될 수 있다.
널 값을 테스트하는 대신 널 객체를 생성하여 사용한다.

메소드 수준의 리팩토링

메소드를 추출한다.
메소드의 코드를 인라인화한다.
긴 메소드를 클래스로 변환한다.
복잡한 알고리즘 대신 간단한 알고리즘을 사용한다.
더 많은 정보가 필요하다면 매개변수를 추가한다.
사용되지 않는 매개변수를 제거한다.
조회와 명령 연산을 구분한다.
매개변수를 이용하여 유사한 메소드를 결합한다.
전달되는 매개변수에 따라 행동하는 메소드를 분리한다.
특정한 필드 대신 전체 객체를 전달한다.
같은 객체에 있는 여러 개의 값을 한 메소드에 전달하고 있다면 전체 객체를 전달하도록 매개변수를 변경하라.
전체 객체 대신 특정한 필드만 전달한다.
단순히 메소드에 전달하려고 객체를 만들었다면 전체 객체 대신 특정한 필드만 받아들이도록 변경하라.
다운캐스팅을 캡슐화한다.

클래스 리팩토링

값 객체를 참조 객체로 변경한다.
크고 복잡한 객체의 복사본을 많이 생성해 관리하고 있다면 하나의 값 객체만 존재하도록 객체의 사용법을 변경하고 나머지 코드는 해당 객체애 다한 참조만 사용하라.
참조 객체를 값 객체로 변경한다.
작고 간단한 객체를 수많은 참조 코드가 참조하고 있다면 모든 객체가 값 객체가 되도록 객체의 사용법을 변경한다.
가상 메소드를 데이터 초기화로 대체한다.
멤버 메소드나 데이터의 위치를 변경한다.
특화된 코드를 서브클래스로 추출한다.
유사한 코드를 슈퍼클래스로 결합한다.

인터페이스 리팩토링

메소드를 다른 클래스로 이동시킨다.
한 클래스를 두 개로 변환한다.
클래스를 제거한다.
위임을 숨긴다.
중개자를 제거한다.
상속을 위임으로 대체한다.
클래스가 다른 클래스를 사용해야 하지만 인터페이스를 제어하고 싶은 경우, 슈퍼클래스를 서브클래스의 필드로 만들고 메소드를 노출한다.
위임을 상속으로 대체한다.
클래스가 위임 클래스의 모든 공개 메소드를 노출하고 있다면 사용하는 대신 위임 클래스로부터 상속받는다.
외부 메소드를 도입한다.
확장 클래스를 도입한다.
노출된 멤버 변수를 캡슐화한다.
변경할 수 없는 필드에 대한 세터를 제거한다.
클래스 외부에서 사용하면 안 되는 메소드를 숨긴다.
사용되지 않은 메소드를 캡슐화한다.
슈퍼클래스와 서브클래스의 구현이 매우 유사하다면 이 둘을 결합한다.

시스템 수준 리팩토링

제어할 수 없는 데이터에 대해 명확한 참조 소스를 생성한다.
단방향 클래스 관계를 양방향 클래스 관계로 바꾼다.
서로의 기능을 사용해야하지만 오직 하나의 클래스만 다른 클래스에 대해 알고 있는 경우, 서로 알게하는 것이 좋다.
양방향 클래스 관계를 단뱡항 클래스 관계로 바꾼다.
간단한 생성자 대신 팩토리 메소드를 제공한다.
오류 코드를 예외로 대체하거나 그 반대로 한다.
오류 처리 전략의 일관성을 지키자.

4. 안전한 리팩토링 방법

리팩토링을 시작하기 전 코드를 백업한다.
리팩토링을 작게 실행한다.
리팩토링은 한 번에 하나만 수행한다.
수행할 단계에 대한 목록을 작성한다.
체크포인트를 자주 설정한다.
컴파일러 경고를 활용한다.
회귀 테스트를 수행한다.
테스트 케이스를 추가한다.
변경사항을 검토한다.
리팩토링의 위험 수준에 따라서 우선순위를 결정한다.

리팩토링이 적합하지 않은 경우

코드를 작성하고 수정하는 것을 감추는 용도로 사용하지 않는다.
코드를 재작성하는 대신 리팩토링하지 않는다.

5. 리팩토링 전략

메소드를 추가할 때 리팩토링한다.
클래스를 추가할 때 리팩토링한다.
결함을 수정할 때 리팩토링한다.
오류를 유발할 가능성이 있는 복잡한 모듈을 대상으로 한다.
유지보수 환경에서는 자신이 맡은 부분을 개선한다.
정돈된 코드와 엉성한 코드 사이의 인터페이스를 정의한 후 인터페이스를 통해 코드를 이동한다.

요점 정리

프로그램이 초기 개발 시와 유지보수 시 변경 가능성은 가능성이 아니라 확신이다.
소프트웨어는 변경될 때 향상되거나 손상될 수 있다. 진화의 기본 원칙은 향상되어야 한다는 것이다.
리팩토링을 성공적으로 수행하기 위해 다양한 경고 표시나 리팩토링이 필요하다는 암시인 코드 스멜에 주의를 기울인다.
외에도 다양한 사례들을 학습하라.
리팩토링을 안전하게 수행하기 위한 접근법을 가진다.
구현 중에 리팩토링을 수행하는 것은 유지보수 시보다 더 낫다. 적극적으로 구현 중에 수행하자.