본문 바로가기
핌팩토링

6. 기본적인 리팩터링

by AsCE_hyunseung 2022. 3. 26.

6. 기본적인 리팩터링

6.1 함수 추출하기

배경

  • 코드를 독립된 함수로 묶을때의 기준은 코드의 목적과 구현을 분리하는 방식이 가장 합리적인 기준으로 보인다.
  • 코드를 보고 무슨 일을 하는지 파악하는데 시간이 걸린다면 그 부분을 함수로 추출한 뒤 ‘무슨 일'에 걸맞는 이름을 짓는다.
  • 이렇게 해두면 나중에 코드를 다시 읽을 때 함수의 목적이 눈에 확 들어오고, 본문 코드에 대해서는 신경 쓸 일이 현저히 줄어들기 때문이다.
  • 짧은 함수의 이점은 이름을 잘 지어야만 발휘되므로 이름 짓기에 신경을 써야한다.

절차

  1. 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙인다(어떻게 X, 무엇을 하는지 O)
    • 함수로 뽑아서 목적이 더 잘 드러나는 이름을 붙일 수 있다면 추출한다.
  2. 추출할 코드를 원본 함수에서 복사하여 새 함수에 붙여넣는다
  3. 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사한다. 있다면 매개변수로 전달한다.
  4. 변수를 다 처리했다면 컴파일한다.
  5. 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꾼다.
  6. 테스트
  7. 다른 코드에 방금 추출한 것과 똑같거나 비슷한 코드가 없는지 살핀다. 있다면 방금 추출한 새 함수를 호출하도록 바꿀지 검토한다.

예시: 지역 변수의 값을 변경할 때

function printOwing(invoice) {
    let outstanding = 0;

    printBanner();
    //미해결 채무(outstanding)를 계산한다.
    for (const o of invoice.orders) {
        outstanding += o.amount;
    }
    recordDueDate(invoice)
    printDetails(invoice, outstanding)
}
  1. 선언문을 변수가 사용되는 코드 근처로 슬라이드한다.
  2. function printOwing(invoice) { printBanner(); let outstanding = 0; // 맨 위에 있던 선언문을 여기로 이동 for (const o of invoice.orders) { outstanding += o.amount; } recordDueDate(invoice) printDetails(invoice, outstanding) }
  3. 추출할 부분을 새로운 함수로 복사한다.
function printOwing(invoice) {
    printBanner();

    let outstanding = 0;

    for (const o of invoice.orders) {
        outstanding += o.amount;
    }
    recordDueDate(invoice)
    printDetails(invoice, outstanding)
}

function calculateOutstanding(invoice) {
    let outstanding = 0; // 추출할 코드 복사
    
    for (const o of invoice.orders) {
        outstanding += o.amount;
    }
    return outstanding // 수정된 값 반환
}
  1. 추출한 코드의 원래 자리를 새로 뽑아낸 함수를 호출하는 문장으로 교체한다.
function printOwing(invoice) {
    printBanner();

    let outstanding = calculateOutstanding(invoice) // 추출한 함수로 교체완료
    recordDueDate(invoice)
    printDetails(invoice, outstanding)
}

function calculateOutstanding(invoice) {
    let outstanding = 0;

    for (const o of invoice.orders) {
        outstanding += o.amount;
    }
    return outstanding 
}
  1. 반환 값의 이름을 내 코딩 스타일에 맞게 바꾼다.
function printOwing(invoice) {
    printBanner();

    let outstanding = calculateOutstanding(invoice)
    recordDueDate(invoice)
    printDetails(invoice, outstanding)
}

function calculateOutstanding(invoice) {
    let result = 0; // 변수 이름 변경

    for (const o of invoice.orders) {
        result += o.amount;
    }
    return result 
}

값을 반환할 변수가 여러 개라면?

  • 함수가 값 하나만 반환하는 방식을 선호하기 때문에 각각을 반환하는 함수 여러 개로 만든다.

6.2 함수 인라인하기

배경

  • 때로는 함수 본문이 이름만큼 명확한 경우도 있다.
  • 함수 본문 코드를 이름만큼 깔끔하게 리팩터링할 때도 있다.
  • 이럴때는 그 함수를 제거한다. 쓸데없는 간접 호출은 거슬릴 뿐이다.
  • 간접 호출을 과하게 쓰는 코드도 흔한 인라인 대상이다.

절차

  1. 다형 메서드인지 확인한다.
    • 서브클래스에서 오버라이드하는 메서드는 인라인하면 안된다.
  2. 인라인할 함수를 호출하는 곳을 모두 찾는다.
  3. 각 호출문을 함수 본문으로 교체한다.
  4. 하나씩 교체할 때마다 테스트한다.
    • 인라인하기가 까다로운 부분이 있다면 일단 남겨두고 여유가 생길 때마다 틈틈이 처리한다
  5. 원본 함수를 삭제한다.

예시

function rating(aDriver) {
    return moreThanFiveLateDeliveries(aDriver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(aDriver) {
    return aDriver.numberOfLateDeliveries > 5;
}
  • 호출되는 함수의 반환문을 그대로 복사해서 호출하는 함수의 호출문을 덮어쓰면 끝이다.
function rating(aDriver) {
    return aDriver.numberOfLateDeliveries > 5 ? 2 : 1;
}
  • 여기서 핵심은 항상 단계를 잘게 나눠서 처리하는 데 있다.
  • 작업을 크게 묶어서 처리할때 테스트가 실패하면 가장 최근의 정상 코드로 돌아온 다음, 단계를 잘게 나눠서 다시 리팩터링한다.

6.3 변수 추출하기

배경

  • 표현식이 너무 복잡해서 이해하기 어려울 때는, 지역 변수를 활용하면 표현식을 쪼개 관리하기 더 쉽게 만들 수 있다.
  • 디버깅할때도 이점이 있는데, 디버깅할때 중단점을 지정하거나 상태를 출렭하는 문장을 추가할 수 있기 떄문이다.

절차

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

예시

function price(order) {
    // 가격 = 기본 가격 - 수량 할인 + 배송비
    return order.quantity * order.itemPrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(order.quantity * order.itemPrice * 0.1, 100)
}
  1. 기본 가격을 담을 변수를 만들고 적절한 이름을 지어주고, 원래 표현식에서 새로 만든 변수에 해당하는 부분을 교체한다.
function price(order) {
	    // 가격 = 기본 가격 - 수량 할인 + 배송비
    const basePrice = order.quantity * order.itemPrice
    return basePrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(order.quantity * order.itemPrice * 0.1, 100)
}
  1. 방금 교체한 표현식이 쓰이는 부분이 더 있다면 마찬가지로 새 변수를 사용하도록 수정한다.
function price(order) {
    // 가격 = 기본 가격 - 수량 할인 + 배송비
    const basePrice = order.quantity * order.itemPrice
    return basePrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(basePrice * 0.1, 100)
}
  1. 수량 할인도 다음과 같이 추출하고 교체한다
function price(order) {
    // 가격 = 기본 가격 - 수량 할인 + 배송비
    const basePrice = order.quantity * order.itemPrice;
    const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
    return basePrice - quantityDiscount +
        Math.min(basePrice * 0.1, 100)
}
  1. 배송비도 똑같이 처리하고 주석을 지운다.
function price(order) {
    const basePrice = order.quantity * order.itemPrice;
    const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
    const shipping = Math.min(basePrice * 0.1, 100);
    return basePrice - quantityDiscount + shipping;
}

예시: 클래스 안에서

class Order {
    constructor(aRecord) {
        this._data = aRecord;
    }

    get quantity() {
        return this._data.quantity;
    }

    get itemPrice() {
        return this._data.itemPrice;
    }

    get Price() {
        return this.quantity * this.itemPrice -
            Math.max(0, this.quantity - 500) * this.itemPrice * 0.05 +
            Math.min(this.quantity * this.itemPrice * 0.1, 100);
    }
}
  • 클래스 전체에 영향을 줄 떄는 변수가 아닌 메서드로 추출한다.
class Order {
    constructor(aRecord) {
        this._data = aRecord;
    }

    get quantity() {
        return this._data.quantity;
    }

    get itemPrice() {
        return this._data.itemPrice;
    }

    get Price() {
        return this.basePrice - this.quantityDiscount + this.shipping
    }

    get basePrice() {
        return this.quantity * this.itemPrice;
    }

    get quantityDiscount() {
        return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05;
    }

    get shipping() {
        return Math.min(this.quantity * this.itemPrice * 0.1, 100);
    }
}
  • 객체는 특정 로직과 데이터를 외부와 공유하려 할 때 공유할 정보를 설명해주는 적당한 크기의 문맥이 되어준다.
  • 덩치가 큰 클래스에서 공통 동작을 별도 이름으로 뽑아내서 추상화해두면 그 객체를 다룰 때 쉽게 활용할 수 있어서 매우 유용하다.

6.4 변수 인라인 하기

배경

  • 변수의 이름이 원래 표현식과 다를 바 없을때나 변수가 주변 코드를 리팩터링하는 데 방해가 되기도 한다. 이럴 때는 그 변수를 인라인한다.

절차

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

예시

let basePrice = anOrder.basePrice;
return (basePrice > 1000)
return anOrder.basePrice > 1000 // 변수 인라인하기

6.5 함수 선언 바꾸기

배경

  • 이름이 좋으면 함수의 구현 코드를 살펴볼 필요 없이 호출문만 보고도 모슨 일을 하는지 파악할 수 있다.
  • 이름이 잘못된 함수를 발견하면 더 나은 이름이 떠오르는 즉시 바꾸라는 명령으로 받아들인다. 그래야 나중에 그 코드를 다시 볼 때 무슨 일을 하는지 또 고민하지 않게 된다.
  • 주석을 이용해 함수의 목적을 설명해보면, 주석이 멋진 이름으로 바뀌어 되돌아올 때가 있다.
  • 동작에 필요한 모듈 수가 줄어들수록 무언가를 수정할 때 머리에 담아둬야 하는 내용도 적어진다.
  • 어떻게 연결하는 것이 더 나은지 더 잘 이해하게 될 떄마다 그에 맞게 코드를 개선할 수 있도록 함수 선언 바꾸기 리팩터링과 친숙해져야한다.

절차

  • 함수 선언 바꾸기는 간단한 절차만으로 충분할 때도 많지만, 더 세분화된 마이그레이션 절차가 훨씬 적합한 경우가 많다.
  • 따라서 이 리팩터링을 할 떄는 먼저 변경 사항을 살펴보고 함수 선언과 호출문들을 단번에 고칠 수 있을지 가늠해본다.
  • 가능하다면 간단한 절차를 따르고, 가능하지 않다면 마이그레이션 절차를 따른다. (마이그레이션 절차를 적용하면 호출문들을 점진적으로 수정할 수 있다.)

간단한 절차

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

마이그레이션 절차

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

예시: 함수 이름 바꾸기(간단한 절차)

function circum(radius) {
    return 2 * Math.PI * radius;
}
  1. 함수 이름을 이해하기 쉽게 바꾼다.
function circumference(radius) { // 함수 이름 변경
    return 2 * Math.PI * radius;
}
  1. circum()을 호출한 곳을 모두 찾아서 circumference()로 바꾼다.
  • 단점은 호출문과 선언문을 한 번에 수정해야 한다는 것이다.

예시: 함수 이름 바꾸기(마이그레이션 절차)

function circum(radius) {
    return 2 * Math.PI * radius;
}
  1. 함수 본문 전체를 새로운 함수로 추출한다.
function circum(radius) {
    return circumference(radius);
}

function circumference(radius) { // 새로운 함수로 추출한다.
    return 2 * Math.PI * radius;
}
  1. 수정한 코드를 테스트한다.
  2. 예전 함수를 인라인한다.
  • 리팩터링 대상은 대부분 직접 수정할 수 있는 코드지만, 함수 선언 바꾸기만큼은 직접 고칠 수 없는 외부 코드가 사용하는 부분(공개된 api)을 리팩터링하기에 좋다.

6.6 변수 캡슐화하기

배경

  • 데이터는 함수보다 다루기 까다롭다
    • 데이터는 참조하는 모든 부분을 한 번에 바꿔야 코드가 제대로 작동하기 때문.
  • 그래서 접근할 수 있는 범위가 넓은 데이터를 옮길 때는 먼저 그 데이터로의 접근을 독점하는 함수를 만드는 식으로 캡슐화하는 것이 좋은 방법일 때가 많다.
    • 데이터 재구성이라는 어려운 작업을 함수 재구성이라는 더 단순한 작업으로 변환하는 것이다.
  • 데이터의 유효범위가 넓을수록 캡슐화해야 한다.
    • 그래야 자주 사용하는 데이터에 대한 결합도가 높아지는 일을 막을 수 있다.
  • 불변성은 강력한 방부제이다.

절차

  1. 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만든다.
  2. 정적 검사를 수행한다.
  3. 변수를 직접 참조하던 부분을 캡슐화 함수 호출로 바꾼다. 하나씩 바꿀 때마다 테스트한다.
  4. 변수의 접근 범위를 제한한다.
    • 변수 이름을 바꿔서 테스트해보면 해당 변수를 참조하는 곳을 쉽게 찾아낼 수 있다.
  5. 테스트
  6. 변수 값이 레코드라면 레코드 캡슐화하기를 적용할지 고려한다.

예시

  1. 기본적인 캡슐화를 위해 가장 먼저 데이터를 읽고 쓰는 함수부터 정의한다.
function getDefaultOwner() { return defaultOwner; }
function setDefaultOwner(arg) {defaultOwner = arg; }
  1. defaultOwner를 참조하는 코드를 찾아서 방금 만든 getter 함수를 호출하도록 고친다. 대입문은 세터 함수로 바꾼다.
spaceship.owner = getDefaultOwner()
setDefaultOwner({firstName: "레베카", lastName: "파슨스"});
  1. 모든 참조를 수정했다면 이제 변수의 가시 범위를 제한한다.
let defaultOwner = {firstName: "마틴", lastName: "파울러"}
function getDefaultOwner() { return defaultOwner; }
function setDefaultOwner(arg) {defaultOwner = arg; }
  • 복제본 만들기와 클래스로 감싸는 방식은 레코드 구조에서 깊이가 1인 속성들까지만 효과가 있다.
  • 더 깊이 들어가면 복제본과 객체 래핑 단계가 더 늘어나게 된다.
  • 데이터의 사용 범위가 넓을수록 적절히 캡슐화하는 게 좋다.

6.7 변수 이름 바꾸기

배경

  • 명확한 프로그래밍의 핵심은 이름짓기다
  • 값이 영속되는 필드라면 이름에 더 신경 써야 한다.

절차

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

예시

const cpyNm = "애크미 구스베리"
  • 먼저 원본의 이름을 바꾼 후, 원본의 원래 이름과 같은 복제본을 만든다.
  • 이제 기존 이름을 참조하는 코드들을 새 이름으로 점진적으로 바꿀 수 있다.
const companyName = "애크미 구스베리";
const cpyNm = companyName

6.8 매개변수 객체 만들기

배경

  • 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다는 이점을 얻는다.
  • 게다가 함수가 이 데이터 구조를 받게 하면 매개변수 수가 줄어든다.
  • 같은 데이터 구조를 사용하는 모든 함수가 원소를 참조할 때 항상 똑같은 이름을 사용하기 때문에 일관성도 높여준다

절차

  1. 적당한 데이터 구조가 아직 마련되어 있지 않다면 새로 만든다.
  2. 테스트한다.
  3. 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가한다.
  4. 테스트한다.
  5. 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  6. 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾼다.

예시

function amountInvoiced(startDate, endDate) {...}
function amountReceived(startDate, endDate) {...}
function amountOverdue(startDate, endDate) {...}
----------------------------------------
function amountInvoiced(aDateRange) {...}
function amountReceived(aDateRange) {...}
function amountOverdue(aDateRange) {...}

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

배경

  • 클래스는 데이터와 함수를 하나의 공유 환경으로 묶은 후, 다른 프로그램 요소와 어우러질 수 있도록 그중 일부를 외부에 제공한다.
  • 클래스로 묶으면 이 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있고, 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있다.

절차

  1. 함수들이 공유하는 공통 데이터 레코드를 캡슐화한다.
    • 공통 데이터가 레코드 구조로 묶여 있지 않다면 먼저 매개변수 객체 만들기로 데이터를 하나로 묶는 레코드를 만든다.
  2. 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮긴다.
    • 공통 레코드의 멤버는 함수 호출문의 인수 목록에서 제거한다.
  3. 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮긴다.

예시

function base(aReading) {}
function taxableCharge(aReading) {}
function calculateBaseCharge(aReading) {}
----------------------------------------
class Reading {
    base() {}
    taxableCharge() {}
    calculateBaseCharge() {}
}

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

배경

  • 변환 함수는 원본 데이터를 입력받아서 필요한 정보를 모두 도출한 뒤, 각각을 출력 데이터의 필드에 넣어 반환한다.
  • 이렇게 해두면 도출 과정을 검토할 일이 생겼을 때 변환 함수만 살펴보면 된다.
  • 여러 함수를 한데 묵는 이유는 도출 로직이 중복되는 것을 피하기 위해서다.

절차

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

예시

function base(aReading) {}
function taxableCharge(aReading) {}
----------------------------------------
function enrichReading(argReading) {
    const aReading =_.cloneDeep(argReading);
    aReading.baseCharge = base(aReading);
    aReading.taxableCharge = taxableCharge(aReading);
    return aReading
}

6.11 단계 쪼개기

배경

  • 모듈이 잘 분리되어 있다면 다른 모듈의 상세 내용은 전혀 기억하지 못해도 원하는 대로 수정을 끝마칠 수도 있다.
  • 다른 단계로 볼 수 있는 코드 영역들이 마침 서로 다른 데이터와 함수를 사용한다면 단계 쪼개기에 적합하다는 뜻이다.

절차

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

예시

'핌팩토링' 카테고리의 다른 글

8. 기능 이동  (0) 2022.04.27
7. 캡슐화  (0) 2022.04.27
4. 테스트 구축하기  (0) 2022.03.12
3. 코드에서 나는 악취  (0) 2022.02.28
2. 리팩터링 원칙  (0) 2022.02.13