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

안광의·2022년 9월 15일
0
post-thumbnail

시작하며

책에서 소개하는 리팩터링 기법에 대해서 복습 겸 정리하고 있지만 가장 중요한건 어떤 코드가 더 좋은 코드인지 판단하는 능력이다. 가장 처음에 소개하는 '함수 추출하기'와 '함수 인라인하기'처럼 완전히 반대되는 리팩터링 기법들이 존재하고 거창하게 이름을 붙였지만 그동안 별 고민없이 작성했던 코드들이 리팩터링 기법 중에 하나일 수 있다. 로직에 대해서 제대로 이해하기 전에 작성했던 코드들, 처음과 달라지는 기획과 추가되는 기능들 때문에 코드는 계속 바뀌고 수정하기 어려워진다. 그렇기 때문에 현재 상황에 맞는 최선의 상태로 유지하기 위해서 계속 리팩터링 해야하고 팀의 컨벤션과 경험을 바탕으로 누구든 손보기 쉬운 코드를 작성하는 것이 중요하다.



기본적인 리팩터링

함수 추출하기

//리팩터링 전
function printOwing(invoice) {
  let outstanding = 0;
  console.log("***********************");
  console.log("**** Customer Owes ****");
  console.log("***********************");
  // calculate outstanding
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
  // record due date
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.
  //print details
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
//리팩터링 후
function printOwing(invoice) {
  let outstanding = 0;
  printBanner();
  // calculate outstanding
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
  // record due date
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.
  //print details
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}

function printBanner() {
  console.log("***********************");
  console.log("**** Customer Owes ****");
  console.log("***********************");
}

함수 추출하기는 코드 조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙이는 기법이다. 함수가 대여섯줄을 넘어간다면 슬슬 함수를 추출해야 한다는 신호일 수 있다.


함수 인라인하기

//리팩터링 전
export function rating(driver) {
  return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(dvr) {
  return dvr.numberOfLateDeliveries > 5;
}
//리팩터링 후
export function rating(driver) {
  return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}

함수 인라인하기는 함수 추출하기와 반대되는 기법으로 함수 본문이 이름만큼 명확한 경우에 오히려 분리시키는 것이 비효율적이고 쓸데없는 간접호출이 이루어지기 때문에 인라인하는 것이 깔끔하다.


변수 추출하기

//리팩터링 전
export 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)
  );
}
//리팩터링 후
export function price(order) {
  const basePrice = order.quantity * order.itemPrice;
  const discount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
  const shipping = Math.min(basePrice * 0.1, 100);
  return basePrice - discount + shipping;
}

표현식이 너무 복잡해서 이해하기 어려울 때는 지역 변수를 활용하여 이해하기 쉬운 변수 이름을 붙여 쪼개는 방식으로 코드의 목적을 훨씬 명확하게 드러낼 수 있다.


변수 인라인하기

//리팩터링 전
let basePrice = anOrder.basePrice
return (basePrice > 1000);
//리팩터링 후
return anOrder.basePrice > 1000;

변수 이름이 원래 표현식과 다를 바 없을 때 주변 코드를 리팩터링하는데 방해가 되기 때문에 인라인하는 것이 좋다.


함수 선언 바꾸기

//리팩터링 전
export default class Book {
  #reservations;
  constructor() {
    this.#reservations = [];
  }

  addReservation(customer) {
    this.#reservations.push(customer);
  }

  hasReservation(customer) {
    return this.#reservations.some(
      (reservedCustomer) => reservedCustomer.id === customer.id
    );
  }
}
//리팩터링 후
export default class Book {
  #reservations;
  constructor() {
    this.#reservations = [];
  }

  addReservation(customer, isPrioirity = false) {
    this.#reservations.push(customer);
  }

  hasReservation(customer) {
    return this.#reservations.some(
      (reservedCustomer) => reservedCustomer.id === customer.id
    );
  }
}

변수 캡슐화하기

//리팩터링 전
let defaultOwner = { firstName: '마틴', lastName: '파울러' };

export function getDefaultOwner() {
  return defaultOwner;
}
//리팩터링 후
let defaultOwnerData = { firstName: '마틴', lastName: '파울러' };

export function defaultOwner() {
  return defaultOwnerData
}

export function setDefaultOwner(arg) {
  defaultOwnerData = arg;
}

데이터는 참조하는 모든 부분을 한 번에 바꿔야 코드가 제대로 작동하기 때문에 유효범위가 넓어질수록 다루기 어려워진다. 접근할 수 있는 범위가 넓은 데이터를 옮길 때는 먼저 그 데이터로의 접근을 독접하는 함수를 만드는 식으로 캡슐화하는 것이 가장 좋은 방법일 때가 많다.


변수 이름 바꾸기

//리팩터링 전
let a = height * width;

const cpyNm = '애크미 구스베리';

let tpHd = '제목없음';
let result = `<h1>${tpHd}</h1>`;
//리팩터링 후
let area = width * height;

const companyName = '애크미 구스베리';

let title = '제목없음';
let result = `<h1>${title}</h1>`;

명확한 프로그래밍의 핵심은 이름짓기이다. 고민을 충분히 하지 않거나 개발을 더 하다 보니 문제에 대한 이해가 놓아져서, 또는 사용자의 요구가 달라져 프로그램의 목적이 변해 이름을 잘 못 지을 때가 많다. 함수 호출 한 번으로 끝나지 않고 값이 영속되는 필드라면 이름에 더 신경써야 한다.


매개변수 객체 만들기

//리팩터링 전
export function readingsOutsideRange(station, min, max) {
  return station.readings.filter((r) => r.temp < min || r.temp > max);
}

const station = {
  name: 'ZB1',
  readings: [
    { temp: 47, time: '2016-11-10 09:10' },
    { temp: 53, time: '2016-11-10 09:20' },
    { temp: 58, time: '2016-11-10 09:30' },
    { temp: 53, time: '2016-11-10 09:40' },
    { temp: 51, time: '2016-11-10 09:50' },
  ],
};
const operationPlan = {
  temperatureFloor: 51,
  temperatureCeiling: 53,
};

readingsOutsideRange(
  station,
  operationPlan.temperatureFloor,
  operationPlan.temperatureCeiling
);
//리팩터링 후
export function readingsOutsideRange(station, range) {
  return station.readings.filter((r) => !range.contains(r.temp));
}

export class NumberRange {
  #max;
  #min;
  constructor(min, max) {
    this.#min = min;
    this.#max = max;
  }

  get min() {
    return this._min;
  }

  get max() {
    return this._max;
  }

  contains(number) {
    return number >= this.#min && number <= this.#max;
  }
}

const station = {
  name: 'ZB1',
  readings: [
    { temp: 47, time: '2016-11-10 09:10' },
    { temp: 53, time: '2016-11-10 09:20' },
    { temp: 58, time: '2016-11-10 09:30' },
    { temp: 53, time: '2016-11-10 09:40' },
    { temp: 51, time: '2016-11-10 09:50' },
  ],
};

const operationPlan = new NumberRange(51, 53);

readingsOutsideRange(
  station,
  operationPlan
);

데이터 항목 여려 개가 이 함수, 저 함수로 몰려다닌는 경우 데이터 구조 하나로 모아주는 것이 좋다. 클래스를 활용하여 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해지고 매개변수가 줄어든다.


여러 함수를 클래스로 묶기

//리팩터링 전
const reading = { customer: 'ivan', quantity: 10, month: 5, year: 2017 };

export function acquireReading() {
  return reading;
//리팩터링 후
class Reading {
  #customer;
  #quantity;
  #month;
  #year;

  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 baseRate() {
    if (this.#year === 2017 && this.#month === 5) return 0.1;
    return 0.2;
}
  get baseCharge() {
    return this.baseRate * this.quantity;
  }

  get taxThreshold() {
    return 0.1;
  }

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

const reading = new Reading({
  customer: 'ivan',
  quantity: 10,
  month: 5,
  year: 2017
});

export function acquireReading() {
  return reading;
}

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

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

//리팩터링 전
const reading = { customer: 'ivan', quantity: 10, month: 5, year: 2017 };

export function acquireReading() {
  return reading;
}

export function baseRate(month, year) {
  if (year === 2017 && month === 5) return 0.1;
  return 0.2;
}
//리팩터링 후
const reading = { customer: 'ivan', quantity: 10, month: 5, year: 2017 };

export function acquireReading() {
  return reading;
}

export function enrichReading(original) {
  const result = JSON.parse(JSON.stringify(original));
  result.baseCharge = calculateBaseCharge(result);
  result.taxableCharge = Math.max(0, result.baseCharge - 0.1);
  return result
}

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

export function baseRate(month, year) {
  if (year === 2017 && month === 5) return 0.1;
  return 0.2;
}

변환 함수를 사용하여 같은 도출 로직이 반복되는 작업들을 한곳에 모아두면 검색과 갱신을 일관된 곳에서 처리할 수 있고 로직 중복도 막을 수 있다.



마치며

리팩터링 공부를 하면서 놀랍다고 생각한 사실은 일단 성능은 고려하지 않고 코드를 작성해야 한다는 것이다. 처음 프로그래밍 언어에 대해 배우고 코딩 테스트 문제를 풀기 시작했을 때 자바스크립트가 동작하는 방식에 대해 이해하고 효율적인 코드를 짜는 것이 중요하다고 생각했고 목표로 삼아야 한다고 생각했다.

예전에는 맞는 말이었을 수도 있지만 현재는 컴퓨터 성능이 비약적으로 좋아졌고 실무를 하면서 알아보기 쉬운 코드, 수정하기 쉬운 코드가 좋은 코드라는 것을 알게 되었다. 코드는 자주 변경되고(프론트엔드는 특히 더) 다른 사람이 알아보기 어렵게 작성한 코드를 보는 것은 꽤나 힘든 일이기 때문에 성능보다는 읽히는 코드를 작성하는 것이 더 중요하다. 성능에 대한 문제가 발생했을때 그때 수정하면 되는 것이다.

예전에 면접관님에게 이제 막 커리어를 시작하는 개발자에게 효율적인 코드를 작성하는 법에 대해서 조언을 해주실 수 있냐고 물어본 적이 있는데 깔끔하고 알아보기 쉽게 작성한 코드가 성능도 나쁘지 않게 나온다는 얘기를 들었는데 점점 공감이 되는 것 같다.

profile
개발자로 성장하기

0개의 댓글