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

Lee Jeong Min·2022년 8월 23일
0

리팩터링 2판

목록 보기
6/12
post-thumbnail

리팩터링 2판의 Chatper 06를 보고 정리한 글입니다.

가장 기본적이고 많이 사용해서 제일 먼저 배워야 하는 리팩터링으로 시작한다. 리팩터링의 용어와 그 의미가 어떤식으로 코드에서 나타내는지 알아보자.

함수 추출하기

이와 반대되는 리팩터링은 함수 인라인하기(뒤에서 다룸)이다.

배경

함수 추출하기는 코드가 무슨일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙이는 것이다. 코드를 추출하는 기준은 아래와 같이 여러개가 있을 수 있다.

  • 길이
  • 재사용성
  • '목적과 구현을 분리'하는 방식

저자는 '목적과 구현을 분리'하는 방식이 가장 합리적이라고 생각한다.

목적과 구현을 분리한 예

스몰토크의 흑백 시스템에서 텍스트나 그래픽을 강조하기 위해 색상을 반전시키는 메서드의 이름이 highlight()(목적)인데 실제 구현은 reverse()(구현)라는 메서드만을 호출하고 있었음

절차

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

예시

책 안에서 3개의 예시를 다룬다.

  • 유효범위를 벗어나는 변수가 없을 때(굉장히 추출하기 쉬운 경우)
  • 지역 변수를 사용할 때(지역 변수를 매개변수로 전달한다.)
  • 지역 변수의 값을 변경할 때(복잡한 경우이므로 예시를 통해 살펴보자.)

리팩터링 전 예시 코드

function printOwing(invoice) {
  let outstanding = 0;

  printBanner();

  for (const o of invoice.oreders) {
    outstanding += o.amount;
  }

  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}

변수가 사용되는 코드 근처로 슬라이드 시키고, 추출할 부분을 새로운 함수로 복사한뒤, 추출한 코드의 원래 자리를 새로 뽑아낸 함수를 호출하는 문장으로 교체하자.

함수 추출하기 적용 후

function printOwing(invoice) {
  printBanner();
  const outstanding = calculateOutstanding(invoice);
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}

function calculateOutstanding(invoice) {
  let outstanding = 0;
  for (const o of invoice.oreders) {
    outstanding += o.amount;
  }
  return outstanding;
}

저자는 마지막에 calculateOutsatnding 함수안의 outstanding을 result로 반환 값의 이름을 바꿈.

값을 반환할 변수가 여러개인 경우 각각을 반환하는 함수 여러개로 만들거나 임시 변수 추출 작업을 임시 변수를 질의 함수로 바꾸거나 변수를 쪼개는 식으로 처리해보자.

또한 리팩터링 진행 시, 중첩함수로 추출할 수 있더라도 최소한 원본 함수와 같은 수준의 문맥으로 먼저 추출하자.

중첩함수로 추출하게 되면 변수를 처리하기가 까다로울 수 있기 때문!

함수 인라인하기

반대되는 리팩터링: 함수 추출하기

배경

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

절차

  1. 다형 메서드인지 확인한다.
    → 서브클래스에서 오버라이드하는 메서드는 인라인하면 안된다.
  2. 인라인할 함수를 호출하는 곳을 모두 찾는다.
  3. 각 호출문을 함수 본문으로 교체한다.
  4. 하나씩 교체할 때마다 테스트한다.
  5. 함수 정의(원래 함수)를 삭제한다.

예시

리팩터링 하기전 코드

function reportLines(aCustomer) {
  const lines = [];
  gatherCustomerData(lines, aCustomer);
  return lines;
}

function gatherCustomerData(out, aCustomer) {
  out.push(['name', aCustomer.name]);
  out.push(['location', aCustomer.location]);
}

함수 인라인하기 적용 후

function reportLines(aCustomer) {
  const lines = [];
  line.push(['name', aCustomer.name]);
  line.push(['location', aCustomer.location]);
  return lines;
}

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

변수 추출하기

반대되는 리팩터링: 변수 인라인하기

배경

표현식이 복잡한 경우 코드를 이해하기 어려울 때가 있다. 이럴 때 지역 변수를 활용하면 표현식을 쪼개 관리하기 쉽게 만들 수 있다. 변수 추출을 고려한다고 함은 표현식에 이름을 붙이고 싶다는 뜻이다.

절차

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

예시

일반적인 함수와 클래스안에서 같은 코드를 가지고 설명하는데, 클래스 안에 있는 코드로 살펴보자.

리팩터링 하기 전의 코드

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.basePrice * 0.1, 100);
  }
}

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

변수 인라인하기

반대되는 리팩터링: 변수 추출하기

배경

변수는 함수 안에서 표현식을 가리키는 이름으로 쓰여 대체로 긍정적인 효과를 주지만 그 이름이 원래 표현식과 다를 바 없을 때도 있다. 또한 변수가 주변 코드를 리팩터링하는 데 방해가 되기도 한다.
→ 이런 경우 변수를 인라인하는 것이 좋다.

절차

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

예시

따로 책에는 나와있지 않음

함수 선언 바꾸기

배경

함수 선언은 실질적으로 소프트웨어 시스템의 구성 요소를 조립하는 연결부 역할을 한다. 이러한 연결부에서 가장 중요한 요소는 함수의 이름이다.

좋은 이름을 떠올리는데 효과적인 방법으로 주석을 이용해 함수의 목적을 설명해보는 것이다.

함수의 매개변수도 마찬가지로, 함수를 사용하는 문맥을 설정한다. 매개변수를 올바르게 선택하기란 단순히 규칙 몇 개로 표현할 수 없고, 이러한 문제의 정답은 정답이 없기 때문에 어떻게 연결하는 것이 더 나은지 더 잘 이해하게 될 때마다 그에 맞게 코드를 개선할 수 있도록 이 리팩터링과 친숙해져야만 한다.

절차

함수 선언 바꾸기는 '간단한 절차'만으로 충분할 때도 많지만, 더 세분화된 '마이그레이션 절차'가 훨씬 적합한 경우도 많기 때문에 두 가지 절차에 대해서 설명한다.

간단한 절차

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

변경할 게 둘 이상이면(이름 변경과 매개변수 추가) 나눠서 처리하는 편이 나으므로 각각을 독립적으로 처리하자.
→ 이러다가 문제가 생기면 작업을 되돌리고 '마이그레이션 절차'를 따른다.

마이그레이션 절차

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

예시

간단한 절차를 이용한 함수 이름 바꾸기 예시

리팩터링 전의 코드

function circum(radius) {
  return 2 * Math.PI * radius;
}

함수 선언을 수정하고, circum()을 호출한 곳을 모두 찾아서 circumference()로 바꾼다.

이러한 간단한 절차의 단점은 호출문과 선언문을 한 번에 수정해야 한다는 것인데, 코드에 따라 복잡한 상황에 처하게 되면 마이그레이션 절차를 따르는것이 좋다.

함수 이름 바꾸기 적용 후 코드

function circumference(radius) {
  return 2 * Math.PI * radius;
}

마이그레이션 절차를 이용한 함수 이름 바꾸기 예시

리팩터링 전의 코드

function circum(radius) {
  return 2 * Math.PI * radius;
}

먼저 함수 본문 전체를 새로운 함수로 추출하고, 수정한 코드를 테스트한 뒤 예전 함수를 인라인 한다. 하나를 변경할 때마다 테스트하면서 한 번에 하나씩 처리하자.

리팩터링 후의 코드

function circum(radius) {
  return circumference(radius);
}

function circumference(radius) {
  return 2 * Math.PI * radius;
}

매개변수를 속성으로 바꾸는 예시

고객이 뉴잉글랜드에 살고 있는지 확인하는 함수

function inNewEngland(aCustomer) {
  return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(aCustomer.address.state);
}

현재는 위와같이 고객을 매개변수로 받는데, 주 식별 코드를 매개변수로로 받도록 리팩터링하여 고객에 대한 의존성을 제거할 것이다.

// 1. 매개변수로 사용할 코드를 변수로 추출
function inNewEngland(aCustomer) {
  const stateCode = aCustomer.address.state;
  return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(stateCode);
}

// 2. 함수 추출하기로 새 함수를 만든다.
function inNewEngland(aCustomer) {
  const stateCode = aCustomer.address.state;
  return xxNewinEngland(stateCode);
}
function xxNewinEngland(stateCode) {
  return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(stateCode);
}

// 3. 기존 함수 안에 변수로 추출해둔 입력 매개변수를 인라인한다.
function inNewEngland(aCustomer) {
  return xxNewinEngland(aCustomer.address.state);
}

// 4. 함수 인라인하기로 기존 함수의 본문을 호출문들에 집어넣는다.
// 호출문
const newEnglanders = someCustomers.filter((c) => xxNewinEngland(c.address.state));

// 기존 함수를 모든 호출문에 인라인하고, 함수 선언 바꾸기를 다시 한번 적용하여 새 함수의 이름을 기존 함수의 이름으로 바꾼다.
const newEnglanders = someCustomers.filter((c) => inNewEngland(c.address.state));

function inNewEngland(stateCode) {
  return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(stateCode);
}

변수 캡슐화하기

배경

데이터는 참조하는 모든 부분을 한 번에 바꿔야 코드가 제대로 작동한다. 유효범위가 넓어질수록 다루기 어려워지며, 전역 데이터가 골칫거리인 이유도 바로 여기에 있다.

그래서 접근할 수 있는 범위가 넓은 데이터를 옮길 때는 먼저 그 데이터로의 접근을 독접하는 함수를 만드는 식으로 캡슐화 하는것이 가장 좋은 방법일 때가 많다.

절차

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

예시

리팩터링 전의 코드

// 전역 변수에 중요한 데이터가 아래와 같이 존재
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
// 참조하는 코드
spaceship.owner = defaultOwner;
// 갱신하는 코드
defaultOwner = { firstName: '레베카', lastName: '파슨스' };

변수 캡슐화하기 적용후 코드

// 1. 기본적인 캡슐화를 위해 가장 먼저 데이터를 읽고 쓰는 함수부터 정의
function getDefaultOwner() {return defaultOwner;}
function setDefaultOwner(arg) {defaultOwner = arg;}

// 3. defaultOwner를 참조하는 코드를 찾아서 방금 만든 게터 함수를 호출하도록 변경
spaceship.owner = getDefaultOwner();
// 대입문은 세터 함수로 변경
setDefaultOwner({ firstName: '레베카', lastName: '파슨스' });

// 변수의 가시 범위를 제한
// defaultOwner.js 파일
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
export function getDefaultOwner() {return defaultOwner;}
export function setDefaultOwner(arg) {defaultOwner = arg;}

이렇게 까지하면 구조로의 접근이나 구조 자체를 다시 대입하는 행위는 제어할 수 있다. 하지만 필드 값을 변경하는 일은 제어할 수 없다.

따라서 변수에 담긴 내용을 변경하는 행위까지 제어할 수 있게 하는 방법으로 2가지가 있다.

  1. 게터가 데이터의 복제본을 반환하도록 수정하는 방법
export function getDefaultOwner() {return Object.assign({}, defaultOwner);}
  1. 레코드 캡슐화하기
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
export function getDefaultOwner() {return new Person(defaultOwnerData)}
export function setDefaultOwner(arg) {defaultOwner = arg;}

class Person {
  constructor(data) {
    this._lastName = data.lastName;
    this._firstName = data.firstName;
  }

  get lastName() { return this._lastName;}
  get firstName() { return this._firstName;}
}

변수 이름 바꾸기

배경

명확한 프로그래밍의 핵심은 이름짓기다. 이름의 중요성은 사용 범위에 영향을 받는데, 맥락으로부터 변수의 목적을 명확히 알 수 있게 하는 것이 중요하다.

저자는 JS와 같은 동적 타입 언어라면 이름 앞에 타입을 드러내는 문자를 붙이는 스타일을 선호한다고 한다. (ex: aCustomer)

절차

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

예시

리팩터링 하기 전의 코드

let tpHd = 'untitled';
result += `<h1>${tpHd}</h1>`;
tpHd = obj.articleTitle;

리팩터링 이후

// 1. 변수 캡슐화하기로 처리
result += `<h1>${tpHd}</h1>`;

setTitle(obj.articleTitle);

function title() {
  return tpHd;
}
function setTitle(arg) {
  tpHd = arg;
}

상수 이름 바꾸기 코드 예시

상수의 이름은 캡슐화하지 않고도 복제방식으로 점진적으로 바꿀 수 있다.

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

// 원본의 이름을 바꾼 후, 원본의 원래 이름과 같은 복제본을 만든다.
const companyName = "애크미 구스베리";
const cpyNm = companyName;

점진적으로 다 바꾼후, 복제본을 삭제한다.

매개변수 객체 만들기

배경

함수에 전달되는 매개변수가 여러개 일때, 이러한 매개변수 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다는 이점을 얻는다.

이러한 리팩터링은 코드를 더 근본적으로 바꿔준다는 데 있다. 데이터 구조가 문제 영역을 훨씬 간결하게 표현하는 새로운 추상개념으로 격상되면서, 코드의 개념적인 그림을 다시 그릴 수도 있다.

절차

  1. 적당한 데이터 구조가 아직 마련되어 있지 않다면 새로 만든다.
    → 저자는 클래스를 만드는 걸 선호하는데, 나중에 동작까지 함께 묶기 좋기 때문이다.
  2. 테스트한다.
  3. 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가한다.
  4. 테스트한다.
  5. 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  6. 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾼다.
  7. 다 바꿨다면 기존 매개변수를 제거하고 테스트한다.

예시

리팩터링 전의 코드

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' },
  ],
};

// 정상범위를 벗어난 측정값을 찾는 함수
function readingsOutsideRange(station, min, max) {
  return station.readings.filter((r) => r.temp < min || r.temp > max);
}

// 호출하는 코드
alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);

이와 같이 범위의 시작과 끝을 호출하는 코드, 즉 범위라는 개념은 객체 하나로 묶어 표현하는게 나은 대표적인 예다.

매개변수 객체 만들기 리팩터링 이후 코드

// 1. 묶은 데이터를 표현하는 클래스를 선언
class NumberRange {
  constructor(min, max) {
    this._data = { min, max };
  }

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

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

// 3. 새로 만든 객체를 ReadingsOutsideRange()의 매개변수로 추가
function readingsOutsideRange(station, min, max, range) {
  return station.readings.filter((r) => r.temp < min || r.temp > max);
}

// 호출문에 null을 적어둠(JS는 그냥 냅둬도 됨)
alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling, null);

// 4. 아직까지 동작은 하나도 바꾸지 않아서 테스트는 통과

// 5. 온도 범위를 객체 형태로 전달하도록 호출문을 하나씩 바꿈
const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling, range);


// 6. 기존 매개변수를 사용하는 부분을 변경
function readingsOutsideRange(station, range) {
  return station.readings.filter((r) => r.temp < range.min || r.temp > range.max);
}
alerts = readingsOutsideRange(station, range);

여러 함수를 클래스로 묶기

배경

클래스로 묶으면 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있고, 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있다.

클래스의 두드러진 장점은 클라이언트가 객체의 핵심 데이터를 변경할 수 있고, 파생 객체들을 일관되게 관리할 수 있다는 것

절차

  1. 함수들이 공유하는 공통 데이터 레코드를 캡슐화한다.
  2. 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮긴다
  3. 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮긴다.

예시

사람들은 매달 차 계량기를 읽어서 측정값을 다음과 같이 기록한다고 하자.

reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};

// 기본요금 계산하는 코드
// 클라이언트 1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;

// 클라이언트 2
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

// 클라이언트 3
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);

function calculateBaseCharge(aReading) { // 기본 요금 계산 함수
  return baseRate(aReading.month, aReading.year);
}

각 클라이언트마다 공통되는 로직이 보이는 경우 기본 요금 계산 함수를 잘 사용할 수 있도록 데이터를 클래스로 만들어 보자.

리팩터링 후의 코드

// 1. 레코드 캡슐화
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;
  }
}


// 2. 이미 만들어져 있는 calculateBaseCharge() 옮기기 이 과정에서 메서드 이름을 바꿔도 됨
// Reading 클래스안의 메서드
get baseCharge() { // 기본 요금 계산 함수
  return baseRate(this.month, this.year) * this.quantity;
}

// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.baseCharge;

// 클라이언트 1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;

// 클라이언트 2
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

// 클라이언트 1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseCharge;

// 클라이언트 2
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
// 변수 인라인화 하기
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));

// 3. 세금을 부과할 소비량을 계산하는 코드를 함수로 추출
function taxableChargeFn(aReading) {
  return Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
}

// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = taxableChargeFn(aReading);

// 추출한 함수를 Reading 클래스로 옮기기
get taxableChargeFn() {
  return Math.max(0, this.baseCharge - taxThreshold(this.year));
}

// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = aReading.taxableCharge

위와 같이 리팩터링을 함으로써 데이터를 필요한 시점에 계산하게 만들고, 클래스를 이용해 데이터와 동작을 한곳에 묶어두게 만들 수 있다.

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

배경

소프트웨어는 데이터를 입력받아 정보를 도출하고 이러한 정보는 여러곳에서 사용된다. 이러한 도출 작업을 한데로 모아두면 검색과 갱신을 일관된 장소에서 처리할 수 있고 로직 중복도 막을 수 있다.

이러한 경우 변환 함수 혹은 여러 함수를 클래스로 묶기 등의 방법을 사용할 수 있는데, 중요한 점은 원본 데이터를 갱신하느냐에 따라 방법을 선택하는 것이 좋다.

원본 데이터 갱신: 클래스로 묶는 편이 낫다.

절차

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

예시

차를 수돗물 처럼 제공하는 서비스의 매달 사용자가 마신 차의 양을 측정하는 상황 가정

리팩터링 전 코드

reading = {
  cutomer: 'ivan',
  quantity: 10,
  month: 5,
  year: 2017,
};

// 클라이언트 1
// 사용자에게 요금을 부과하기 위해 기본 요금 계산하는 코드
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;

// 클라이언트 2
// 세금을 부과할 소비량을 계산하는 코드
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

// 클라이언트 3
// 이러한 계산코드가 여러곳에서 반복되어 다른곳에서 함수로 만들어 둔 것을 발견
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);

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

이를 해결하는 방법으로 다양한 파생 정보 계산 로직을 하나의 변환 단계로 모음

리팩터링 이후 코드

// 1. 입력 객체를 그대로 복사해 반환하는 변환 함수를 만든다.
function enrichReading(original) {
  const result = _.cloneDeep(original);
  return result;
}

// 2. 변경하려는 계산 로직 중 하나를 고른다.
// 클라이언트 3
const rawReading = acquireReading(); // 미가공 측정값
const aReading = enrichReading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);// calculateBaseCharge()를 부가 정보를 덧붙이는 코드 근처로 옮김
// 정보를 추가해 반환하는 함수의 경우 원본 측정값 레코드는 변경하는 지 확인하는 테스트 코드를 작성하면 좋음
function enrichReading(original) {
  const result = _.cloneDeep(original);
  result.baseCharge = calculateBaseCharge(result);
  return result;
}

// 기존 코드 변경
const basicChargeAmount = calculateBaseCharge(aReading);const basicChargeAmount = aReading.baseCharge;

// 4. 세금 부과할 소비량 계산 코드에서 먼저 변환 함수부터 끼어넣자.
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));

// 계산 코드를 변환함수로 옮기자.
function enrichReading(original) {
  const result = _.cloneDeep(original);
  result.baseCharge = calculateBaseCharge(result);
  result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
  return result;
}

// 새로 만든 필드를 사용하도록 원본 코드를 수정한다.
...
const taxableCharge = aReading.taxableCharge;

단계 쪼개기

배경

서로 다른 두 대상을 한꺼번에 다루는 코드를 발견하면 각각을 별개 모듈로 나누는 방법을 모색해보자.
→ 코드를 수정해야 할 때 두 대상을 동시에 생각할 필요없이 하나에만 집중할 수 있게해줌

가장 대표적인 예: 컴파일러(토큰화 → 파싱 후 구문트리 → 구문트리 변환 → 목적 코드 생성)

절차

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

예시

상품의 결제 금액을 계산하는 코드

리팩터링 전의 코드

function priceOrder(product, quantity, shippingMethod) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
  const shippingPerCase =
    basePrice > shippingMethod.discountThreshold ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = quantity * shippingPerCase;
  const price = basePrice - discount + shippingCost;
  return price;
}

계산이 두 단계(상품 가격 계산, 배송비 계산)로 이루어 지는 것을 확인할 수 있다. 이러한 코드는 두 단계로 나누는 것이 좋다.

리팩터링 이후 코드

  1. 배송비 계산 부분을 함수로 추출
function priceOrder(product, quantity, shippingMethod) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
  const price = applyShipping(basePrice, shippingMethod, quantity, discount);
  return price;
}

// 두 번째 단계 처리함수
function applyShipping(basePrice, shippingMethod, quantity, discount) {
  const shippingPerCase =
    basePrice > shippingMethod.discountThreshold ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = quantity * shippingPerCase;
  const price = basePrice - discount + shippingCost;
  return price;
}
  1. 중간 데이터 구조 생성
function priceOrder(product, quantity, shippingMethod) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
  const priceData = {}; // 중간 데이터 구조
  const price = applyShipping(priceData, basePrice, shippingMethod, quantity, discount);
  return price;
}

function applyShipping(priceData, basePrice, shippingMethod, quantity, discount) {
  const shippingPerCase =
    basePrice > shippingMethod.discountThreshold ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = quantity * shippingPerCase;
  const price = basePrice - discount + shippingCost;
  return price;
}
  1. 중간 데이터 구조로 매개변수를 옮기고 매개변수 목록에서 제거
function priceOrder(product, quantity, shippingMethod) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
  const priceData = { basePrice, quantity, discount }; // 중간 데이터 구조
  const price = applyShipping(priceData, shippingMethod);
  return price;
}

function applyShipping(priceData, shippingMethod) {
  const shippingPerCase =
    priceData.basePrice > shippingMethod.discountThreshold ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = priceData.quantity * shippingPerCase;
  const price = priceData.basePrice - priceData.discount + shippingCost;
  return price;
}
  1. 첫 번째 단계 코드를 함수로 추출하고 이 데이터 구조를 반환
function priceOrder(product, quantity, shippingMethod) {
  const priceData = calculatePricingData(product, quantity);
  // 반환 값 바로 처리
  return applyShipping(priceData, shippingMethod);
}

// 첫 번째 단계 처리함수
function calculatePricingData(product, quantity) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
  return { basePrice, quantity, discount };
}

function applyShipping(priceData, shippingMethod) {
  const shippingPerCase =
    priceData.basePrice > shippingMethod.discountThreshold ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = priceData.quantity * shippingPerCase;
  // 남은 상수 데이터 바로 반환함으로써 처리
  return priceData.basePrice - priceData.discount + shippingCost;
}
profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글