[리팩터링 2판] 06. 기본적인 리팩터링

6장부터 모든 챕터는 배경, 절차, 예시 순으로 이루어져있다.
예시로 요약할 내용이 빈약하거나 없는 경우는 생략한다.
총 11개의 리팩터링 기법을 소개한다.

javascript에서는 중첩함수를 지원하는데, java에서는 지원하지 않는다. 중첩합수를 지원하는 언어에서는 추출한 함수가 상위 함수에 정의된 모든 변수에 접근할 수 있다.

function printOwing(invoice) {
    printDetails();
    
    function printDetails() {
        console.log('고객명: ${invoice.customer}')
    }
}

위 예시와 같이 invoice를 printDetails의 파라미터로 받지 않아도 invoice 변수에 접근할 수 있다.


6.0. Impact Sentences

최적화를 할 때는 다음 두 규칙을 따르기 바란다.
첫 번째, 하지마라.
두 번째, 아직 하지 마라.

본질은 같고 부가 정보만 덧붙이는 변환 함수의 이름을 enrich라 하고, 형태가 변할때만 transform이라는 이름을 쓴다.


6.1. 함수 추출하기

배경

  • 목적과 구현을 분리하는 방식이 가장 합리적인 기준으로 보인다.
    코드를 보고 무슨일을 하는지 파악하는데 한참이 걸린다면 그 부분을 함수로 추출한 뒤 ‘무슨 일’에 걸맞는 이름을 짓는다.

  • 단 한줄짜리 함수를 만드는 일도 적지 않았다.

    예를 들어 highlight() 메서드가 reverse()라는 메서드만 호출할지라도,
    코드의 목적(강조)과 구현(반전) 사이의 차이가 그만큼 크기 때문에 괜찮다.

절차

  • 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙니다.
  • 추출한 코드를 원본 함수에서 복사하여 새 함수에 붙여넣는다.
  • 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사한다. 있다면 매개변수로 전달한다.
    • 추출한 코드 안에서 값이 바뀌는 변수 중에서 값으로 전달되는 것들은 주의해서 처리한다.
  • 변수를 다 커리했다면 컴파일한다.
  • 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꾼다.
  • 테스트한다.
  • 다른 코드에 방금 추출한 것과 똑같거나 비슷한 코드가 없는지 살핀다. 있다면 방금 추출한 새 함수를 호출하도록 바꿀지 검토한다.

예시

  • 유효범위를 벗어나는 변수가 없을 때

    -> 그냥 추출

  • 지역 변수를 사용할 때 (변수를 사용하지만 다른 값을 대입하지는 않을 때)

    -> 지역 변수를 매개변수로 넘긴다.

  • 지역 변수의 값을 변경할 때

    -> 변경되는 변수를 반환한다.

    function calculateOutstanding(invoice){
        let outstanding = 0;
        for (const o of invoice.orders) {
            outstanding += o.amount;
        }
        return outstanding;	// 수정된 값 반환
    }
    
  • 값을 반환할 변수가 여러개라면?

    -> 각각을 반환하는 함수 여러 개로 만든다.


6.2. 함수 인라인하기

배경

  • 때로는 함수 본문이 이름만큼 명확한 경우 또는 함수 본문 코드를 이름만큼 깔끔하게 리팩터링할 때도 있다.
  • 그럴 때는 그 함수를 제거한다.

절차

  • 다형 메서드(polymorphic method) 인지 확인한다.
    • 서브클래스에서 오버라이드하는 메서드는 인라인하면 안 된다.
  • 인라인할 함수를 호출하는 곳을 모두 찾는다.
  • 각 호출문을 함수 본문으로 교체한다.
  • 하나씩 교체할 때마다 테스트한다.
    • 여유가 생길 떄마다 틈틈히 처리한다. 한 번에 할 필요는 없다.😀
  • 함수 정의(원래 함수)를 삭제한다.

예시

  • 간단한 경우는 그냥 옮기면 된다.

  • 전달하는 인수 이름과 함수 정의에 쓰인 이름이 다른 경우, 변수 이름을 통일해줘야하는 번거로움이 있다.

    function rating(aDriver) {
        return moreThanFiveDeliveries(aDriver) ? 2 : 1;
    }
      
    function moreThanFiveDeliveries(dvr) {
        return dvr.numberOfLateDeliveries > 5;
    }
    

    -> aDriver 과 dvr로 변수이름이 다르므로 aDriver로 통일해줘야한다.

  • 이보다 일이 더 많은 경우는 한줄한줄 옮겨준다.

    • 핵심은 항상 단계를 잘게 나눠서 처리하는 데 있다.


6.3. 변수 추출하기

배경

  • 변수 추출을 고려한다고 함은 표현식에 이름을 붙이고 싶다는 것이다.
  • 현재 함수 안에서만 의미 있다면 변수로, 더 넓은 문맥에 의미가 된다면 함수로 추출해야한다.

절차

  • 추출하려는 표현식에 부작용은 없는지 확인한다.
  • 불변 변수를 하나 선언하고 이름을 붙일 표현식의 복제본을 대입한다.
  • 원본 표현식을 새로 만든 변수로 교체한다.
  • 테스트한다.
  • 표현식을 여러 곳에서 사용한다면 각각을 새로 만든 변수로 교체한다. 하나 교체할 때마다 테스트한다.


6.4. 변수 인라인하기

배경

  • 변수 이름이 원래 표현식과 다를 바 없을 때는 인라인 하는 것이 좋다.

절차

  • 대입문의 우변(표현식)에서 부작용이 생기지는 않는지 확인한다.
  • 변수가 불변으로 선언되지 않았다면 불변으로 만든 후 테스트한다.
    • 이렇게 하면 변수에 값이 단 한번만 대입되는지 확인할 수 있다.
  • 이 변수를 가장 처음 사용하는 코드를 찾아서 대입문의 우변의 코드로 바꾼다.
  • 테스트한다.
  • 변수를 사용하는 부분을 모두 교체할 때까지 이 과정을 반복한다.
  • 변수 선언문과 대입문을 지운다.
  • 테스트한다.


6.5. 함수 선언 바꾸기

배경

  • 좋은 이름을 떠올리는데 효과적인 방법이 하나 있다. 바로 주석을 이용해 함수의 목적을 설명해보는 것이다. 그러다 보면 주석이 멋진 이름으로 바뀌어 되돌아올 때가 있다.

  • 이 문제의 정답은 바로 정답이 없다는 것이다.

간단한 절차

  • 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인한다.
  • 메서드 선언을 원하는 형태로 바꾼다.
  • 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.
  • 테스트한다.

사실 이건 요즘 웬만한 IDE에서 지원하지 않나 싶다. 😅

마이그레이션 절차

  • 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링한다.
  • 함수 본문을 새로운 함수로 추출한다.
    • 새로 만들 함수 이름이 기존 함수와 같아면 일단 검색하기 쉬운 이름을 임시로 붙여둔다.
  • 추출한 함수에 매개변수를 추가해야 한다면 ‘간단한 절차’를 따라 추가한다.
  • 테스트한다.
  • 기존 함수를 인라인한다.
  • 이름을 임시로 붙여뒀다면 함수 선언 바꾸기를 한 번 더 적용해서 원래 이름으로 되돌린다.
  • 테스트한다.

사실 예전 함수를 deprecated로 지정하고 모든 클라이언트가 새 함수로 이전할 때까지 기다린다.
클라이언트들이 모두 이전했다는 확신이 들면 예전 함수를 지운다.

예시

  • 함수 선언 바꾸기는 공개된 API, 다시 말해 직접 고칠 수 없는 외부 코드가 사용하는 부분을 리팩터링하기에 좋다.


6.6. 변수 캡슐화하기

배경

  • 데이터는 함수보다 다루기가 까다롭다.
  • 그 데이터로의 접근을 독점하는 함수를 만드는 식으로 캡슐화하는 것이 가장 좋은 방법일 때가 많다.
  • 객체의 데이터를 항상 private으로 유지해야 한다.
  • 불변 데이터는 가변 데이터보다 캡슐화할 이유가 적다.

절차

  • 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만든다.
  • 정적 검사를 수행한다.
  • 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 바꾼다. 하나씩 바꿀 때마다 테스트
  • 변수의 접근 범위를 제한한다.
  • 테스트한다.
  • 변수 값이 레코드라면 레코드 캡슐화하기를 적용할지 고려해본다.

예시

  • 게터와 세터를 만든다.
  • 필드 값을 변경하는 일은 제어할 수 없기에, 제어하고 싶은 경우 게터가 데이터의 복제본을 반환하도록 수정하는 식으로 처리한다.

    export function defaultOwner() {return Object.assign({}, defaultOwnerData);}
    


6.7. 변수 이름 바꾸기

배경

  • 명확한 프로그래밍의 핵심은 이름짓기다.

절차

  • 폭넓게 쓰이는 변수라면 변수 캡슐화하기를 고려한다.
  • 이름을 바꿀 변수를 참조하는 곳을 모두 찾아서, 하나씩 변경한다.
  • 테스트한다.


6.8. 매개변수 객체 만들기

배경

  • 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다.
  • 매개변수의 수가 줄어든다.
  • 이런 데이터구조를 활용하도록 프로그램 동작을 재구성하기 때문에 코드를 더 근본적으로 바꿔준다.

절차

  • 적당한 데이터 구조가 아직 마련되어 있지 않다면 새로 만든다.
    • 클래스로 만드는 것을 선호, 동작까지 함께 묶기 좋기 때문에
    • 데이터 구조를 값 객체(Value Object) 로 만든다.
  • 테스트한다.
  • 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  • 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾼다.
  • 다 바꿨다면 기존 매개변수를 제거하고 테스트한다.

예시

  • 범위(range) 라는 개념은 객체 하나로 묶어 표현하는 게 나은 대표적인 예다.
  • 대부분 값 객체를 만들지만, 이 예시에서는 클래스로 묶었다.
  • 이렇게 클래스로 만들어두면 다음에 이 허용범위안에 있는지를 검사하는 매서드를 클래스에 추가할 수 있다.


6.9. 여러 함수를 클래스로 묶기

배경

  • 공통 데이터를 중심으로 긴밀하게 엮어 작동하는 함수 무리는 클래스로 묶자
  • 함수를 한데 묶는 또 다른 방법으로 여러 함수를 변환 함수로 묶기도 있다.

절차

  • 함수들이 공유하는 공통 데이터 레코드를 캡슐화한다.
    • 공통데이터가 레코드 구조가 아니라면 먼저 매개변수 객체 만들기(6.8) 로 데이터를 하나로 묶는다.
  • 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮긴다.
  • 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮긴다.

예시

  • 차(tea) 계량기를 읽어서 측정값을 기록하여 세금을 부과하는 예시를 상상해보자.

  • 최상위 함수로 두면 못보고 지나치기 쉽다는 문제가 있다.

  • 데이터를 클래스로 만들고, 함수를 추가한다.

    class Reading {
        get baseCharge(){
        return baseRate(this.month, this.year) * this.year;
    	}
    }
    
    // 클라이언트
    const basicChargeAmount = aReading.baseCharge;
    
  • 이렇게 이름을 바꾸고 나면 Reading 클래스의 클라이언트는 baseCharge가 필드인지, 계산된 값(함수 호출)인지 구분할 수 없다.
    단일 접근 원칙 (Uniform Access Principle)


6.10. 여러 함수를 변환 함수로 묶기

배경

  • 변환 함수는 원본 데이터를 입력받아서 필요한 정보를 모두 도출한 뒤, 각가을 출력 데이터의 필드에 넣어 반환한다.
  • 이 리팩터링 대신 여러 함수를 클래스로 묶기로 처리해도 된다.
  • 원본 데이터가 코드 안에서 갱신될 때는 클래스로 묶는 편이 훨씬 낫다.
    변환 함수로 묶으면 가공한 데이터를 새로운 레코드에 저장하기 떄문이다.

절차

  • 변환할 레코드를 이력받아서 값을 그대로 반환하는 변환 함수를 만든다.
  • 묶을 함수 중 함수 하나를 골라서 본문 코드를 변환 함수로 옮기고, 처리 결과를 레코드에 새 필드로 기록한다.
    그런 다음 클라이언트 코드가 이 필드를 사용하도록 수정한다.
  • 테스트한다.
  • 나머지 관련 함수도 위 과정에 따라 처리한다.

예시

  • 클라이언트가 데이터를 변경하면 심각한 문제가 생길 경우, 가장 좋은 방법은 이 리팩터링이다.
    ex) 사용량 필드를 변경하면 데이터의 일관성이 깨진다.


6.11. 단계 쪼개기

배경

  • 서로 다른 두 대상을 한꺼번에 다루는 코드를 발견하면 각각을 별개 모듈로 나누는 방법을 모색한다.
  • 가장 대표적인 예는 컴파일러다.
  • 컴파일러의 역사가 오래되다 보니 사람들은 컴파일 작업을 여러 단계가 수나적으로 연결된 형태로 분리하면 좋다는 사실을 깨달았다.

절차

  • 두 번째 단계에 해당하는 코드를 독립 함수로 추출한다.
  • 테스트한다.
  • 중간 데이터 구조를 만들어서 앞에서 추출한 함수의 인수를 추가한다.
  • 테스트한다.
  • 추출한 두 번째 단계 함수의 매개변수를 하나씩 검토한다.
    그중 첫 번째 단계에서 사용되는 것은 중간 데이터 구조로 옮긴다.
    하나씩 옮길 때마다 테스트한다.
  • 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조를 반환하도록 만든다.

예시

  • 단계를 분리하고, 중간 데이터 구조를 만드는 것이 핵심!
  • 최종 결과 상수처리

    const price = applyShipping(priceData, shippingMethod);
    return price;
      
    // 최종 결과 상수처리는 이렇게가 더 깔끔한 방법이다.
    return applyShipping(priceData, shippingMethod);
    
  • 명령줄 호출과 표준 출력에 쓰는 느릭 불편한 작업과 자주 테스트해야 할 복잡한 동작을 분리함으로써 테스트를 더 쉽게 수행하게 만들었다.
    험블 객체 패턴 (Humble Object pattern)

댓글남기기