TIL. 160 리팩토링(refactoring) 기법

조윤식·2022년 9월 19일
0

1. 함수 추출하기 (Extract Function)

코드 조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙인다.

When?

언제 독립된 함수로 묶어야 할지에 관한 의견이 분분함

  • 길이 기준
  • 재사용성 기준
  • 코드가 무슨 일을 하는지 파악하는데 시간이 걸린다면, 그 부분을 함수로 추출한 뒤 '무슨 일'에 걸맞는 이름을 짓자. 목적과 구현을 분리

Why?

함수의 목적이 눈에 확 들어오고, 본문 코드(목적을 이루기 위해 구체적으로 수행하는 일)에 대해서는 더 이상 신경 쓸 필요가 없다.
함수명으로 함수의 목적을 나타내고, 그 목적을 이루기 위한 구현코드를 분리한다.

function printOwing(invoice) {
        printBanner();
        let outstanding = calculateOutstanding();

        // 세부 사항 출력
        console.log(`고객명 : ${invoice.customer}`);
        console.log(`채무액 : ${outstanding}`);
}
function printOwing(invoice) {
        printBanner();
        let outstanding = calculateOutstanding();
        printDetails(outstanding);

        function printDetails(outstanding){
            console.log(`고객명 : ${invoice.customer}`);
            console.log(`채무액 : ${outstanding}`);
        }
}
  1. 함수 인라인하기 (Inline Function)

함수 본문이 이름만큼 명확한 경우 또는 함수 본문 코드를 이름만큼 깔끔하게 리팩터링가능하다면 함수를 제거하고 인라인으로 기술한다.

함수 추출하기와 반대되는 개념
잘못 추출한 함수들을 다시 인라인한다.(과도한 함수추출) 유용한 것만 남기고 나머지는 제거해야 한다.

// 등급
function getRating(driver) {
        return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
****
function moreThanFiveLateDeliveries(driver) {
        return driver.numberOfLateDeliveries > 5;
}
function getRating(driver) {
    return (driver.numberOfLateDeliveries > 5) ? 2 : 1;
}
  1. 변수 추출하기(Extract Variable)

표현식이 너무 복잡하여 이해하기 어려울 경우, 지역 변수를 활용하여 표현식을 쪼개 관리

로직을 구성하는 단계마다 이름을 붙여 목적을 명확하게 드러낼 수 있다.

// 가격(price) = 기본 가격 - 수량 할인 + 배송비
return order.quantity * order.itemPrice - 
    Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
    Math.min(order.quantity * order.itemPrice * 0.1, 100);
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;

4. 변수 인라인하기(Inline Variable)

변수 이름이 원래 표현식과 다를 바가 없을 때

let basePrice = anOrder.basePrice;
return (basePrice > 1000);
return anOrder.basePrice > 1000;

5. 함수 선언 바꾸기(Change Function Declaration) - (함수 이름 바꾸기, 시그니처 바꾸기)

함수는 잘 정의하면 새로운 부분을 추가하기가 쉬워지지만, 잘못 정의하면 지속적인 방해요인이 될 수 있다. 함수의 존재가 방해요인이 될때 수정을 생각해봐야 한다.

// 함수 이름이 너무 축약되어 있음
function circum(radius) {...}
function circumference(radius) {...}

1. 함수의 이름

  • 구현 코드를 살펴볼 필요 없이 호출문만 보고 무슨 일을 하는지 파악할 수 있게 이름을 짓자.
  • 고작 이름일 뿐이지 않은가? 라는 악마의 속삭임에 빠지지말자.

2. 함수의 매개변수

  • 매개변수를 올바르게 선택하는 것은 어렵다. → 상황에 맞게 코드를 개선할 수 있도록 친숙해져야 한다. (정답이 없음)

지불 기한(30일 기준)이 넘었는지 판단하는 함수 존재할 때 매개변수를 어떤것으로 설정할 것인가?

  • 지불 객체 - 지불 객체 인터페이스와 결합됨. 대신 지불이 제공하는 여러 속성에 쉽게 접근 가능하여 내부로직이 복잡해지더라도 함수를 호출하는 코드를 일일이 찾아서 변경하지 않아도됨
  • 마감일 - 다른 모듈과 결합을 제거. 지불객체를 모르는 모듈에서도 사용이 가능하며, 함수의 활용 범위를 넓힐 수 있음
private boolean 지불기한이지났는지체크(지불객체) {
        return baseDate + 30 < 지불객체.마감일;
}

private boolean 지불기한이지났는지체크(마감일) {
        return baseDate + 30 < 마감일;
}

// 로직이 복잡해진다면?
private boolean 지불기한이지났는지체크(지불객체) {
        if(지불객체.유예기간연장여부) ...
        ...
        ...
        return baseDate + 30 < 지불객체.마감일;
}
// 메서드 시그니처가 변경됨
private boolean 지불기한이지났는지체크(마감일, 유예기간, ...) {
        ....
        return baseDate + 30 < 마감일;
}

6. 변수 캡슐화하기(Encapsulate Variable)

데이터의 사용 범위가 넓을수록 절절히 캡슐화하자

let defaultOwner = {firstName: "마틴", lastName: "파울러"};
let defaultOwner = {firstName: "마틴", lastName: "파울러"};
export function defaultOwner() {return defaultOwner;} // 접근
export function setDefaultOwner(arg) {defaultOwnerData = arg;} // 갱신
  • 데이터 변경 전 검증이나 변경 후 추가로직을 쉽게 끼어넣을 수 있음
  • 코드의 결합도를 낮출 수 있음(내부구현의 변화가 발생하더라도 협력하는 외부 객체에 변화의 영향이 퍼져나가지 않도록 막기 위함)
  • 가시범위를 제한하여 내부 정보 직접 접근/조작을 막을 수 있음

7. 변수 이름 바꾸기

명확한 프로그램의 핵심은 이름짓기!

let a = height * width;

let area = height * width;

한 줄짜리 담다식에서 사용하는 변수는 맥락으로부터 변수의 목적을 명확히 알 수 있어 한 글자로 된 이름을 짓기도 함

8. 매개변수 객체 만들기

데이터 항목 여러 개가 공통적으로 매개변수로 사용될 경우 데이터 무리를 데이터 구조 하나로 모아주자.

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

// 범위(range)라는 개념은 객체 하나로 묶어 표현하는 게 나은 대표적인 예
function amountInvoiced(aDateRange) {...}
function amountReceived(aDateRange) {...}
function amountOverdue(aDateRange) {...}

why?

  1. 데이터 사이의 관계가 명확해짐
  2. 매개변수 수가 줄어듬
  3. 일관성이 높아짐(같은 데이터 구조를 사용하는 함수가 원소를 참조할 때 항상 똑같은 이름을 사용하기 때문)

데이터 구조에 담길 데이터에 공통으로 적용되는 동작을 추출해서 함수로 사용한다면 문제를 훨씬 간결하게 표현할 수 있다.

// 측정
function readingsOutsideRange(station, range) {
        return station.readings
                .filter(r => r.temp < range.min || r.temp > range.max);
}

function readingsOutsideRange(station, range) {
        return station.readings
                .filter(r => !range.contains(r.temp));
}

// NumberRange
contains(args) { return (arg >= this.min && arg <= this.max);}

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

공통 데이터를 중심으로 긴밀하게 엮여 작동하는 함수 무리는 하나의 클래스로 묶자

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

클래스 : 데이터와 함수를 하나의 공유 환경으로 묶은 후, 다른 프로그램 요소와 어우러질 수 있도록 그중 일부를 외부에 제공한다. (캡슐화)

why?

  • 함수들이 공유하는 공통 환경을 더 명확하게 표현 가능 - 최상위 함수로 두면 못 보고 지나치기 쉽다
  • 각 함수에 전달되는 인수를 줄여 객체 안에서의 함수 호출을 간결하게 만들 수 있음
// 측정값 기록
reading = {custmer: "ivan", quantity: 10, month: 5, year: 2017};

// client1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity; // 기본요금

// client2
const aReading = acquireReading();
const base = baseRate(aReading.month, aReading.year) * aReading.quantity;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year)); // 면세

// client3
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);

function calculateBaseCharge(aReading) {
     return baseRate(aReading.month, aReading.year) * aReading.quantity;
}

// 함수들이 공유하는 공통 데이터를 캡슐화하고 메소드를 추가하자.
class Reading {
      constructor(data) {
          this._customer = data.customer;
          this._quantity = data.quantity;
          this._month = data.month;
          this._year = data.year;
      }

      get customer() {return this._customer;}
      get quantity() {return this._quantity;}
      get month()    {return this._month;}
      get year()     {return this._year;}

        // 함수들을 데이터 처리 코드 가까이에 둔다.
      get baseCharge() {
          return  baseRate(this.month, this.year) * this.quantity;
      }

      get taxableCharge() {
          return Math.max(0, this.baseCharge - taxThreshold(reading.year));
      }
}

// client1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.baseCharge;

파생 데이터 모두 필요한 시점에 계산되게 만들었으니 저장된 데이터를 갱신하더라도 문제가 생길 일이 없다.

⇒ 가변적인 데이터를 사용할때 (데이터 갱신 가능성이 있을 경우) 클래스로 묶어두면 큰 도움이된다.

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

데이터를 입력받아서 여러 가지 정보를 도출하는 로직이 반복되는 경우 고려해보자

변환함수?

  • 원본 데이터를 입력받아서 필요한 정보를 모두 구한 후, 각각을 출력 데이터의 필드에 넣어 반환
function base(aReading) {...}
function taxableCharge(aReading) {...}
function enrichReading(argReading) {
        const aReading = _.cloentDeep(argReading);
        aReading.baseCharge = base(aReading);
        aReading.taxableCharge = taxableCharge(aReading);
        return aReading;
}

갱신을 일관된 장소에서 처리할 수 있고 로직 중복도 막을 수 있다.

차이점?

rawReading = {custmer: "ivan", quantity: 10, month: 5, year: 2017};

// 변환함수로 묶기 - 원본 데이터가 갱신되는 경우 사용 x, 일관성이 깨질 수 있음 // 불변 또는 읽기전용 문맥
aReading = enrichReading(rawReading);
...
aReading.quantity = aReading.quantity + 1;
...
pay(aReading.baseCharge); // quantity 11 ?

// 클래스로 묶기 - 원본 데이터가 갱신되는 경우 사용 o
aReading = new Reading(rawReading);
...
aReading.quantity = aReading.quantity + 1;
....
pay(aReading.baseCharge);
  1. 단계 쪼개기

서로 다른 두 대상을 한꺼번에 다루는 코드를 발견하면, 각각 별개 모듈로 나누는 방법을 모색하자.

orderData = "필통-0 2"
priceList = ["1000", "2000", ...]

const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord, priceList);

function parseOrder(aString) {
        const values = aString.split(/\s+/);
        return ({
                productID: values[0].split("-")[1],
                quantity: parseInt(values[1]) 
        });
}

function price(order, priceList) {
        return order.quantity * priceList[order.productID];
}

코드를 수정해야 할 때 두 대상을 동시에 생각할 필요 없이 하나에만 집중하기 위함
동작을 연이은 단계로 쪼개자
입력이 처리 로직에 적합하지 않은 상태로 들어오는 경우

  1. 입력값을 다루기 편한 형태로 가공
  2. 입력값으로 로직 처리

컴파일러

  1. 텍스트를 토큰화 함
  2. 토큰을 파싱해서 구문 트리 작성
  3. 구문 트리 변환(최적화 등)
  4. 목적 코드 생성

출처: https://cornswrold.tistory.com/550 [평범한개발자노트:티스토리]

profile
Slow and steady wins the race

0개의 댓글