복합 리팩토링

Updated:

복합 리팩토링

  • 지금부터는 상황별로 복합적 리팩토링이므로 정확한 순서와 방법은 없다.
  • 그리고 시간이 오래 걸릴 수도 있다.
  • 따라서 리팩토링은 주변 정리부터 매일 조금씩 해야 한다.
  • 리팩토링응 기능을 추가할 때나 버그를 수정할 때 실시한다.
  • 리팩토링은 시작했을 때 끝을 봐야 하는 것은 아니다.
  • 실제 작업을 수행하는 데 필요한 만큼 하자.
  • 복합 리팩토링은 단순 리팩토링 기법과 달리 개발 팀 전원의 합의 하에 실시한다.

복합 리팩토링의 필요성

  • 리픽토링은 재미로 하는게 아니다.
  • 리팩토링 없이 불가능한 일이 리팩토링을 하면 가능해지리란 기대 때문에 리팩토링하는 것이다.

1. 필드 상향

  • 하나의 상속 계층이 두 작업을 동시에 수행할 땐 상속 계층을 하나 더 만들어서 위임을 통해 다른 계층을 호출하자.

1.1 동기

  • 상속 구조로 만들면 하위클래스 안에 작성할 코드가 상당히 줄어든다.
  • 메서드 하나는 비록 크기는 작지만 상속 계층에 들어 있다는 것만으로 상당히 중요하다.
  • 상속을 그렇다고 오용은 하지 말자.
  • 상속 계층이 엉키면 코드 중복이 생긴다.

1.2 방법

  • 계층구조에 의해 수행되는 각종 기능등을 확인하자. 2차원 테이블을 그리고 가로 축과 세로 축에 기능을 나타내는 라벨을 붙이자. 테이블이 2차원 이상이라면 이 리팩토링을 한 번에 하나씩 반복 적용해야 한다.
  • 기능의 우선순위를 정하고, 어떤 기능을 현재 계층에 남겨두고 어떤 기능을 다른 계층으로 옮길지 정하자.
  • 공통 상위클래스에 클래스 추출을 적용해서 원본 계층에 있는 각 하위클래스별 객체를 작성하고 이 객체를 저장할 인스턴스 변수를 선언하자.
  • 공통된 상위클래스에 클래스 추출을 적용해서 원본 클래스 안 각 하위클래스를 추출한 객체의 하위클래스를 만들자. 앞 단계에서 선언한 인스턴스 변수를 이 하위클래의 인스턴스로 초기화하자.
  • 하위클래스마다 메서드 이동을 실시해서 하위클래스의 해당 기능을 관련된 추출 객체로 옮기자.
  • 하위클래스에 남아 있는 코드가 없으면 그 하위클래스를 삭제하자.
  • 부수적인 하위클래스를 모두 삭제할 때까지 위 과정을 계속하자. 새 계층구조를 관찰하면서 메서드 상향이나 필드 상향 같은 리팩토링 기법을 실시할 여자기 있는지 살핀다.

1.3 에제

  • 위의 그림에서 윗부분을 보자.
  • 이 계층구조는 Deal이 원래의 하나의 거래를 표시하는 용도로만 사용되다 보니 이렇게 된 것이다.
  • 여러 개의 거래를 하나의 표로 표시하면 좋겠다는 아이디어가 생각났다.
  • 간단한 하위클래스 ActiveDeal을 작성해서 실험해보면 정말 사소한 작업으로 표 형식을 표시할 수 있음을 알게 된다.
  • PassiveDeal의 표를 표시하는 것도 문제없다.
  • 작은 하위클래스를 하나 더 작성하면 된다.
  • 두 달 후 표 코드는 복잡해졌지만, 늘 그렇듯이 그 코드를 간단히 넣을 곳이 없고 시간은 촉박하다.
  • 표현 로직과 엉켜서 이제 새로운 종류의 Deal을 추가하기가 어려워졌다.
  • 다음 방법을 따라하자.
  • 우선 상속 계층에 의해 처리되는 기능들을 확인하자.
  • 첫 번째 기능은 거래 유형에 따른 변화를 감지하는 것이고, 두 번째 기능은 표현 스타일에 따른 변화를 감지하는 것이다.
Deal Active Deal Passive Deal
Tabular Deal    
  • 그 다음 어느 기능이 더 중요한지 결정하자.
  • 물건 거래가 표현 스타일보다 훨씬 중요하므로 Deal만 남겨두고 표현 스타일을 별도의 상속 계층으로 빼내자.
  • 사실상 거래와 가장 관련된 코드가 들어 있는 기능만 남겨야 하므로 옮길 코드는 더 적다.
  • 클래스 추출을 실시해서 하위클래스마다 표현 스타일을 작성하자.
  • 이제 추출한 클래스의 하위클래스를 작성하든지, 원본 클래스의 하위클래스 각각에 대해 하위클래스를 작성하자.
  • 그리고 인스턴스 변수를 적절한 하위클래스로 초기화하자.
ActvieDeal constructor
	... presentation = new SingleActivePresentationSytle();...
  • 원래보다 클래스가 더 많아졌는데, 전보다 더 좋아졌다고 의문이 들수도 있다.
  • 큰 장점을 위해 작은 단점을 감수해야 할 때도 있다.
  • 이렇게 복잡하게 얽힌 계층구조의 경우 추출한 객체의 계층구조 거의 대부분은 객체가 추출되고 나면 상당히 단순해진다.
  • 그러나 리팩토링을 한 번에 한 단계씩 거치는 것이 여러 단계를 한꺼번에 하는 것보다 더 안전하다.
  • 메서드 이동과 필드 이동을 실시해서 Deal의 하위클래스에 들어있는 표현 관련 메서드와 변수를 표현 스타일 클래스로 옮기자.
  • 여기까지 하면 TabularActiveDeal, TabularPassiveDeal 클래스에 남은 코드가 없으므로 삭제한다.
  • 두 기능을 분리했으니 이레 둘을 따로 단순화할 수 있다.
  • 이 리팩토링을 마치면 추출한 클래스와 원본 객체를 상당히 단순화할 수 있다.

2. 절차 코드를 객체로 전환

  • 코드가 절차식으로 작성되어 있을 땐 데이터 레코드를 객체로 바꾸고, 기능을 쪼개서 각각의 객체를 옮기자.

2.1 동기

  • 자바가 객체지향 언어이긴 해도 생성자를 호출하는 것보단 인스턴스화를 사용할 때나 더 많다.
  • 객체를 잘 사용할 수 있게 되기까지는 시행착오와 학습 시간이 필요하다.
  • 절차식 코드를 비교적 객체지향적으로 만들어야 할 때가 많다.
  • 보통은 클래스에 몇 가지 데이터, 읽기/쓰기 메서드만으로 구성된 덤 데이터 객체, 절차식 메서드가 들어 있다.
  • 완전한 절차식 프로그램을 변환할 때는 아예 이것조차 없을 수 있는데 차라리 그런 경우가 더 변환하기 쉽다.
  • 그렇다고 해서 데이터가 거의 없거나 아예 없는 기능 위주의 객체를 작성해서는 안 된다는 이야기는 아니다.
  • 기능을 변화시켜야 할 때는 작은 전략 객체를 사용할 때도 많다.
  • 단, 그런 절차 객체는 보통 작으며 유연성을 위한 특수 용도로만 사용된다.

2.2 방법

  • 각 레코드 타입을 읽기/쓰기 메서드만 있는 덤 데이터 객체로 바꾸자.
    • 관계 데이터베이스를 사용한다면 각 테이블을 덤 데이터 객체로 바꾸자.
  • 모든 절차 코드를 하나의 클래스에 넣자.
    • 그 클래스를 싱클턴으로 만들든지, 메서드를 static 타입으로 만들자. 싱글턴으로 만드는 방법은 다시 초기화할 때 쉽다.
  • 긴 프로시저는 대상으로 메서드 추출과 관련된 리팩토링 기법들을 실시해서 쪼개자. 프로시저를 쪼개면서 메서드 이동을 적용해서 각각을 적절한 덤 데이터 클래스로 옮기자.
  • 원본 클래스에서 모든 기능을 삭제하게 될 때까지 계속하자. 원본 클래스가 순수한 절차 클래스였다면 그 클래스를 삭제할 때 속이 시원할 것이다.

2.3 예제

  • 1장 예제 코드에서 볼 수 있다.
  • 첫 단계에서 statement 메서드를 쪼개서 여러 위치로 기능을 분산시킨 부분이 그렇다.
  • 이 리팩토링을 완료했으면 이제 스마트해진 데이터 객체를 대상으로 다른 리팩토링을 적용할 수 있다.

3.도메인 로직을 표현과 분리

  • 도메인 로직이 들어 있는 GUI 클래스가 있을 땐 도메인 로직을 별도의 도메인 클래스로 떼어내자.

3.1 동기

  • MVC 패턴의 핵심은 사용자 인터페이스 코드(뷰)와 도메인 로직(모델)을 분리하는 것이다.
  • 표현 클래스에는 사용자 인터페이스 처리에 필요한 로직만 들어간다.
  • 도메인 객체에는 표현이나 시각적 코드는 전혀 들어가지 않고 비즈니스 로직만 들어간다.
  • 이렇게 하면 프로그램의 복잡한 두 부분이 수정하기 쉬운 조각으로 분리되면, 하나의 비즈니스 로직에 여러 개의 표현을 구현할 수도 있다.

3.2 방법 & 예제

  • 오래된 책이다보니 요즘은 더 발전한 부분이라 생략

4. 계층구조 추출

  • 한 클래스에 기능이 너무 많고 일부분에라도 조건문이 많을 땐 각 조건에 해당하는 하위클래스를 작성해서 계층구조를 만들자.

4.1 동기

  • 처음엔 클래스를 간단히 작성해도 시간이 지날수록 복잡해진다.

4.2 방법

  • 방법은 두 가지다.
  1. 첫 번째 방법은 각 기능을 어디에 넣을지 모를 때 적용하면 된다.
  • 각 기능을 확인하고 구분 짓자.
    • 객체가 존재할 동안 각 기능이 변할 수 있다면, 클래스 추출을 실시해서 각각의 클래스로 빼내자.
  • 해당 특수 상황별 하위클래스를 작성하고 원본 클래스에 생성자를 팩토리 메서드로 전환을 적용하자. 적절한 하위클래스의 인스턴스를 반환하게 팩토리 메서드를 수정하자.
  • 조건문이 든 메서드를 한 번에 하나씩 하위클래스로 복사한 후 상위클래스와 하위클래스의 메서드의 기능을 알맞게 제한하여 단순화하자.
    • 필요하다면 상위클래스에 메서드 추출을 적용해서 메서드의 조건문을 나머지 코드와 분리하자.
  • 상위클래스의 모든 메서드에 하위클래스 구현부와 들어갈 때가지 위의 단계를 각 기능별로 계속하자.
  • 모든 하위클래스 안에 재정의한 메서드를 상위클래스에서 삭제하고 상위클래스를 abstract 타입으로 바꾸자.
  1. 처음부터 각 기능이 확실히 구분될 땐 다음과 같이 두 번째 방법을 사용하면 된다
  • 각 기능별 하위클래스를 작성하자.
  • 생성자를 팩토리 메서드로 전환을 적용해서 각 기능에 대응하는 하위클래스를 반환하게 하자.
    • 각 기능을 분류 부호로 구별했다면 분류 부호를 하위클래스로 전환을 실시하자.
    • 단, 각 기능이 클래스가 존재하는 동안 변할 수 있다면 분류 부호를 상태/전략 패턴으로 전환을 실시하자.
  • 조건문이 있는 메서드마다 조건문을 재정의로 전환을 적용하자. 각 메서드가 일부 코드만 다를 땐 다른 부분에만 메서드 추출을 적용해서 메서드로 빼내자.

4.3 예제

  • 각 기능 구분이 불분명한 경우의 예제이다.
  • 기능 부분이 분명한 경우의 예제는 분류 부호를 하위클래스로 전환, 분류 부호를 상태/전략 패턴으로 전환, 조건문을 재정의로 전환절의 리팩토링 예제를 참고하자.

  • 전기요금을 계산하는 프로그램이다.
  • 하절기, 동절기 계산 방식이 달라야 하며, 가정용, 소규모 사업장, 기초생활수급자 등 계산 방식이 다르다.
  • 따라서 BillingScheme 클래스가 복잡해지는게 당연하다.
  • 우선 조건문에서 차이 나는 부분을 골라야 한다. (여기선 장애인 요금을 가지고 한다)
  • 차이나는 부분을 빼낼 하위클래스를 작성하자.
  • 우선 생성자를 팩토리 메서드로 전환을 실시한 후 해당되는 경우에 장애인 요금을 반환하는 절을 작성하자.
  • BillingScheme 클래스에 있는 각종 메서드를 살피면서 장애인 요금 기준에 따라 다른 코드를 실행하는 조건문이 들어 있는 메서드를 찾자.
  • createBill 메서드도 조건문이 들어 있으니 윗 그림처럼 이 메서드를 하위클래스를 복사하자.

  • 하위클래스로 복사한 creatBill 메서드는 확실히 장애인 요금 기준으로만 계산하면 되므로 다음과 같이 단순화 할 수 있다.
if (disabilityScheme()) doSomething
  • 앞의 코드는 다음과 같이 바꿔도 된다.
doSomething
  • 장애인 요금이 사업장 요금과 중복 적용되지 않아야 한다면 사업장 요금 계산에 해당하는 조건문 코드를 전부 삭제하면 된다.
  • 이때 차이 나는 코드를 서로 같은 코드와 분리하는 게 좋다.
  • 이를 위해 메서드 추출과 메서드 쪼개기를 실시하자.
  • 장애인 요금에 해당하는 대부분의 조건문이 없어질 때까지 BillingScheme 클래스의 각종 메서드를 대상으로 이 과정을 반복한다.
  • 그런 다음, 기초생활수급자 등 다른 대상을 선택해서 같은 과정을 반복한다.
  • 그러나 두 번째 부터는 첫 번째 작업한 내용과 차이를 비교하는 방법을 살펴본다.
  • 목적은 같되 두 경우에 다른 기능의 메서드 호출을 넣을 수 있는 상황은 구분해야 한다.
  • 두 경우의 세금 계산법도 달라질 수 있다.
  • 두 메서드를 시그너처가 같은 각 하위클래스에 넣어야 한다.
  • 그러면 장애인 요금 계산 클래스를 수정해서 하위클래스를 연결할 수 있다.
  • 다른 기능 부분을 더 많이 메서드로 빼낼수록, 비슷한 메서드와 다른 메서드가 안정화되며 나중에 차이 나는 기능을 새로 넣기도 더 쉽다.