24장: 리팩터링

24장: 리팩터링

  • 현실에서 코드는 초기 개발 단계 만큼이나 유지보수 단계에서 상당수가 수정된다.
  • 최근에는 코드 변경 가능성을 염두해두고 코드를 작성하기에 코드가 어느 때보다 진화할 가능성이 높다.

24.1 소프트웨어 진화의 종류

  • 소프트웨어 진화는 생물학적 진화처럼 이로운 진화와 해로운 진화 모두 존재함.
  • 소프트웨어 진화 시 가장 중요한 것은 프로그램 품질의 향상 여부임.
  • 소프트웨어 진화가 구현 도중에 발생하는지, 유지보수도중에 발생하는지 또한 다른 진화 환경을 만들어 냄.

소프트웨어 진화의 철학

  • 진화가 무의식적으로 일어나는 현상이 아니라 불가피하고 중요한 과정임을 인식하고 계획을 세워야 함.
  • 변경은 위험인 동시에 시스템을 완벽에 가깝게 다듬을 수 있는 기회임.
  • 코드를 변경할 때는 나중에 더 쉽게 변경할 수 있도록 구조적 품질을 향상시켜야 함.
  • 소프트웨어 진화의 기본 원칙은 프로그램의 내적 품질을 꾸준히 높이는 것임.

24.2 리팩터링 소개

  • 소프트웨어를 더 쉽게 이해하고 적은 비용으로 수정할 수 있도록 외부 동작의 변화 없이 내부 구조를 변경하는 것임.

리팩터링하는 이유

  • 코드가 중복되어 있다: 한 곳을 변경할 때 다른 곳도 병렬 수정해야 하며 이는 DRY(반복하지마라.) 법칙 위반이자 설계 오류임.

  • 루틴이 너무 길다: 객체지향에서는 한 화면을 넘어가는 루틴이 거의 필요 없으며 모듈화를 높여 명확성을 확보해야 함.

  • 루프가 너무 길거나 깊이 중첩되어 있다: 루프 내부 구조를 별도 루틴으로 추출하면 코드 분해와 복잡도 감소에 효과적임.

  • 클래스의 응집력이 약하다: 연관 없는 여러 기능을 담당하는 클래스는 관련 역할별로 여러 클래스로 분할해야 함.

  • 클래스 인터페이스가 일관된 추상화 수준을 제공하지 않는다: 편의 위주의 변경이 누적되면 무결성을 잃어 본래 목적과 다른 인터페이스가 됨.

  • 매개변수가 너무 많다: 잘 분리된 프로그램은 작고 잘 정의된 루틴을 가지며 긴 매개변수는 추상화 부실을 나타냄.

  • 클래스 내의 변경 사항이 상호 관계를 고려하지 않고 구분되는 경향이 있다: 한 클래스가 둘 이상의 책임을 지고 있다는 신호이므로 책임별 분리가 필요함.

  • 변경할 때 여러 개의 클래스를 동시에 수정해야 한다: 같은 클래스 집합을 반복 수정한다면 변경 영향이 한 클래스에 집중되도록 재정렬해야 함.

  • 상속 계층 구조가 병렬로 변경되어야 한다: 클래스 확장 시 다른 클래스의 서브클래스도 함께 만들어야 하는 병렬 수정을 해결해야 함.

      // 캐릭터 계층 클래스                    // 무가 계층 클래스 (직업 따라 강제로 만들어야 함)
      class Warrior extends Character {}  --> class WarriorSword extends Weapon {}
      class Mage extends Character {}     --> class MageStaff extends Weapon {}
      class Archer extends Character {}   --> class ArcherBow extends Weapon {}
    
  • case 문이 병렬로 변경되어야 한다: 여러 위치에서 유사한 case 문을 함께 수정하고 있다면 다형성을 활용한 상속이 나은 대안임.
      class Character {
          String type; // WARRIOR, MAGE
    
          // 1. 공격 동작 분기
          void attack() { 
              switch (type) {
                  case "WARRIOR": System.out.println("검 공격"); break;
                  case "MAGE": System.out.println("마법 공격"); break;
                  // ARCHER 추가 시 여기에 case "ARCHER" 추가 필요.
                  // 오버라이딩 활용하는 것이 효과적
              }
          }
      }
    
  • 함께 사용되는 연관된 데이터 항목이 클래스로 구성되지 않았다: 동일 데이터 집합이 반복 처리된다면 고유 클래스로 결합하는 것이 바람직함.

  • 루틴이 자신이 포함된 클래스보다 다른 클래스의 기능을 더 많이 사용한다: 해당 루틴은 주 타깃이 되는 다른 클래스로 이동시키는 것이 맞음.

  • 기본 데이터형을 오버로드했다: 정수 등 기본형 대신 Money, Temperature 같은 전용 클래스를 생성해야 컴파일러의 형 검사 보호를 받음.

  • 클래스가 많은 일을 수행하지 않는다: 리팩터링 후 남은 무의미한 클래스는 책임을 이전하고 삭제하는 편이 나음.

  • 일련의 루틴이 뜨내기 데이터를 전달한다: 단순히 다른 루틴으로 데이터를 전달만 하는 구조는 인터페이스 추상화 일관성을 재검토해야 함.

  • 중개 역할을 하는 객체가 아무것도 하지 않는다: 대부분의 코드가 타 클래스 호출 대행에 불과하다면 중개 클래스를 제거하고 직접 호출함.

  • 한 클래스가 지나치게 다른 클래스를 참견한다: 필요 이상으로 타 클래스 세부사항을 알면 캡슐화를 강화하여 격리해야 함.

  • 루틴의 이름이 엉성하다: 발견 즉시 호출부까지 모두 이름을 변경해야 나중에 발생할 더 큰 수고를 방지함.

  • 공개 데이터 멤버다: 인터페이스와 구현의 경계를 허물고 캡슐화를 위반하므로 접근 루틴 뒤로 숨겨야 함.

  • 서브클래스가 부모 클래스 루틴의 일부만을 사용한다: is-a 관계가 아닌 우연한 코드 재사용이므로 has-a(상속 대신 위임) 관계로 전환함.

  • 주석을 이용해 어려운 코드를 설명한다: 나쁜 코드를 설명하는 도구로 주석을 쓰지 말고 코드 자체를 다시 작성해야 함.

  • 전역 변수를 사용한다: 전역 변수는 접근 루틴으로 고립시키거나 더 직관적인 국소 변수 구조로 리팩터링해야 함.

  • 루틴 호출 전후로 설정 및 분해 코드를 사용한다: 호출 전후에 불필요한 객체 생성/해제나 값 설정/복구 작업이 포함되면 인터페이스 추상화가 잘못된 것임.
      //루틴 호출에 대한 설정 및 분해 코드를 사용하고 있는 나쁜 예제
      WithdrawalTransaction withdrawal;
      withdrawal.SetCustomerId( customerId );
      withdrawal.SetBalance( balance );
      withdrawal.SetWithdrawalAmount( withdrawalAmount );
      withdrawal.SetWithdrawalDate( withdrawalDate );
      ProcessWithdrawal( withdrawal );
      customerId = withdrawal.GetCustomerId();
      balance = withdrawal.GetBalance();
      withdrawalAmount = withdrawal.GetWithdrawalAmount();
      withdrawalDate = withdrawal.GetWithdrawalDate();
    
      //메서드 호출에 대한 설정 및 분해 코드를 사용하고 있는 나쁜 예제
      withdrawal = new WithdrawalTransaction( customerId, balance, withdrawalAmount, withdrawalDate );
      withdrawal.ProcessWithdrawal();
      delete withdrawal;
    
      //설정 코드나 분해 코드를 요구하지 않은 루틴에 대한 좋은 예제 (+캡슐화)
      ProcessWithdrawal( customerId, balance, withdrawalAmount, withdrawalDate );
    
      //여러 개의 메서드 호출이 필요한 예제
      ProcessWithdrawal( withdrawal.GetCustomerId(), withdrawal.GetBalance(), withdrawal.GetWithdrawalAmount(), withdrawal.GetWithdrawalDate() );
    
  • 프로그램이 언젠가 필요할 것 같은 코드를 포함하고 있음: 미래 예측 코드는 잘못된 추측으로 버려지거나 복잡성 및 결함만 유발하므로 현재 요구사항에 집중해 작성해야 함.

체크리스트: 리팩터링하는 이유

  • 코드가 중복되어 있는가?
  • 루틴이 너무 긴가?
  • 루프가 너무 길거나 깊이 중첩되어 있는가?
  • 클래스의 응집력이 약한가?
  • 클래스 인터페이스가 일관된 추상화 수준을 제공하지 않는가?
  • 매개변수가 너무 많은가?
  • 클래스 내의 변경 사항이 상호 관계를 고려하지 않고 구분되는 경향이 있는가?
  • 상속 계층 구조가 병렬로 변경되어야 하는가?
  • case 문이 병렬로 변경되어야 하는가?
  • 함께 사용되는 연관된 데이터 항목이 클래스로 구성되지 않았는가?
  • 루틴이 자신이 포함된 클래스보다 다른 클래스의 기능을 더 많이 사용하는가?
  • 기본 데이터형을 오버로드하고 있는가?
  • 클래스가 많은 일을 수행하지 않는가?
  • 일련의 루틴이 뜨내기 데이터를 전달하고 있는가?
  • 중개 역할을 하는 객체가 아무것도 하지 않는가?
  • 한 클래스가 지나치게 다른 클래스를 참견하는가?
  • 루틴의 이름이 엉성한가?
  • 공개 데이터 멤버를 사용하고 있는가?
  • 서브클래스가 부모 클래스가 제공하는 루틴의 일부만 사용하는가?
  • 어려운 코드를 설명하기 위해 주석을 사용하는가?
  • 전역 변수를 사용하는가?
  • 루틴이 루틴을 호출하기 전에 설정 코드를 사용하거나 루틴을 호출한 다음에 결과를 나누는 코드를 사용하는가?
  • 프로그램이 언젠가 필요할 것 같은 코드를 포함하고 있는가?

리팩터링하면 안되는 이유

  • 막연한 기능 추가 및 설계 변경 그 자체는 미덕이 아님.
  • 원칙이 적용된 목적이 있는 변경은 유지보수를 통해 꾸준히 프로그램의 품질을 향상시키는 방법임.

24.3 구체적인 리팩터링

데이터 수준 리팩터링

  • 매직 넘버를 이름 상수로 대체함: 코드 내 의미가 불분명한 숫자나 문자 리터럴을 의미 있는 이름 상수로 변경함.

  • 변수 이름을 더 분명하고 많은 정보를 제공하는 이름으로 다시 지음: 의미가 모호한 변수, 상수, 클래스, 루틴 이름을 명확한 표현으로 변경함.
  • 표현식을 인라인화함: 중간 변수에 표현식 결과를 임시 할당하는 대신 표현식 자체를 직접 사용함.
  • 표현식을 루틴으로 대체함: 중복 사용되거나 복잡한 표현식을 별도 루틴으로 추출함.
  • 중간 변수를 사용함: 복잡한 표현식의 결과를 목적이 명확히 드러나는 이름을 가진 중간 변수에 할당함.
  • 여러 목적으로 사용되는 변수를 단일 목적을 갖는 변수 여러 개로 변환함: i, temp, x 등 여러 용도로 재조작되는 변수를 용도별 개별 변수로 분리함.
  • 로컬에서 사용할 목적이라면 매개변수 대신 지역 변수를 사용함: 입력 전용 매개변수를 루틴 내부에서 변경하며 사용하는 대신 별도 지역 변수를 만들어 활용함.
  • 기본형 데이터를 클래스로 변환함: 기본형 데이터에 엄격한 형 검사나 추가 데이터·행위가 요구되면 클래스나 열거형으로 변환함.
  • 형 선언 코드 집합을 클래스나 열거형으로 변환함: 독립적인 정수 상수 집합을 클래스나 열거형으로 묶어 컴파일러의 형 검사 보호를 받도록 함.
      //나쁜 예제
      const int SCREEN = 0;
      const int PRINTER = 1;
      const int FILE = 2;
    
      //좋은 예제
      enum class OutputType {
      Screen,
      Printer,
      File
      };
    
  • 형 선언 코드 집합을 서브클래스를 갖는 클래스로 변환함: 형 코드별로 서로 다른 행위가 존재할 경우 다형성을 활용하도록 서브클래스 구조로 변환함.
  • 배열을 객체로 변경함: 각 요소가 서로 다른 의미나 형식을 갖는 배열을 멤버 필드를 가진 객체로 변환함.
      //나쁜 예제
      int player[4];
      player[0] = 1001; // ID
      player[1] = 100;  // HP
      player[2] = 50;   // MP
      player[3] = 25;   // 공격력
        
      //좋은 예제
      struct Player {
          int id;
          int hp;
          int mp;
          int attackPower;
      };
    
  • 컬렉션을 캡슐화함: 클래스가 내부 컬렉션을 노출할 때는 읽기 전용으로 리턴하게 하고 추가·삭제는 전용 루틴을 제공함.
  • 전형적인 레코드를 데이터 클래스로 대체함: 단순 레코드 멤버를 클래스로 감싸 오류 검사와 관련 연산을 고립시킴.

명령문 수준 리팩터링

  • 불린 표현식을 분해함: 의미를 분명히 나타내는 중간 변수를 사용하여 복잡한 불린 조건식을 단순화함.

  • 복잡한 불린 표현식을 명확한 이름의 불린 함수로 옮김: 가독성을 높이고 중복 표현식의 병렬 수정 및 오류 가능성을 줄임.
  • 서로 다른 조건문 내에 중복으로 사용된 코드를 결합함: if 및 else 블록 끝에 동일하게 작성된 코드는 조건문 블록 뒤로 이동시킴.
  • 루프 제어 변수 대신 break나 return을 사용함: done과 같은 제어 변수 대신 break/return을 사용해 루프를 명확히 탈출함.
  • 중첩된 if-then-else 명령문 내에서 리턴 값을 할당하는 대신 답을 알았을 때 곧바로 리턴함: 답이 확인된 시점에 즉시 탈출함으로써 가독성을 높이고 오류 발생률을 줄임. (early return)
  • 조건문(특히 반복되는 case 문)을 다형성으로 대체함: 반복 발생하는 case 문 논리를 상속 계층과 다형성 루틴 호출 구조로 재구성함.
  • 널 값을 테스트하는 대신 널 개체를 생성하여 사용함: 클라이언트 코드가 null 검사를 반복하는 대신 널 개체 자체가 기본 행위나 데이터를 처리하도록 책임을 이전함.
      User* user = FindUser(id);
      if (user != nullptr) {
          user->SendMessage("Hello");
      }
    
      
      User* user = FindUser(id);  // Null 개체 반환  
      user->SendMessage("Hello"); // Null 개체라면 내부에서 그냥 아무 일도 안 함
    

루틴 수준 리팩터링

  • 루틴을 추출함/메서드를 추출함: 루틴에서 인라인 코드를 추출하여 별도의 개별 루틴으로 변환함.

  • 루틴의 코드를 인라인화함: 코드가 단순하고 직관적인 경우 해당 루틴 본문을 호출부에 직접 삽입함.
  • 긴 루틴을 클래스로 변환함: 루틴이 너무 긴 경우 독립된 클래스로 전환한 뒤 여러 작은 루틴으로 분해해 가독성을 높임.
  • 복잡한 알고리즘 대신 간단한 알고리즘을 사용함: 지나치게 복잡한 논리를 가독성이 우수하고 간단한 알고리즘으로 대체함.
  • 매개변수를 추가함: 루틴 수행에 추가 정보가 필요해진 경우 호출 인터페이스에 매개변수를 추가함.
  • 매개변수를 제거함: 루틴 내부에서 더 이상 사용하지 않는 불필요한 매개변수를 인터페이스에서 삭제함.
  • 변경 연산과 쿼리 연산을 구분함: 객체 상태를 변경하는 연산과 상태를 조회하는 쿼리 연산을 완전히 독립된 두 루틴으로 분리함.
  • 매개변수를 이용하여 유사한 루틴을 결합함: 내부 상수 값만 다르고 구조가 유사한 루틴들을 상수를 매개변수로 전달받는 하나의 루틴으로 통합함.
  • 전달되는 매개변수에 따라 행동하는 루틴을 분리함: 매개변수 플래그 값에 따라 수행 동작이 달라지는 루틴을 개별 동작을 수행하는 독립된 루틴들로 분리함.
  • 특정한 필드 대신 전체 객체를 전달함: 한 객체의 여러 필드를 개별 매개변수로 넘기는 대신 객체 전체를 전달하여 인터페이스를 간소화함.
  • 전체 객체 대신 특정한 필드만 전달함: 루틴에 필요 없는 객체 전체를 넘기기보다 실제로 사용하는 특정 필드만 전달하여 결합도를 낮춤.
  • 다운캐스팅을 캡슐화함: 호출자가 직접 다운캐스팅하지 않도록 루틴이 가능한 가장 구체적인 타입을 리턴하게 만듦.

클래스 구현 리팩터링

  • 값 객체를 참조 객체로 변경함: 크고 복잡한 객체 복사본을 다량 생성해 관리하는 대신 하나의 마스터 객체만 두고 참조로 접근하게 변경함.

  • 참조 객체를 값 객체로 변경함: 작고 단순한 객체에 수많은 참조 코드가 난무하는 경우 모든 객체를 값 객체로 다루도록 사용법을 변경함.
  • 가상 루틴을 데이터 초기화로 대체함: 리턴 값만 다른 서브클래스들의 가상 루틴 오버라이딩을 지우고, 기본 클래스가 초기화된 상수 값을 활용하도록 일반화함.
  • 멤버 루틴이나 데이터의 위치를 변경함: 중복 제거나 특수화 지원을 위해 상속 계층 구조 내에서 루틴, 필드, 생성자 코드를 슈퍼클래스 또는 파생 클래스로 이동시킴.
  • 특화된 코드를 서브클래스로 추출함: 클래스 내 특정 인스턴스만 사용하는 특화 코드를 별도의 서브클래스로 분리 이동시킴.
  • 유사한 코드를 슈퍼클래스로 결합함: 두 서브클래스가 공유하는 유사한 코드를 통합하여 공통 슈퍼클래스로 이동시킴.

클래스 인터페이스 리팩터링

  • 루틴을 다른 클래스로 이동시킴: 해당 루틴 코드를 대상 클래스로 옮기고 기존 위치에서는 새 루틴을 호출하도록 전환함.

  • 한 클래스를 두 개로 변환함: 하나의 클래스가 둘 이상의 별개 책임을 지고 있다면 각각 명확한 책임을 갖는 개별 클래스들로 분할함.
  • 클래스를 제거함: 역할이 거의 없는 클래스의 코드는 다른 응집력 있는 클래스로 옮기고 해당 클래스는 삭제함.
  • 위임을 숨김: 클라이언트가 타깃 객체에 직접 접근하는 대신 중계 클래스의 인터페이스를 통해 숨겨진 객체와 통신하도록 캡슐화함.
  • 중개자를 제거함: 중개 클래스가 단순히 호출 대행만 수행한다면 중개자를 제거하고 클라이언트가 대상 클래스를 직접 호출하게 변경함.
  • 상속을 위임으로 대체함: 슈퍼클래스의 인터페이스 제어가 필요한 경우 상속 대신 대상 클래스를 멤버 필드로 두고 필요한 루틴만 노출함.
  • 위임을 상속으로 대체함: 클래스가 위임 객체의 모든 공개 루틴을 단순히 노출만 하고 있다면 위임 대신 해당 클래스를 상속함.
  • 외부 루틴을 도입함: 수정할 수 없는 기존 클래스에 추가 루틴이 필요하면 클라이언트 클래스 내에 독립 루틴을 생성함.
  • 확장 클래스를 도입함: 수정 불가능한 클래스에 여러 추가 기능이 필요하다면 서브클래싱이나 래퍼(Wrapper) 클래스를 작성함.
  • 노출된 멤버 변수를 캡슐화함: public 멤버 변수를 private으로 전환하고 전용 접근 루틴을 통해 값을 노출함.
  • 변경할 수 없는 필드에 대한 Set() 루틴을 제거함: 생성 시점에만 결정되는 불변 필드는 오해를 부르는 Set() 루틴을 지우고 생성자에서 초기화함.
  • 클래스 외부에서 사용하면 안 되는 루틴을 숨김: 인터페이스 응집도를 높이기 위해 외부 불필요 루틴의 접근 제어를 제한함.
  • 사용되지 않는 루틴을 캡슐화함: 자주 사용되는 인터페이스만 묶어 응집력 있는 추상화를 제공하는 전용 인터페이스/클래스로 감쌈.
  • 슈퍼클래스와 서브클래스의 구현이 매우 유사하다면 이 둘을 결합함: 별다른 특수화 역할을 하지 못하는 서브클래스는 슈퍼클래스로 통합함.

시스템 수준 리팩터링

  • 제어할 수 없는 데이터에 대해 명확한 참조 소스를 생성함: GUI 컨트롤 등 접근 및 일관성 유지가 어려운 데이터는 이를 전담 제어하는 별도 데이터 클래스를 만들어 유일한 참조 소스로 다룸.
      // GUI 화면 클래스 나쁜 예제
      class UserSettingForm {
      private:
          QLineEdit* txtAge; // GUI 나이 입력창 
          QLineEdit* txtVolume; // GUI 볼륨 슬라이더/입력창
    
      public:
          void OnSaveButtonClicked() { //UI 컨트롤에서 직접 데이터를 긁어와 로직을 수행함
              int age = std::stoi(txtAge->text().toStdString()); 
              ServerAPI::SendUserData(txtAge->text().toStdString());
          }
      };
    
      // GUI 화면 클래스 좋은 예제
      class UserDataModel {
      private:
          int age = 0; 
          int volume = 50;
    
      public:
          void SetAge(int newAge) {  age = newAge; }
          int GetAge() const { return age; }
          int GetVolume() const { return volume; }
      };
    
      class UserSettingForm {
      private:
          QLineEdit* txtAge;
          UserDataModel& userModel; 
    
      public:
          UserSettingForm(UserDataModel& model) : userModel(model) {}
    
          // GUI -> Data Model 반영
          void OnAgeInputChanged() {
              int age = std::stoi(txtAge->text().toStdString());
              userModel.SetAge(age); 
          }
    
          // Data Model -> GUI 반영 
          void UpdateUI() {
              txtAge->setText(std::to_string(userModel.GetAge()));
          }
      };
    
  • 단방향 클래스 관계를 양방향 클래스 관계로 바꿈: 서로의 기능을 공유해야 하나 한쪽만 인지하고 있다면 양쪽이 모두 접근 가능하도록 관계를 변경함.
  • 양방향 클래스 관계를 단방향 클래스 관계로 바꿈: 복잡하게 얽힌 두 클래스 중 실제로는 한쪽만 참조하면 충분한 경우 결합도를 낮추기 위해 단방향 관계로 단순화함.
  • 간단한 생성자 대신 팩토리 메서드를 제공함: 타입 코드에 따른 객체 생성이나 참조 객체 처리가 필요할 때 생성자 직접 호출 대신 팩토리 루틴을 사용함.
  • 오류 코드를 예외로 대체하거나 그 반대로 함: 전체 시스템의 오류 처리 전략에 맞춰 예외 처리 방식과 오류 코드 전달 방식을 일관되게 정립함.

체크리스트: 구체적인 리팩터링

데이터 수준 리팩터링

  • 매직 넘버를 이름 상수로 대체했는가?
  • 변수 이름을 더 분명하고 많은 정보를 제공하는 이름으로 다시 지었는가?
  • 표현식을 인라인화했는가?
  • 표현식을 루틴으로 대체했는가?
  • 중간 변수를 사용했는가?
  • 여러 목적으로 사용되는 변수를 단일 목적을 갖는 변수 여러 개로 변환했는가?
  • 로컬에서 사용할 목적이라면 매개변수 대신 지역 변수를 사용했는가?
  • 기본 데이터를 클래스로 변환했는가?
  • 형 선언 코드 집합을 클래스나 열거형으로 변환했는가?
  • 형 선언 코드 집합을 서브클래스가 있는 클래스로 변환했는가?
  • 배열을 객체로 변경했는가?
  • 컬렉션을 캡슐화했는가?
  • 전형적인 레코드를 데이터 클래스로 대체했는가?

명령문 수준 리팩터링

  • 불린 표현식을 분해했는가?
  • 복잡한 불린 표현식을 잘 명명된 불린 함수로 이동했는가?
  • 서로 다른 조건문 내에 중복으로 사용된 코드를 결합했는가?
  • 루프 제어 변수 대신 break나 return을 사용했는가?
  • 중첩된 if-then-else 명령문 내에서 리턴 값을 할당하는 대신 답을 알았을 때 곧바로 리턴했는가?
  • 조건문(특히 반복되는 case 문)을 다형성으로 대체했는가?
  • 널 값을 테스트하는 대신 널 객체를 생성하여 사용했는가?

루틴 수준 리팩터링

  • 루틴을 추출했는가?
  • 루틴의 코드를 인라인화했는가?
  • 긴 루틴을 클래스로 변환했는가?
  • 복잡한 알고리즘 대신 간단한 알고리즘을 사용했는가?
  • 매개변수를 추가했는가?
  • 매개변수를 제거했는가?
  • 변경 연산으로부터 쿼리 연산을 분리했는가?
  • 매개변수를 이용하여 유사한 루틴을 결합했는가?
  • 전달되는 매개변수에 따라 행동하는 루틴을 분리했는가?
  • 특정한 필드 대신 전체 객체를 전달했는가?
  • 전체 객체 대신 특정한 필드만 전달했는가?
  • 다운캐스팅을 캡슐화했는가?

클래스 구현 리팩터링

  • 값 객체를 참조 객체로 변경했는가?
  • 참조 객체를 값 객체로 변경했는가?
  • 가상 루틴을 데이터 초기화로 대체했는가?
  • 멤버 루틴이나 데이터의 위치를 변경했는가?
  • 특화된 코드를 서브클래스로 추출했는가?
  • 유사한 코드를 슈퍼클래스로 결합했는가?

클래스 인터페이스 리팩터링

  • 루틴을 다른 클래스로 이동했는가?
  • 한 클래스를 두 개로 변환했는가?
  • 클래스를 제거했는가?
  • 위임을 숨겼는가?
  • 중개자를 제거했는가?
  • 상속을 위임으로 대체했는가?
  • 위임을 상속으로 대체했는가?
  • 외부 루틴을 도입했는가?
  • 확장 클래스를 도입했는가?
  • 노출된 멤버 변수를 캡슐화했는가?
  • 변경할 수 없는 필드에 대한 Set() 루틴을 제거했는가?
  • 클래스 외부에서 사용하면 안 되는 루틴을 숨겼는가?
  • 사용되지 않는 루틴을 캡슐화했는가?
  • 슈퍼클래스와 서브클래스의 구현이 매우 유사하다면 이 둘을 결합했는가?

시스템 수준 리팩터링

  • 제어할 수 없는 데이터에 대해 명확한 참조 소스를 생성했는가?
  • 단방향 클래스 관계를 양방향 클래스 관계로 바꿨는가?
  • 양방향 클래스 관계를 단방향 클래스 관계로 바꿨는가?
  • 간단한 생성자 대신 팩토리 메서드를 제공했는가?
  • 오류 코드를 예외로 대체하거나 그 반대로 했는가?

24.4 안전한 리팩터링 방법

안전한 리팩터링 지침

  • 리팩터링을 시작하기 전에 코드를 저장함: 언제든지 원본 코드로 복구할 수 있도록 버전 관리 시스템에 저장하거나 백업 디렉터리에 복사함.

  • 리팩터링을 작게 유지함: 변경 내용이 시스템에 미치는 영향을 완벽히 파악할 수 있도록 리팩터링의 범위를 소규모로 관리함.
  • 리팩터링은 한 번에 하나만 수행함: 복잡한 작업일수록 한 단계씩 진행하며, 다음 리팩터링 전 반드시 재컴파일 및 재테스트를 수행함.
  • 수행할 단계에 대한 목록을 만듦: 목표 지점까지의 단계별 리팩터링 목록을 사전 작성하여 상황 변화에 능동적으로 대응함.
  • 주차장을 만듦: 당장 진행 중인 작업과 직접 관련 없는 아이디어나 변경 사항은 ‘주차장’ 목록에 기록해 두고 나중에 처리함.
  • 체크포인트를 자주 설정함: 작업이 의도치 않은 방향으로 흘러갈 때 즉시 작동 가능한 상태로 돌아올 수 있도록 자주 저장함.
  • 컴파일러 경고를 활용함: 컴파일러의 경고 수준을 가장 엄격하게 설정하여 자잘한 실수를 즉각 감지하고 수정함.
  • 다시 테스트함: 코드를 변경한 뒤에는 기존 테스트 케이스를 재실행하여 회귀 오류가 발생하지 않았는지 확인함.
  • 테스트 케이스를 추가함: 리팩터링된 신규 코드를 검증하는 단위 테스트를 추가하고, 효용이 다한 옛 테스트는 제거함.
  • 변경 사항을 검토함: 소규모 변경일수록 안일하게 처리해 오류가 발생하기 쉬우므로, 한 줄 수정이라도 반드시 엄격한 검토 과정을 거침.
  • 리팩터링의 위험 수준에 따라서 접근 방법을 조절함: 상수 변환 등 안전한 작업은 능률적으로 처리하되, 인터페이스나 데이터베이스 변경 등 위험도 높은 작업은 짝 프로그래밍이나 동료 검토를 거침.

리팩터링에 좋지 않은 시기

  • 코드를 작성하고 수정하는 것을 감추는 용도로 리팩터링을 사용하지 않음: 동작하지 않는 결함 코드를 임시방편으로 수정하는 행위는 리팩터링이 아니라 해킹에 불과함.

  • 코드를 재작성하는 대신 리팩터링하지 않음: 구조적 결함이 심각한 코드는 누더기식 수정 대신 완전히 버리고 처음부터 다시 설계·구현함.

24.5 리팩터링 전략

리팩터링 대상 선택 지침

  • 루틴을 추가할 때 리팩터링함: 신규 루틴을 추가할 때 연관된 기존 루틴의 구조를 검사하고 미흡한 부분을 개선함.

  • 클래스를 추가할 때 리팩터링함: 클래스 추가 과정에서 드러나는 기존 코드의 문제점을 발견하여 연관 클래스를 함께 리팩터링함.
  • 결함을 수정할 때 리팩터링함: 버그 수정 과정을 통해 파악한 구조적 원인을 바탕으로 유사한 결함 위험이 있는 코드를 선제 개선함.
  • 오류를 유발할 가능성이 있는 모듈을 대상으로 삼음: 팀원들이 수정을 두려워하는 불안정한 고위험 모듈을 정면으로 다루어 리팩터링함.
  • 복잡도가 높은 모듈을 대상으로 삼음: 가장 복잡한 모듈에 리팩터링 노력을 집중하면 시스템 전체의 품질을 효율적으로 향상시킬 수 있음.
  • 유지보수 환경에서는 자신이 맡은 부분을 개선함: 변경될 일이 없는 코드는 그대로 두되, 손대는 코드는 기존보다 더 나은 상태로 만들어 반납함.
  • 정돈된 코드와 엉성한 코드 사이의 인터페이스를 정의한 후 인터페이스를 통해 코드를 이동시킴: 혼란스러운 현실 세계의 코드와 새로운 이상적 코드 사이에 경계 인터페이스를 구축하고, 코드를 다룰 때마다 정돈된 영역으로 차근차근 이관함.

체크리스트: 안전한 리팩터링 방법

  • 각 변경 사항이 체계적인 변경 전략의 일부인가?
  • 리팩터링을 시작하기 전에 코드를 저장했는가?
  • 각 리팩터링을 작게 유지하고 있는가?
  • 리팩터링을 한 번에 하나만 수행하는가?
  • 리팩터링 중에 취할 단계에 대한 목록을 만들었는가?
  • 리팩터링하는 도중에 발생하는 아이디어를 기억할 주차장을 만들었는가?
  • 리팩터링한 후 다시 테스트했는가?
  • 변경 사항이 컴파일되는지 혹은 중요한 코드에 영향을 미치는지 검토했는가?
  • 특정한 리팩터링의 위험성과 그에 따라 접근 방법을 조정할 것을 고려해 봤는가?
  • 변경 사항이 프로그램의 내부 품질을 떨어뜨리지 않고 향상시키고 있는가?
  • 코드를 작성하고 수정하는 것을 감추는 용도나 나쁜 코드를 다시 작성하는 것을 감추는 용도로 리팩터링을 사용하지 않았는가?

참고 자료

  • 마틴 파울러, 리팩토링: 코드 품질을 개선하는 객체지향 사고법』(한빛미디어, 2012): 리팩터링 기법과 단계별 코드 예제를 상세히 다루는 확실한 지침서임.

요점 정리

  • 프로그램이 초기 개발 시와 초기 배포 후에 변경될 수 있다는 것은 피할 수 없는 현실이다.

  • 소프트웨어는 변경을 거치면서 향상되거나 손상될 수 있으며, 소프트웨어 진화의 기본 원칙은 코드 진화 시 반드시 내부적인 품질을 향상시키는 것이다.
  • 리팩터링을 성공적으로 수행하기 위한 핵심 요건은 리팩터링의 필요성을 암시하는 다양한 경고 신호와 코드 냄새에 주의를 기울이는 것이다.
  • 성공적인 리팩터링을 위한 또 다른 핵심 요소는 다양하고 구체적인 리팩터링 기법들을 숙지하고 활용하는 것이다.
  • 리팩터링을 안전하게 수행하기 위해서는 변경 사항을 작게 유지하고 주기적인 테스트와 검토를 거치는 체계적인 접근 전략을 갖춰야 한다.
  • 개발 단계에서의 리팩터링은 프로그램을 최상의 구조로 개선하고 원래 의도했던 완성도에 도달할 수 있는 가장 좋은 기회를 제공한다.

results matching ""

    No results matching ""