[리팩터링 2판] - 데이터 조직화

Lee Jeong Min·2022년 9월 5일
0

리팩터링 2판

목록 보기
9/12
post-thumbnail

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

데이터 구조는 프로그램에서 중요한 역할을 한다. 이 장에서는 데이터 구조에 집중한 리팩터링에 대해서 다룬다.

변수 쪼개기

배경

나중에 쉽게 참조하려는 목적으로 쓰인 변수에는 값을 단 한번만 대입해야 한다. 대입이 두 번 이상 이뤄진다면 여러가지 역할을 수행한다는 신호이다.

역할이 둘 이상인 변수가 있다면 코드를 읽는 이에게 혼란을 주기 때문에 쪼개자.

절차

  1. 변수를 선언한 곳과 값을 처음 대입하는 곳에서 변수 이름을 바꾼다.
  2. 가능하면 이때 불변으로 선언한다.
  3. 이 변수에 두 번째로 값을 대입하는 곳 앞까지의 모든 참조(이 변수가 쓰인곳)를 새로운 변수 이름으로 바꾼다.
  4. 두 번째 대입 시 변수를 원래 이름으로 다시 선언한다.
  5. 테스트한다.
  6. 반복한다. 매 반복에서 변수를 새로운 이름으로 선언하고 다음번 대입 때까지의 모든 참조를 새 변수명으로 바꾼다. 이 과정을 마지막 대입까지 반복한다.

예시

리팩터링 전

음식이 다른 지역으로 전파된 거리를 구하는 코드

function distanceTravelled(scenario, time) {
  let result;
  let acc = scenario.primaryForce / scenario.mass; // 가속도(a) = 힘(F) / 질량(m)
  const primaryTime = Math.min(time, scenario.delay);
  result = 0.5 * acc * primaryTime * primaryTime; // 전파된 거리
  const secondaryTime = time - scenario.delay;
  if (secondaryTime > 0) { // 두 번째 힘을 반영해 다시 계산
    const primaryVelocity = acc * scenario.delay;
    acc = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass;
    result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
  }
  return result;
}

이 코드를 자세히보면, acc 변수에 값이 두 번 대입되는 것을 볼 수 있다. 하나는 첫 번째 힘이 유발한 초기 가속도를 저장하는 역할이고, 다른 하나는 두 번째 힘까지 반영된 후의 가속도를 저장하는 역할이다.
→ 쪼개야 할 변수다.

리팩터링 후

function distanceTravelled(scenario, time) {
  let result;
  const primaryAcceleration = scenario.primaryForce / scenario.mass; 
  const primaryTime = Math.min(time, scenario.delay);
  result = 0.5 * primaryAcceleration * primaryTime * primaryTime; 
  const secondaryTime = time - scenario.delay;
  if (secondaryTime > 0) {
    const primaryVelocity = primaryAcceleration * scenario.delay;
    // 여기 원래의 acc까지 1차 리팩터링
    const secondaryAcceleration = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass;
    result += primaryVelocity * secondaryTime + 0.5 * secondaryAcceleration * secondaryTime * secondaryTime;
  }
  // 이후 2차 리팩터링
  return result;
}

이 예제 말고 입력 매개변수의 값을 수정하는 예제도 나오는데, 입력 값에 기초하여 코드의 명확한 목적을 더 잘 드러내는 코드로 리팩터링하는 예제이다.

리팩터링 전

function discount(inputValue, quantity) {
  if (inputValue > 50) inputValue -= 2;
  if (quantity > 100) inputValue -= 1;
  return inputValue;
}

리팩터링 후

function discount(inputValue, quantity) {
  let result = inputValue;
  if (inputValue > 50) result -= 2;
  if (quantity > 100) result -= 1;
  return result;
}

필드 이름 바꾸기

배경

레코드 구조체의 필드 이름과 같은 데이터 구조는 프로그램을 이해하는 데 큰 역할을 한다.

클래스의 게터와 세터 이름 바꾸기도 사용자 입장에서 필드와 다름 없어서 레코드 구조체의 필드 이름 바꾸기와 똑같이 중요하다.

절차

  1. 레코드의 유효 범위가 제한적이라면 필드에 접근하는 모든 코드를 수정한 후 테스트한다. 이후 단계는 필요 없다.
  2. 레코드가 캡슐화되지 않았다면 우선 레코드를 캡슐화한다.
  3. 캡슐화된 객체 안의 private 필드명을 변경하고, 그에 맞게 내부 메서드들을 수정한다.
  4. 테스트한다.
  5. 생성자의 매개변수 중 필드와 이름이 겹치는 게 있다면 함수 선언 바꾸기로 결정한다.
  6. 접근자들의 이름도 바꿔준다.

예시

리팩터링 전

const organization = { name: '애크미 구스베리', country: 'GB' };

이 상수의 'name'을 'title'로 바꿔보자.

리팩터링 후

  1. 캡슐화 진행
class Organization {
  constructor(data) {
    this._name = data.name;
    this._country = data.country;
  }

  get name() { return this._name; }
  set name(aString) { this._name = aString; }
  get country() { return this._country; }
  set country(aCountryCode) { this._country = aCountryCode; }
}
  1. 게터, 세터, 내부 데이터 구조 변경과 constructor 내부에서 title 값 받는 로직 변경 + 호출하는 쪽 코드 변경
class Organization {
  constructor(data) {
    this._title = (data.title !== undefined) ? data.title : data.name;
    this._country = data.country;
  }

  get title() { return this._title; }
  set title(aString) { this._title = aString; }
  get country() { return this._country; }
  set country(aCountryCode) { this._country = aCountryCode; }
}

const organization = new Organization({ title: '애크미 구스베리', country: 'GB' });

파생 변수를 질의 함수로 바꾸기

배경

가변 데이터는 연쇄 효과를 일으켜 다른 쪽 코드에 원인을 찾기 어려운 문제를 야기한다.
→ 저자는 가변 데이터의 유효 범위를 가능한 한 좁혀야 한다고 주장한다.

단, 새로운 데이터 구조를 생성하는 변형 연산과 같이 계산 결과가 일정한 불변인 경우를 제외하고.

절차

  1. 변수 값이 갱신되는 지점을 모두 찾는다. 필요하면 변수 쪼개기를 활용해 각 갱신 지점에서 변수를 분리한다.
  2. 해당 변수의 값을 계산해주는 함수를 만든다.
  3. 해당 변수가 사용되는 모든 곳에서 어서션을 추가하여 함수의 계산 결과가 변수의 값과 같은지 확인한다.
  4. 테스트한다.
  5. 변수를 읽는 코드를 모두 함수 호출로 대체한다.
  6. 테스트한다.
  7. 변수를 선언하고 갱신하는 코드를 죽은 코드 제거하기로 없앤다.

예시

리팩터링 전

class ProductionPlan {
  ...

  get production() { return this._production; }
  applyAdjustment(anAdjustment) {
    this._adjustments.push(anAdjustment);
    this._production += anAdjustment.amount;
  }
}

리팩터링 후

책에서는 어서션을 추가해보고, 실패하지 않으면 코드를 수정하여 계산 결과를 직접 반환받도록 리팩터링이 진행된다.

class ProductionPlan {
  ...

  get production() { return this._adjustments.reduce((sum, a) => sum + a.amount, 0); }
  applyAdjustment(anAdjustment) {
    this._adjustments.push(anAdjustment);
  }
}

참조를 값으로 바꾸기

반대 리팩터링: 값을 참조로 바꾸기

배경

객체(데이터 구조)를 다른 객체(데이터 구조)에 중첩하면 내부 객체를 참조 혹은 값으로 취급할 수 있다.
→ 불변을 원한다면 값으로 취급하는게 맞다.

그러나 이러한 리팩터링은 특정 객체를 여러 객체에서 공유하고자 한다면, 참조로 다뤄야하기 때문에 맞지 않다.

절차

  1. 후보 클래스가 불변인지, 혹은 불변이 될 수 있는지 확인한다.
  2. 각각의 세터를 하나씩 제거한다.
  3. 이 값 객체의 필드들을 사용하는 동치성 비교 메서드를 만든다.

예시

리팩터링 전

class Person {
  constructor() {
    this._telephoneNumber = new TelephoneNumber();
  }

  get officeAreaCode() {return this._telephoneNumber.areaCode;}
  set officeAreaCode(arg) {this._telephoneNumber.areaCode = arg;}
  get officeNumber() {return this._telephoneNumber.number;}
  set officeNumber(arg) {this._telephoneNumber.number = arg;}
}

class TelephoneNumber {
  get areaCode() {return this._areaCode;}
  set areaCode(arg) {this._areaCode = arg;}
  get number() {return this._number;}
  set number(arg) {this._number = arg;}
}

리팩터링 후

전화번호를 불변으로 만들고, 필드들의 세터를 제거하자.

class Person {
  constructor() {
    this._telephoneNumber = new TelephoneNumber();
  }

  get officeAreaCode() {return this._telephoneNumber.areaCode;}
  set officeAreaCode(arg) {this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber);}
  get officeNumber() {return this._telephoneNumber.number;}
  set officeNumber(arg) {this._telephoneNumber = new TelephoneNumber(this.officeAreaCode, arg);}
}

class TelephoneNumber {
  constructor(areaCode, number) {
    this._areaCode = areaCode;
    this._number = number;
  }

  get areaCode() {return this._areaCode;}
  set areaCode(arg) {this._areaCode = arg;}
  get number() {return this._number;}
  set number(arg) {this._number = arg;}
}

이후 동치성을 값 기반으로 평가하여 동치성 검사를 수행하자.

값을 참조로 바꾸기

반대 리팩터링: 참조를 값으로 바꾸기

배경

논리적으로 같은 데이터를 물리적으로 복제해 사용할 때 가장 크게 문제되는 상황은 데이터를 갱신해야 할 때다.
→ 이런 상황이라면 복제된 데이터들을 모두 참조로 바꿔주는게 좋다.

절차

  1. 같은 부류에 속하는 객체들을 보관할 저장소를 만든다.
  2. 생성자에서 이 부류의 객체들 중 특정 객체를 정확히 찾아내는 방법이 있는지 확인한다.
  3. 호스트 객체의 생성자들을 수정하여 필요한 객체를 이 저장소에서 찾도록 한다. 하나 수정할 때마다 테스트한다.

예시

리팩터링 전

class Order {
  constructor(data) {
    this._number = data.number;
    this._customer = new Customer(data.customer);
  }

  get customer() {return this._customer;}
}

class Customer {
  constructor(id){
    this._id = id;
  }

  get id() {return this._id;}
}

현재는 고객이 값이고, 주문 다섯 개를 생성한다면 독립된 고객 객체가 다섯 개 만들어진다. 이를 참조로 바꾸어 물리적으로 똑같은 고객 객체를 사용하도록 만들어보자.

리팩터링 후

저자는 저장소 객체를 사용하여 리팩터링함

let _repositoryData;

export function initialize() {
  _repositoryData = {};
  _repositoryData.customers = new Map();
}

export function registerCustomer() {
  if(!_repositoryData.customers.has(id)) _repositoryData.customers.set(id, new Customer(id));
  return findCustomer(id);
}

export function findCustomer(id) {
  return _repositoryData.customers.get(id);
}

class Order {
  constructor(data) {
    this._number = data.number;
    this._customer = registerCustomer(data.customer);
    // 다른 데이터를 읽어 들인다.
  }

  get customer() {return this._customer}
}

위 repository는 ID 하나당 오직 고객 객체만 생성됨을 보장한다. 이를 통해 같은 고객을 공유하는 주문 모두에서 갱신된 데이터를 사용하게 된다.

매직 리터럴 바꾸기

배경

매직 리터럴 = 소스 코드에 등장하는 일반적인 리터럴 값

이러한 리터럴 값은 코드를 읽는사람이 의미를 모르는경우 가독성을 방해하며 코드 자체의 뜻을 분명하게 드러내는 게 좋다.

절차

  1. 상수를 선언하고 매직 리터럴을 대입한다.
  2. 해당 리터럴이 사용되는 곳을 찾는다.
  3. 찾은 곳 각각에서 리터럴이 새 상수와 똑같은 의미로 쓰였는지 확인하여, 같은 의미라면 상수로 대체한 후 테스트한다.

예시

리팩터링 전

function potentialEnergy(mass, height) {
  return mass * 9.81 * height;
}

리팩터링 후

const STANDARD_GRAVITY = 9.81;
function potentialEnergy(mass, height) {
  return mass * STANDARD_GRAVITY * height;
}
profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글