본문 바로가기
1_ 맛있는프로그래밍/Cord lab

리팩토링(Software Cord Refactoring)

by 준환이형님 2014. 2. 28.

이번에 다녀온 교육에 대한 잘 정리된 내용이 있어 공유합니다


- 출처 : 최성민님 블로그 (http://forclack.egloos.com/523040)

- 그림출처 : 돼지고기님 블로그 (http://please261.tistory.com/entry/%EB%A6%AC%EB%B7%B0-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%BD%94%EB%93%9C-%ED%92%88%EC%A7%88%EC%9D%84-%EA%B0%9C%EC%84%A0%ED%95%98%EB%8A%94-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%82%AC%EA%B3%A0%EB%B2%95-%EB%A6%AC%EB%B7%B0)


1. Refactoring 개요

 1-1 Refactoring 의 정의
  리팩토링은 소프트웨어 시스템의 원래 기능은 그대로 두면서 내부의 구조를 개선하는 것을 의미한다. 
  그것은 버그의 가능성을 최소화하기 위해서 코드를 깔끔하게 정리하는 엄정한 방법이다. 
  한마디로 리팩토링을 한다는 것은 이미 작성된 코드의 설계를 나중에 개선하는 것이다.

  "코드가 작성된 후에 디자인을 개선한다."

 1-2 왜 Refactoring을 해야 하는가
  - 리팩토링은 소프트웨어의 디자인을 개선시킨다. 
  - 리팩토링은 소프트웨어를 더 이해하기 쉽게 만든다. 
  - 리팩토링은 버그를 찾도록 도와준다. 
  - 리팩토링은 프로그램을 빨리 작성하도록 도와준다.
 
  "나는 훌륭한 프로그래머는 아니다. 그냥 훌륭한 습관을 가지고 있는 좋은 프로그래머이다. - Kent Beck"

 1-3. 언제 Refactoring을 해야 하는가
  - 삼진 규칙(세 번째로 비슷한 것을 하게 되면 리팩토링 한다.).
  - 기능을 추가할 때 리팩토링을 하라.
  - 버그를 수정해야 할 때 리팩토링을 하라.
  - 코드 검토(code review)를 할 때 리팩토링을 하라.

  여기서 또 Kent Beck의 '리팩토링이 작동하는 이유'라는 글이 나온다. 
  "프로그램은 두 종류의 가치를 가지고 있는데 하나는 오늘 당장을 위한 것, 하나는 내일을 위한 것이라고 한다. 
  오늘 할일은 확실하지만 내일 할일은 알 수 없다. 
  하지만 오늘만을 위해서 일한다면 내일은 전혀 일을 할 수 없을 지도 모른다. 
  리팩토링은 이런 상황에서 빠져 나오는 방법이다. "

 1-4. Refactoring을 할 때의 문제
  - 데이터베이스
  - 인터페이스 변경
  - 리팩토링이 어려운 디자인 변경
  - 언제 리팩토링을 하지 말아야 하는가?

 1-5. Refactoring과 디자인 
  1.5.1 리팩토링이 없다면?
   - 초기 디자인을 할 때 완벽하게 하려고 많은 시간과 노력을 들이게 된다.
   - 다시 디자인을 변경하는 것은 비용이 너무 크다.

  1.5.2 리팩토링의 장점
   - 초기 디자인을 할 때 부담이 없다.
   - 단순한 디자인을 가지고 시작하게 된다.

  "시스템이 어떻게 돌아가는지 정확하게 알고 있다 하더라도, 
   추측만 하지 말고 실제로 퍼포먼스를 측정해보라. 
   무엇인가 배울 것이고, 십중팔구는 추측이 틀렸을 것이다."

 1-6. Refactoring과 퍼포먼스
  - 소프트웨어를 더 이해하기 쉽게 만들기 위한 수정은 종종 프로그램을 더 느리게 하기도 한다.
  - 리팩토링을 해 놓으면 코드가 잘 분해되어 있으므로 기능을 더 빨리 추가할 수 있어 퍼포먼스 튜닝에만 집중할 수 있도록 한다.
  - 리팩토링을 해 놓으면 프로그램이 잘 분해되어 있으므로 퍼포먼스 분석시 좀 더 세밀한 분석이 가능하도록 한다. 
  - 리팩토링을 하는 동안 단기적으로는 소프트웨어를 느리게 하지만, 최적화 단계에서는 소프트웨어를 튜닝하는 것을 더 쉽게 한다.


2. Bad Smells in Code

 2.1 리팩토링이 동작하는 매커니즘을 아는 것 만큼 중요한 것은, 언제 리팩토링을 적용할까 하는 것이다.

 2.2 어떠한 리팩토링을 해야 할 지 확신할 수 없을때 이 부분을 참조하라.
 
  - 중복된 코드 (Duplicated Code)
  - 긴 메소드 (Long Method)
  - 거대한 클래스 (Large Class)
  - 긴 파라미터 리스트 (Long Parameter List)
  - 확산적 변경 (Divergent Change)
  - 산탄총 수술 (Shotgun Surgery)
  - 기능에 대한 욕심 (Feature Envy)
  - 데이터 덩어리 (Data Clumps)
  - 기본타입에 대한 강박관념(Primitive Obsession)
  - Switch Statements
  - 게으른 클래스 (Lazy Class)
  - 추측성 일반화 (Speculative Generality)
  - 임시 필드 (Temporary Field)
  - Message Chains
  - Middle Man
  - 다른 인터페이스를 가진 대체 클래스 (Alternative Classes with Different Interfaces)
  - 불완전한 라이브러리 클래스 (Incomplete Library Class)
  - Data Class
  - 거부된 유산 (Refused Bequest)
  - 주석 (Comments)


3. Refactoring과 Test Code (by Kent Beck)
 3.1 선행조건
  - 리팩토링을 위한 필수적인 선행조건은 견고한 테스트를 하는 것이다.
  - 좋은 테스트는 프로그래밍 속도를 비약적으로 향상시킨다는 것을 발견했다. ( Kent Beck )

 3.2 The Value of Self-testing Code ( Kent Beck )
  - 모든 테스트가 자동화되었는지 확인하고 테스트들의 결과를 테스트 코드 스스로 체크하도록 해라. 
  - test suite는 버그를 찾는 시간을 줄여주는 강력한 버그 탐지기이다. 
  - 테스팅 코드는 Extreme Programming 의 중요한 부분이다.


4. Refactoring 방법
 4-1. 메소드 정리 (Composing Methods)
  - Extract Method
   그룹으로 함께 묶을 수 있는 코드 조각이 있으면
   ☞ 코드의 목적이 잘 드러나도록 메소드의 이름을 지어 별도의 메소드로 뽑아낸다.


      void printOwing( double amount ) { 
   printBanner(); 
   // print details 
   System.out.println( "name:" + _name); 
   System.out.println( "amount" + amount); 
      }
  ☞ 
      void printOwing(double amount){ 
   printBanner(); 
   printDetails( amount ); 
      } 

      void printDetails (double amount){ 
   System.out.println( "name:" + _name); 
   System.out.println( "amount" + amount); 
      }

  - Inline Method
   메소드의 몸체가 메소드의 이름 만큼이나 명확할 때는
   ☞ 호출하는 곳에 메소드의 몸체를 넣고, 메소드를 삭제하라.
 
      int getRating() { 
   return (moreThanFiveLateDeliveries())?2:1; 
      } 
      boolean moreThanFiveLateDeliveries(){ 
   return _numberOfLateDeliveries > 5; 
      } 
  ☞
      int getRating(){ 
   return (_numberOfLateDeliveries>5)?2:1; 
      }

  - Inline Temp
   간단한 수식의 결과값을 가지는 임시변수가 있고, 그 임시변수가 다른 리팩토링을 하는데 방해가 된다면, 
   ☞ 이 임시변수를 참조하는 부분을 모두 원래의 수식으로 바꿔라.

      double basePrice = anOrder.basePrice(); 
      return (basePrice > 1000) 
  ☞
      return (anOrder.BasePrice() > 1000)


  - Replace Temp with Query 
   어떤 수식의 결과값을 저장하기 위해서 임시변수를 사용하고 있다면
   ☞ 수식을 뽑아내서 메소드로 만들고, 임시변수를 참조하는 곳을 찾아 모두 메소드 호출로 바꾼다. 
       새로 만든 메소드는 다른 메소드에서도 사용될 수 있다.

      double basePrice = _quantity * _itemPrice; 
      if ( basePrice > 1000) 
   return basePrice * 0.95; 
      else 
   return basePrice 
  ☞
   if ( basePrice() > 1000) 
       return basePrice() * 0.95; 
   else 
       return basePrice() * 0.98; 
   ...

      double basePrice() { 
   return _quantity * _itemPrice; 
      }


  - Introduce Explaining Variable 
   복잡한 수식이 있는 경우에는
   ☞ 수식의 결과나 또는 수식의 일부에 자신의 목적을 잘 설명하는 이름으로 된 임시변수를 사용하라.  

   if ( (platform.toUpperCase().indexOf("MAC") > -1) && 
    (browser.toUpperCase().indexOf("IE") > -1) && 
    wasInittialized() && resize > 0)  
   { 
    // do something 
   } 
  ☞
   final boolean isMacOs = platform.toUpperCase().indexOf("MAX") > -1; 
   final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1); 
   final boolean wasResized = resize > 0;  

   if ( isMaxOs && isIEBrowser && wasResized ) { 
    // do something 
   }

  - Split Temprorary Variable 
   루프 안에 있는 변수나 collecting temporary variable도 아닌 임시변수에 값을 여러 번 대입하는 경우에는 
   ☞ 각각의 대입에 대해서 따로따로 임시변수를 만들어라.    

   double temp = 2 * (_height + _width); 
   System.out.println (temp); 
   temp = _height * _width; 
   System.out.println(temp); 
  ☞
   final double perimeter = 2 * (_height + width); 
   System.out.println(perimeter); 
   final dougle area = _height * _width; 
   System.out.println(area);


  - Remove Assignments to Parameters
   파라미터에 값을 대입하는 코드가 있으면
   ☞ 대신 임시변수를 사용하도록 하라.  

   int descount ( int inputVal, int Quantity, int yearToDate) { 
    if (inputVal > 50) inputVal -= 2;   
  ☞

   int descount ( int inputVal, int Quantity, int yearToDate) { 
    int result = inputVal; 
    if (inputVal > 50) result -= 2;

  - Replace Method with Method Object 
   긴 메소드가 있는데, 지역변수 때문에 Extract Method를 적용할 수 없는 경우에는
   ☞ 메소드를 그 자신을 위한 객체로 바꿔서 모든 지역변수가 그 객체의 필드가 되도록 한다. 
       이렇게 하면 메소드를 같은 객체 안의 여러 메소드로 분해할 수 있다. 
    
       class Order ... 
    double price() { 
        double primaryBasePrice; 
        double secondaryBasePrice; 
        double teriaryBasePrice; 
        // long computation; 
        ... 
    } 
    ...
   ☞
       class Order ... 
    double primaryBasePrice; 
    double secondaryBasePrice; 
    double teriaryBasePrice;

    double price() { 
        // long computation; 
        ... 
    } 
    ...
  

  - Substititude Algorithm 
   알고리즘을 보다 명확한 것으로 바꾸고 싶을 때는 
   ☞ 메소드의 몸체를 새로운 알고리즘으로 바꾼다.    

   String foundPerson(String[] people) { 
    for (int i = 0; i < people.length; i++) {  
        if (people[i].equals ("Don")) { 
     return "Don"; 
        } 
        if (people[i].equals ("John")){ 
     return "John"; 
        } 
        if (people[i].equals ("Kent")){ 
     return "Kent"; 
        } 
    } 
    return ""; 
       } 
   ☞
       String foundPerson(String[] people) { 
    ListCandidates = Arrays.asList(new String[] {"Don", John", "Kent"}); 
    for (int i = 0; i < people.length; i++) 
        if (candidates.contains(people[i]))
     return people[i]; 
    return ""; 
       }


 4-2. 객체간의 기능 이동 (Moving Features Between Objects)
  - 객체 디자인에서 가장 기본이 되는 것 중의 하나(원칙은 아닐지라도)는 책임을 어디에 둘지를 결정하는 것이다.
  - 종종 이런 문제는 Move Method와 Move Field를 사용해서 동작을 옮김으로써 간단히 해결할 수 있다.

  - Move Method
  - Move Field
  - Extract Class
  - Inline Class
  - Hide Delegate
  - Remove Middle Man
  - Introduce Foreign Method
  - Introduce Local Extension


 4-3. 데이터 구성 (Organizing Data)
  - 종종 간단한 데이터 값으로 시작했다가 나중에 객체가 더 편리할 것이라고 깨닫게 되면 
    멍청한 데이터를 똑똑한 객체로 바꿀 수 있게 한다.

  - Self Encapsulate Field 
   필드에 직접 접근하고 있는데 필드에 대한 결합이 이상해지면
   ☞ 그 필드에 대한 get/set 메소드를 만들고 항상 이 메소드를 사용하여 필드에 접근하라.   

    private int _low, _high; 
        boolean includes (int arg){ 
     return arg >= _low && arg <= _high; 
        } 
   ☞
        private int _low, _high; 
        boolean includes (int arg){ 
     return arg >= getLow() && arg <= getHigh(); 
        } 
        int getLow() {return _low;} 
        int getHigh() {return _high;}


  - Replace Array with Object 
   배열의 특정 요소가 다른 뜻을 가지고 있다면
   ☞ 배열을 각각의 요소에 대한 필드를 가지는 객체로 바꿔라.   

    String[] row = new String[3]; 
    row [0] = "Liverpool"; 
    row [1] = "15"; 
   ☞
    Performance row = new Performance(); 
    row.setName("Liverpool"); 
    row.setWins("15");


  - Replace Magic Number with Symbolic Constant 
   특별한 의미를 가지는 숫자 리터럴이 있으면
   ☞ 상수를 만들고, 의미를 잘 나타내도록 이름을 지은다음, 숫자를 상수로 바꾸어라.    

    double potentialEnergy(double mass, double height){ 
     return mass * 9.91 * height; 
        } 
   ☞

        double potentialEnergy(double mass, double height){ 
     return mass * GRAVITATION_CONSTNAT * height; 
        } 
        static final double GRAVITATIONAL_CONSTANT = 9,81;

   

  - Encapsulate Field 
   public 필드가 있는 경우
   ☞ 그 필드를 private으로 만들고, 접근자를 제공하라.    

    public String _name; 
   ☞
    private String _name; 
    public String getName() {return _name;} 
    public void setName(String arg) { _name = arg;} 

  - Replace Data Value with Object
  - Change Value to Reference  
  - Change Reference to Value 
  - Duplicate Observed Data 
  - Change Unidirectional Association to Bidirectional 
  - Change Bidirectional Association to Unidirectional 
  - Encapsulate Collection 
  - Replace Record with Data Class 
  - Replace Type Code with Class 
  - Replace Type Code with Subclasses 
  - Replace Type code with State/Strategy 
  - Replace Subclass with Fields


 4-4. 조건문의 단순화 (Simplifying Conditional Expressions)

  - Decompose Conditional
   복잡한 조건문(if-then-else)이 있는 경우
   ☞ 조건, then 부분, 그리고 else 부분에서 메소드를 추출하라.    

    if (data.before( SUMMER_START ) || data.after(SUMMER_END)) 
     charge = quantity * _winterRate + _winterServeceCharge; 
    else charge = quantity * _summerRate; 
   ☞
    if (notSummer(date)) 
     charge = winterCharge(quantity); 
    else charge = summerCharge(quatity); 
  

  - Consolidate Conditional Expression
   같은 결과를 초래하는 일련의 조건 테스트가 있는 경우 
   ☞ 그것을 하나의 조건 식으로 결합하여 뽑아내라.  

   double disabilityAmount() { 
    if (_seniority < 2) return 0; 
    if ( _monthsDisabled > 12) return 0; 
    if ( _isPartTime) return 0; 
    // compute the disability amount 
   ☞
       double disabilityAmount() { 
    if( isNotEligableForDisability()) return 0; 
    // compute the disability amount; 
   

  - Consolidate Duplicate Conditional Fragments
   동일한 코드 조각이 조건문의 모든 분기 안에 있는 경우
   ☞ 동일한 코드를 조건문 밖으로 옮겨라.   

    if (isSpecialDeal()){ 
        total = price * 0.95; 
        send(); 
    } 
    else { 
        total = price * 0.98; 
        send(); 
    } 
   ☞
    if (isSpecialDeal()) 
        total = price * 0.95 
    else      
        total = price * 0.98; 
    send();

  

  - Replace Nested Conditional with Guard Clauses
   메소드가 정상적인 실행 경로를 불명확하게 하는 조건 동작을 가지고 있는 경우
   ☞ 모든 특별한 경우에 대해서 보호절(guard clause)을 사용하라.  

    double getPayAmount(){ 
        double result; 
        if( _isDead) result = deadAmount(); 
        else { 
     if (_isSeparated) result = separatedAmount(); 
     else { 
         if (_isRetried) result = retiredAmount(); 
         else result = normalPayAmount(); 
     }; 
        } 
        return result; 
    }
   ☞
    double getPayAmount() { 
        if (_isDead) return deadAmount(); 
        if (_isSeparated) return separatedAmount(); 
        if (_isRetried) return retiredAmount(); 
        return normalPayAmount(); 
    }
  

  - Replace Conditional with Polymorphism
   객체의 타입에 따라 다른 동작을 선택하는 조건문을 가지고 있는 경우
   ☞ 조건문의 각 부분을 서브클래스에 있는 오버라이딩 메소드로 옮겨라. 
       그리고 원래 메소드를 abstract로 만들어라.    

   double getSpeed() { 
    switch (_type) {
        case EUROPEAN: 
     return getBaseSpeed(); 
        case AFRICAN: 
     return getBaseSpeed() - getLoadFactor() * _numberofCoconuts; 
        case NORWEGIAN_BLUE: 
     return (_isNailed) ? 0 : getBaseSpeed(_voltage); 
    } 
    throw new RuntimeException ("Should be unreachable"); 
       } 
   ☞  

  - Introduce Null Object
   null 체크를 반복적으로 하고 있다면
   ☞ null 값을 null 객체로 대체하라.   

   if (customer == null) plan = BillingPlan.basic(); 
       else plan = customer.getPlan(); 
   ☞


  - Introduce Assertion
   코드의 한 부분이 프로그램의 상태에 대하여 어떤 것을 가정하고 있으며
   ☞ assertion을 써서 가정을 명시되게(explicit) 만들어라.   

    double getExpenseLimit() { 
     //should have eigher expense limit or a primary project 
     return (_expenseLimit != NULL_EXPENSE)? 
      _expenseLimit: 
      _primaryProject.getMemberExpenseLimit(); 
    }     
   ☞
    double getExpenseLimit() { 
     Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null); 
     return (_expenseLimit != NULL_EXPENSE)? 
     _expenseLimit: 
     _primaryProject.getMemberExpenseLimit(); 
    }
 

 4-5. 메소드 호출의 단순화 (Making Method Calls Simpler)

  - Replace Paramter with Explicit Method
   파라미터의 값에 따라서 다른 코드를 실행하는 메소드가 있다면
   ☞ 각각의 파라미터 값에 대한 별도의 메소드를 만들어라. 

    void setValue (String name, int value) { 
     if (name.equals("height")) 
      _height = value; 
     if (name.equals("width")) 
      _width = value; 
     Assert.shouldNeverReachHere(); 
    } 
   ☞
    void setHeight (int arg) { 
     _height = arg; 
    } 
    void setWidth (int arg) { 
     _width = arg; 
    }

  - Preserve Whole Object
   어떤 객체에서 여러 개의 값을 얻은 다음 메소드를 호출하면서 파라미터로 넘기고 있다면
   ☞ 대신 그 객체를 파라미터로 넘겨라.    

    int low = daysTempRange().getLow(); 
    int high = days.TempRange().getHight(); 
    withinPlan = plan.withinRange (low, high); 
   ☞
    withinPlan = plan.withinRange (daysTempRange()); 
 

  - Replace Parameter with Method
   객체가 메소드를 호출한 다음, 결과를 다른 메소드에 대한 파라미터로 넘기고 있다. 수신자 또한 이 메소드를 호출할 수 있다면 
   ☞ 그 파라미터를 제거하고 수신자가 그 메소드를 호출하도록 하라. 

    int basePrice = _quantity * _itemPrice; 
    discountLevel = getDiscountLevel (); 
    double finalPrice = discountedPrice (basePrice, discountLevel); 
   ☞
    int basePrice = _quantity * _itemPrice; 
    double finalPrice = discountedPrice (basePrice); 
    
  - Replace Constructor with Factory Method
   객체를 생성할 때 단순히 생성하는 것 이외에 다른 작업도 하고 있다면
   ☞ 생성자를 팩토리 메소드로 대체하라.    

    Emplyee (int type) { 
     _type = type; 
    } 
   ☞
    static Emplyee create (int type) { 
     return new Emplyee (type); 
    } 
  

  - Encapsulate Downcast
   메소드가 그 호출부에서 다운캐스트 될 필요가 있는 객체를 리턴하고 있다면
   ☞ 다운캐스트 하는 것을 메소드 안으로 옮겨라.   

    Object lastReading () { 
     return readings.lastElement (); 
    } 
   ☞
    Reading lastReading () { 
     return (Reading) readings.lastElement (); 
    }

 

  - Replace Error Code with Exception
  메소드가 에러를 나타내는 특별한 코드를 가지고 있다면
  ☞ 대신 예외를 던져라.

   int withdraw(int amount) {
    if (amount > _balance)
     return ?1;
    else {
     _balance -= amount;
     return 0;
    }
   }
  ☞
   void withdraw(int amount) throws BalanceException {
    if(amount > balance) throw new BalanceException();
        _balance -= amount;
   }

  - Replace Exception with Test
   호출부에서 먼저 검사할 수 있는 조건에 대해 예외를 던지고 있다면
   ☞ 호출부가 먼저 검사하도록 바꿔라.

    double getValueForPeriod(int periodNumber) {
     try{
      return _values[periodNumber];
     }
     catch(ArrayIndexOutOfBoundsException e) {
      return 0;
     }
    }
   ☞
    double getValueForPeriod(int periodNumber) {
     if (periodNumber >= _values.length) return 0;
     return _values[periodNumber];
    }


  - Introduce Parameter Object
  - Rename Method
  - Add Parameter
  - Remove Parameter 
  - Separate Query from Modifier
  - Parameterize Method
  - Remove Setting Method
  - Hide Method


 4-6. 일반화 다루기(Dealing With Generalization)
 
  4.6.1 정의 
   - 일반화(generalization)는 하나의 리팩토링 군을 만드는데, 주로 상속 구조에서 메소드를 옮기는 것을 다룬다.

  4.6.2 일반화 정리

   - Pull Up Constructor Body
    서브클래스들이 대부분 동일한 몸체를 가진 생성자를 가지고 있다면
    ☞ 수퍼클래스에 생성자를 만들고 서브클래스 메소드에서 이것을 호출하라. 

     class Manager extends Employee... 
            public Manager (String name, String id, int grade) { 
             _name = name; 
             _id = id; 
             _grade = grade; 
            } 
    ☞
     public Manager (String name, String id, int grade) { 
         super (name, id); 
        _grade = grade; 
     } 
    
   - Pull Up Field
   - Pull Up Method
   - Push Down Method
   - Push Down Field
   - Extract Subclass
   - Extract Superclass
   - Extract Interface
   - Collapse Hierarchy
   - Form Template Method
   - Replace Inheritance with Delegation
   - Replace Delegation with Inheritance


 4-7. 대규모 리팩토링 (Big Refactorings)

  - Tease Apart Inheritance
  - Convert Procedural Design to Objects
  - Separate Domain from Presentation
  - Extract Hierarchy


5. 결론 - 하나로 합치기 by Kent Beck
 - 목표를 잡는데 익숙하라.
 - 확실하지 않을 때는 멈추어라.
 - 왔던 길로 되돌아가기. (Backtrack)
 - 듀엣 (Duets)

 


1. 다음코드의 문제점은 ???

 for (int index = 0; index < list.size(); index++) {
  list.remove(index);
 }

 ☞

 for (int index = list.size() - 1; index >= 0; index--) {
  list.remove(index);
 }

 ☞

 list = new ArrayList();