본문 바로가기
Programming/13. Book

리팩터링 - 8, 9장

by @sangseophwang 2024. 3. 2.

Chapter 08. 기능 이동

✏️ 함수 옮기기

배경

  • 좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐이다.
  • 이를 모듈성(modularity)라고 한다.

모듈성: 프로그램의 어딘가를 수정하려 할 때 해당 기능과 깊이 관련된 작은 일부만 이해해도 가능하게 해주는 능력.

  • 모듈성을 높이려면 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야 한다.
  • 독립적으로 고유한 가치가 있는 함수는 분리하고, 관련이 있는 함수끼리는 묶어준다.
  • 이를 결정하기 위해서는 대상 함수의 컨택스트(해당 함수가 호출하는 함수는 무엇이 있는지, 사용하는 데이터가 무엇인지)를 살펴본다.

절차

  1. 선택한 함수가 현재 컨택스트에서 사용중인 모든 요소를 살펴본다. 그리고 이 중 함께 옮길만한 게 있는지 확인한다.
    1. 호출되는 함수 중 함께 옮길 게 있다면 대체로 그 함수를 먼저 옮기는 게 낫다.
    2. 얽혀 있는 함수가 여러 개면 영향이 적은 함수부터 옮긴다.
  2. 선택한 함수가 다형 메서드인지 확인한다.
  3. 해당 함수를 타깃 컨택스트(옮기려는 위치)에 복사한다.
    1. 기존의 함수는 소스 함수(source function), 새로 복사한 함수를 타깃 함수(target function)라 한다.
  4. 새로운 컨택스트에 어울리는 새로운 이름으로 변경해주고 소스 함수를 타깃 함수의 위임 함수가 되도록 수정한다.

예시

Before

// 현재 상태 : trackSummary 내부에 연산 함수들을 포함하고 있음.
// 목표 : calculateDistance 함수를 최상위로 옮겨 독립적으로 계산

function trackSummary(points) { // 추적 거리의 총 계산
  const totalTime = calculateTime();
  const totalDistance = calculateDistance();
  const pace = totalTime / 60 / totalDistance;
  return {
    time: totalTime,
    distance: totalDistance,
    pace: pace,
  };

  function calculateDistance() { // 총 거리 계산 ⭐️
    let result = 0;
    for (let i = 1; i < points.length; i++) {
      result += distance(points[i - 1], points[i]);
    }
    return result;
  }
  
  function distance(p1, p2) { ... } // 두 지점의 거리 계산
  function radians(degrees) { ... } // 라디안 값으로 변환
  function calculateTime() { ... } // 총 시간
}

After

  1. trackSummary 함수 외부로 복사한다.
  2. 새로운 (임시) 이름을 지어준다. (top_calculateDistance)
  3. calculateDistance에서만 사용중인 distance, radians 함수를 calculateDistance 함수로 옮긴다.
  4. 정적 분석(lint)과 테스트를 활용해 검증 후 top_calculateDistance에 동일하게 복사해준다.
  5. 이름을 totalDistance로 변경해준다.
  6. 소스 함수를 제거하고 totalDistance 함수에 의존적이지 않은 distance, radians 함수도 최상위로 옮겨준다.
function trackSummary(points) {
  const totalTime = calculateTime();
  const pace = totalTime / 60 / totalDistance(points); // 변수 인라인하기
  return {
    time: totalTime,
    distance: totalDistance(points),
    pace: pace,
  };
}

function totalDistance(points) { // ⭐️
  let result = 0;
  for (let i = 1; i < points.length; i++) {
    result += distance(points[i - 1], points[i]);
  }
  return result;
}

function distance(p1, p2) { ... }
function radians(degrees) { ... }

Deep dive

  • 데이터끼리 상호 의존하기 쉬우니 중첩 함수는 되도록 만들지 말자’ 는 내용이 있다. 하지만 때론 위와 같이 함수를 최상위로 분리하기보다는 중첩으로 사용하는 것이 더 나을 때도 있을 것이다. 어떤 경우가 있을까?

→ ex) 재사용성은 낮으나 함수의 변수화로 코드의 이해도를 높이고자 하는 경우

✏️ 필드 옮기기

배경

  • 프로그램의 진짜 힘은 데이터 구조에서 나온다.
  • 적합한 데이터 구조는 동작 코드를 자연스럽게 단순하고 직관적으로 짤 수 있게 한다.
  • 반면 잘못 선택한 데이터 구조는 코드를 이해하기 어렵게 만들고, 더 나아가 프로그램의 역할을 흐릿하게 한다.
  • 필드를 옮겨야 하는 상황은 다음과 같다.
    • 함수에 어떤 레코드를 넘길 때 마다 또 다른 레코드의 필드도 함께 넘기고 있을 때
    • 한 레코드를 변경하는데 다른 레코드의 필드까지 변경해야 할 때
    • 구조체 여러 개에 정의된 똑같은 필드들을 갱신해야 할 때

절차

  1. 소스 필드가 캡슐화되어 있지 않다면 캡슐화한다.
  2. 타깃 객체에 필드(와 접근자 메서드들)을 생성한다.
  3. 소스 객체에서 타깃 객체를 참조할 수 있는지 확인한다.
  4. 접근자들이 타깃 필드를 사용하도록 수정한다.
  5. 소스 필드를 제거한다.

예시

Before

// 목표 : 할인율을 뜻하는 discountRate 필드를 CustomerContract로 옮기기

class Customer { // 고객 클래스
  constructor(name, discountRate) {
    this._name = name;
    this._discountRate = discountRate;
    this._contract = new CustomerContract(dateToday());
  }

  get discountRate() {
    return this._discountRate;
  }

  becomePreferred() {
    this._discountRate += 0.03;
    ...
  }

  applyDiscount(amount) {
    return amount.subtract(amount.multiply(this._discountRate));
  }

  dateToday() {
    return new Date();
  }
}

class CustomerContract { // 계약 클래스
  constructor(startDate) {
    this._startDate = startDate;
  }
}

After

  1. 필드를 캡슐화한다.
    1. this._setDiscountRate(discountRate)
  2. CustomerContract 클래스에 필드 하나와 접근자들을 추가한다.
  3. Customer의 접근자들이 새로운 필드를 사용하도록 수정한다.
class Customer {
  constructor(name, discountRate) {
    this._name = name;
    this._contract = new CustomerContract(this.dateToday());
    this._setDiscountRate(discountRate); // contract 클래스 호출 뒤 실행
  }

  get discountRate() {
    return this._contract.discountRate;
  }

  setDiscountRate(number) {
    return this._contract.discountRate = number;
  }

  becomePreferred() {
    this._contract.discountRate += 0.03;
    // 다른 코드들이 있음...
  }

  applyDiscount(amount) {
    return amount.subtract(amount.multiply(this._contract.discountRate));
  }

  dateToday() {
    return new Date();
  }
}

class CustomerContract {
  constructor(startDate, discountRate) {
    this._startDate = startDate;
    this._discountRate = discountRate; // Customer 클래스에서 setDiscountRate로 값 생성
  }

  get discountRate() {
    return this._discountRate;
  }

  set discountRate(arg) {
    this._discountRate = arg;
  }
}

 

Deep dive

  • 이를 함수에서 응용한다면 어떤 예가 있을까?
    • custom hook으로의 상태 이동?

✏️ 문장을 함수로 옮기기

배경

  • 중복 제거는 코드를 건강하게 관리하는 가장 효과적인 방법 중 하나다.
    • 특정 함수를 호출하는 코드가 나올 때마다 그 앞, 뒤에서 똑같은 코드가 추가로 실행될 때
    • 반복되는 부분을 피호출 함수로 합치는 방법에 대해 고민해볼 필요가 있다.
    • 이렇게 하면 수정할 일이 생겼을 때 단 한 곳만 수정하면 된다.

절차

  1. 반복 코드가 함수 호출 부분과 멀리 떨어져 있다면 문장 슬라이스하기를 적용해 근처로 옮긴다.
  2. 타깃 함수를 호출하는 곳이 한 곳뿐이면, 단순히 소스 위치에서 해당 코드를 잘라내어 피호출 함수로 복사한다.
  3. 호출자가 둘 이상이면 호출자 중 하나에서 '타깃 함수 호출부와 그 함수로 옮기려는 문장 등을 함께' 다른 함수로 추출한다. 추출한 함수에 기억하기 쉬운 임시 이름을 지어준다.
  4. 다른 호출자 모두가 방금 추출한 함수를 사용하도록 수정한다.
  5. 모든 호출자가 새로운 함수를 사용하게 되면 원래 함수를 새로운 함수 안으로 인라인한 후 원래 함수를 제거한다.
  6. 새로운 함수의 이름을 원래 함수의 이름으로 바꿔준다. (더 나은 이름이 있다면 그 이름을 쓴다)

예시

Before

// 함수 내용 : 사진 관련 데이터를 HTML로 내보내는 코드
// 목표 : 제목을 출력하는 코드를 emitPhotoData로 옮겨 중복 제거

function renderPerson(person) { // result 배열에 이름, 사진 데이터, 정보 HTML을 넣는 역할
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(renderPhoto(person.photo));
  result.push(`<p>제목: ${person.photo.title}</p>`); // 제목 출력 ⭐️
  result.push(emitPhotoData(person.photo));           // ⭐️
  return result.join('\n');
}

function photoDiv(aPhoto) {
  return [
    '<div>', 
    `<p>제목: ${p.title}</p>`, // 제목 출력 ⭐️
    emitPhotoData(aPhoto),           // ⭐️
    '</div>'].join('\n');
}

function emitPhotoData(aPhoto) {
  const result = [];
  result.push(`<p>위치: ${aPhoto.location}</p>`);
  result.push(`<p>날짜: ${aPhoto.date.toDateString()}</p>`);
  return result.join('\n');
}

function renderPhoto(aPhoto) {
  return '';
}

After

  1. 제목과 emitPhotoData를 호출하는 새 함수를 생성한다.
  2. 새 함수를 호출하도록 수정한다.
  3. emitPhotoData 함수를 인라인한다.
  4. 기존 함수를 제거하고 새 함수의 이름을 기존 함수로 변경한다.
function renderPerson(person) { 
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(renderPhoto(person.photo));
  result.push(emitPhotoData(person.photo)); // ⭐️
  return result.join('\n');
}

function photoDiv(aPhoto) {
  return [
    '<div>', 
    emitPhotoData(aPhoto),  // ⭐️
    '</div>'].join('\n');
}

function emitPhotoData(aPhoto) {
  return [
    `<p>제목: ${aPhoto.title}</p>`,  // ⭐️
    `<p>위치: ${aPhoto.location}</p>`
    `<p>날짜: ${aPhoto.date.toDateString()}</p>`
  ].join('\n');
}

✏️ 문장을 호출한 곳으로 옮기기

배경

  • 함수는 프로그래머가 쌓아 올리는 추상화의 기본 블록이다.
  • 하지만 초기에는 응집도 높고 한 가지 일만 수행하던 함수가 어느새 둘 이상의 다른 일을 수행하게 된다.
  • 이를 발견한다면 달라진 동작을 유발하는 코드를 적절한 위치로 옮겨줘 그 경계를 명확히 해야 한다.

절차

  1. 호출자가 한두 개뿐이고 피호출 함수도 간단한 단순한 상황이면, 피호출 함수의 처음(혹은 마지막)줄을 잘라내어 호출자로 복사해 넣는다.
  2. 복잡한 상황에서는, 이동하지 '않길' 원하는 모든 문장을 함수로 추출한 다음 검색하기 쉬운 임시 이름을 지어준다.
  3. 원래 함수를 인라인한다.
  4. 추출된 함수의 이름을 원래 함수의 이름으로 변경한다. (더 나은 이름이 있다면 그 이름을 쓴다)

예시

Before

// 현재 상황 : emitPhotoData를 통해 제목, 날짜, 위치가 div 태그 내에 들어가도록 설정돼있음
// 목표: listRecentPhotos가 위치 정보를 외부로 빼 다르게 렌더링하도록 수정하기

// 이름, 사진, 정보를 생성하는 함수
function renderPerson(outStream, person) {
  outStream.write(`<p>${person.name}</p>\n`);
  renderPhoto(outStream, person.photo);
  emitPhotoData(outStream, person.photo);
}

// 기간 필터 후 각 사진별로 div 태그 안에 emitPhotoData(사진 정보) 태그 추가
function listRecentPhotos(outStream, photos) {
  photos
    .filter((p) => p.date > recentDateCutoff())
    .forEach((p) => {
      outStream.write('<div>\n');
      emitPhotoData(outStream, p);
      outStream.write('</div>\n');
    });
}

// 제목, 날짜, 위치 생성
function emitPhotoData(outStream, photo) {
  outStream.write(`<p>제목: ${photo.title}</p>\n`);
  outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
  outStream.write(`<p>위치: ${photo.location}</p>\n`); // ⭐️
}


// renderPerson 렌더링 결과

<p>{person.name}</p>
<img />
<div>
  <p>제목: {photo.title}</p>
  <p>날짜: {photo.date.toDataString()}</p>
  <p>위치: {photo.location}</p> // ⭐️
</div>

After

function renderPerson(outStream, person) {
  outStream.write(`<p>${person.name}</p>\n`);
  renderPhoto(outStream, person.photo);
  emitPhotoData(outStream, person.photo); // ⭐️
  outStream.write(`<p>위치: ${person.photo.location}</p>\n`)
}

function listRecentPhotos(outStream, photos) {
  photos
    .filter((p) => p.date > recentDateCutoff())
    .forEach((p) => {
      outStream.write('<div>\n');
      emitPhotoData(outStream, p); // ⭐️
      outStream.write(`<p>위치: ${p.location}</p>\n`);
      outStream.write('</div>\n');
    });
}

function emitPhotoData(outStream, photo) { // ⭐️
  outStream.write(`<p>제목: ${photo.title}</p>\n`);
  outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
}

// renderPerson 렌더링 결과

<p>{person.name}</p>
<img />
<div>
  <p>제목: {photo.title}</p>
  <p>날짜: {photo.date.toDataString()}</p>
</div>
<p>위치: {photo.location}</p> // ⭐️

✏️ 인라인 코드를 함수 호출로 바꾸기

배경

  • 함수는 여러 동작을 하나로 묶어준다.
  • 똑같은 코드를 반복하는 대신 함수를 호출하면 된다.
  • 이미 존재하는 함수와 똑같은 일을 하는 인라인 코드가 있다면
    • 함수 이름을 확인하고 적절하다면 해당 함수로 교체한다.
    • 기능만 (우연히) 같을 뿐 사용 목적이 다르다면 함수 추출하기를 사용한다.

절차

  1. 인라인 코드를 함수 호출로 대체한다.

예시

Before

let appliesToMass = false
for (const s of states) {
    if (s === 'MA') {
        appliesToMass = true
    }
}

After

appliesToMass = states.includes('MA')

✏️ 문장 슬라이드하기

배경

  • 관련된 코드들이 가까이 모여 있다면 이해하기가 더 쉽다.
  • 관련된 코드끼리 모으는 작업은 다른 리팩터링의 준비 단계로 자주 행해진다.
  • 가장 흔한 사례는 변수를 선언하고 사용할 때 관련 있는 변수끼리 모아주는 작업이 있다.

절차

  1. 코드 조각(문장들)을 이동할 목표 위치를 찾는다.
    1. 원래 위치와 목표 위치 사이의 코드를 확인하고 이동 가능 범위가 어디까지인지 확인한다.
  2. 원래 위치에서 잘라내 목표 위치에 붙여넣는다.

예시

Before

// 예제 1
// 목표 : 연관 있는 변수끼리 묶을 수 있도록 위치 이동하기

const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit; // ⭐️

// 예제 2
function someFunc() {
  let result;
  if (availableResources.length === 0) {
    result = createResource();
    allocatedResources.push(result); // ⭐️
  } else {
    result = availableResources.pop();
    allocatedResources.push(result); // ⭐️
  }
  return result;
}

After

// 예제 1
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit; // ⭐️

const order = retreiveOrder();
let charge;

// 예제 2
function someFunc() {
  let result;
  if (availableResources.length === 0) {
    result = createResource();
  } else {
    result = availableResources.pop();
  }
  allocatedResources.push(result); // ⭐️
  return result;
}

Deep dive

  • 이 파트에서는 코드 이동에 있어 부수효과가 없음을 확인하는 방식으로 ‘명령-질의 분리 원칙(Command-Query Separation Principle)을 지켜가며 코딩’하는 습관에 대한 소개가 있었다. 그렇다면 이 원칙은 무엇이며, 어떻게 이 원칙을 지키며 작성을 할 수 있을까?

✏️ 반복문 쪼개기

배경

  • 종종 반복문 하나에서 두 가지 일을 수행하는 모습을 보게 된다.
  • 이 때 각각의 반복문으로 분리해두면 문제 발생 시 수정할 동작 하나만 이해하고 고칠 수 있다.

절차

  1. 반복문을 복제해 두 개로 만든다.
  2. 반복문이 중복돼 생기는 부수효과를 파악해 제거한다.
  3. 각 반복문을 함수로 추출할지 고민한다.

예시

Before

// 코드 내용 : 전체 급여와 최연소 나이를 계산하는 코드
// 목표 : 반복문 내 두 가지 계산을 처리하는 역할을 분리하기

let youngest = people[0] ? people[0].age : Infinity;
let totalSalary = 0;
for (const p of people) {
  if (p.age < youngest) youngest = p.age; // ⭐️
  totalSalary += p.salary; // ⭐️
}

return `최연소: ${youngest}, 총 급여: ${totalSalary}`;

After

let youngest = people[0] ? people[0].age : Infinity;
let totalSalary = 0;
for (const p of people) { // ⭐️
  totalSalary += p.salary;
}

for (const p of people) { // ⭐️
  if (p.age < youngest) youngest = p.age;
}

return `youngestAge: ${youngest}, totalSalary: ${totalSalary}`;


// 더 가다듬기
// 반복문을 함수로 호출하기 + 내장 메서드 활용하기

return `youngestAge: ${youngestAge()}, totalSalary: ${totalSalary()}`;

function totalSalary() {
  return people.reduce((total, p) => total + p.salary, 0);
}

function youngestAge() {
  return Math.min(...people.map(p => p.age));
}

✏️ 반복문을 파이프라인으로 바꾸기

배경

  • 컬렉션 파이프라인(Collection Pipeline)을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다.
  • 대표적인 연산은 map과 filter다.
  • map은 함수를 사용해 입력 컬렉션의 각 원소를 반환하고, filter는 또 다른 함수를 사용해 입력 컬렉션을 필터링해 부분집합을 만든다.

절차

  1. 반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.
  2. 반복문 각각의 단위 행위를 적절한 컬렉션 파이프라인 연산으로 대체한다.
  3. 모든 동작을 대체했다면 반복문 자체를 지운다.

예시

Before

// 함수 역할 : 인도에 자리한 사무실을 찾아 도시명과 전화번호를 반환
// 목표 : 반복문을 컬렉션 파이프라인으로 바꾸기

function acquireData(input) {
  const lines = input.split('\n');     // 컬렉션
  let firstLine = true;
  const result = [];
  for (const line of lines) {            // 반복문
    if (firstLine) {                     // 첫 줄 건너뛰기
      firstLine = false;
      continue;
    }
    if (line.trim() === '') continue;    // 빈 줄 지우기
    const record = line.split(',');      // , 를 기준으로 문장 나누기
    if (record[1].trim() === 'India') {  // 인도가 확인되면 해당 사무실 레코드를 추출
      result.push({ city: record[0].trim(), phone: record[2].trim() });
    }
  }
  return result;
}

After

function acquireData(input) {
  const lines = input.split('\n');
  return lines
      .slice  (1)
      .filter (line   => line.trim() != '')
      .map    (line   => line.split(','))
      .filter (fields => fields[1].trim() === 'India')
      .map    (fields => ({city: fields[0].trim(), phone: fields[2].trim()}));
}

✏️ 죽은 코드 제거하기

배경

  • 사용하지 않는 코드들은 최신 컴파일러에 의해 자동으로 제거된다.
  • 그렇더라도 이런 코드들은 동작 이해에 커다란 걸림돌이 될 수 있다.
  • 그러므로 코드가 더 이상 사용되지 않게 됐다면 지워야 한다.
    • 후에 필요할 지도 모른다는 걱정이 든다면 버전 관리 시스템을 활용하자!

절차

  1. 죽은 코드를 외부에서 참조할 수 있는 경우라면 호출하는 곳이 있는지 확인한다.
  2. 없다면 제거한다.

Chapter 09. 데이터 조직화

✏️ 변수 쪼개기

배경

  • 변수의 대입이 두 번 이상 이루어진다면 여러 가지 역할을 수행한다는 신호다.
  • 이 경우 예외 없이 해당 변수를 역할별로 쪼개야 한다.
  • 여러 용도로 쓰인 변수는 코드를 읽는 이에게 커다란 혼란을 주기 때문이다.

절차

  1. 변수를 선언한 곳과 처음 대입하는 곳에서 변수 이름을 바꿔준다.
    1. 이 때 가능하면 불변으로 선언한다.
  2. 모든 참조에 새로운 변수명으로 변경한다.

예시

Before

// 예제 1
let temp = 2 * (height + width);
console.log(temp);
temp = height * width; // ⭐️
console.log(temp);

// 예제 2
// 현재 상황: 매개변수 값을 조건에 따라 변경하고 반환하는 상황
function discount(inputValue, quantity) {
  if (inputValue > 50) inputValue = inputValue - 2;
  if (quantity > 100) inputValue = inputValue - 1;
  return inputValue;
}

After

// 예제 1
const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width; // ⭐️
console.log(area);

// 예제 2
// 변수 생성 후 해당 변수에 연산값을 적용한 후 반환
function discount(inputValue, quantity) {
  let result = inputValue; // ⭐️
  if (inputValue > 50) result = inputValue - 2;
  if (quantity > 100) result = inputValue - 1;
  return result;
}

✏️ 필드 이름 바꾸기

배경

  • 데이터 구조는 무슨 일이 벌어지는지를 이해하는 열쇠다.
  • 중요한만큼 반드시 깔끔하게 관리해야 한다.

절차

  1. 레코드의 유효 범위가 제한적이면 필드에 접근하는 모든 코드를 수정하고 끝낸다.
    1. 범위가 크면 다음 단계로 넘어간다.
  2. 우선 레코드를 캡슐화한다.
  3. 캡슐화된 객체 안의 private 필드명을 변경하고, 그에 맞게 내부 메서드들을 수정한다.
  4. 매개변수 중 필드명과 겹치는 이름이 있다면 함수 선언 바꾸기로 변경한다.

예시

Before

// 목표: "name"을 "title"로 변경하기

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;}
}

const organization = new Organization({name: "애크미 구스베리", county: "GB"});

After

  1. 생성자, 접근자 데이터의 name을 title로 변경해준다.
  2. 생성자에서 title도 받아들일 수 있도록 조치한다.
    1. this._title = (data._title !== undefined) ? data.title : data.name;
  3. 생성자를 호출하는 쪽을 새로운 이름으로 변경해준다.
    1. const organization = new Organization({title: …
  4. 수정 후 name을 사용할 수 있게 하던 코드를 제거한다.
  5. 접근자의 이름을 title로 변경해준다.
class Organization {
    constructor(data) {
      this._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: "애크미 구스베리", county: "GB"});

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

배경

  • 가변 데이터는 소프트웨어에 문제를 일으키는 가장 큰 골칫거리에 속한다.
  • 예컨데 한 쪽 코드에서 수정한 값이 연쇄 효과를 일으켜 다른 쪽 코드에 원인을 찾기 어려운 문제를 야기하기도 한다.
  • 효과가 좋은 방법으로, 값을 쉽게 계산해낼 수 있는 변수들을 모두 제거할 수 있다.

절차

  1. 변수 값이 갱신되는 지점을 모두 찾는다.
  2. 해당 변수의 값을 계산해주는 함수를 만든다.
  3. 해당 변수가 사용되는 모든 곳에 어서션(assetion)을 추가해 함수의 계산 결과가 같은지 확인한다.
  4. 변수를 읽는 코드를 모두 함수 호출로 대체한다.
  5. 변수를 선언하고 갱신하는 코드를 죽은 코드 제거하기로 없앤다.

예시

Before

// 현재 상황: adjustment 값을 적용하면서 production까지 갱신

class ProductionPlan {
  get production() {
    return this._production;
  }
  
  applyAdjustment(adjustment) {
    this._adjustments.push(adjustment); 
    this._production += adjustment.amount; // ⭐️
  }
}

After

  1. 어서션을 추가해 테스트해본다.
    1. assert(this._production === this.calculatedProduction);
    2. get calculatedProduction() {…}
  2. 메서드를 인라인하고 옛 변수를 참조하는 코드를 제거한다.
class ProductionPlan {
  get production() {
    return this._adjustments.reduce((sum, a) => sum + a.amount, 0); // ⭐️
  }
  
  applyAdjustment(adjustment) {
    this._adjustiments.push(adjustment);
  }
}

✏️ 참조를 값으로 바꾸기

배경

  • 객체를 다른 객체에 중첩하면 내부 객체를 참조 혹은 값으로 취급할 수 있다.
    • 참조로 다루는 경우 내부 객체는 그대로 둔 채 그 객체의 속성만 갱신한다.
    • 값으로 다루는 경우는 새로운 속성을 담은 객체로 기존 내부 객체를 통째로 대체한다.
  • 필드를 값으로 다루면 내부 객체의 클래스를 수정해 값 객체로 만들 수 있다.
  • 값 객체는 불변이기에 자유롭게 활용하기 좋다.

절차

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

예시

Before

// 현재 상황: Person 클래스에서 생성한 telephoneNumber 데이터의 값을 갱신할 수 있다.
// 목표: 값을 변경하지 않는 방향으로 코드 개선

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 {
  constructor(area, number) {
    this._areaCode = area;
    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; // ⭐️
  }
}

After

  1. TelephoneNumber 클래스를 불변으로 만든다.
    1. 세터 함수 제거하기
  2. Person 클래스 세터 함수에서는 전화번호를 매번 대입해 바꾸도록 한다.
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(area, number) {
    this._areaCode = area;
    this._number = number;
  }
  
  get areaCode() {
    return this._areaCode;
  }

  get number() {
    return this._number;
  }
}

✏️ 값을 참조로 바꾸기

배경

  • 논리적으로 같은 데이터를 물리적으로 복제해 사용할 때 가장 크게 문제되는 상황은 그 데이터를 갱신할 때다.
  • 모든 복제본을 찾아 빠짐없이 갱신해야 하며, 하나라도 놓치면 데이터 일관성이 깨져버린다.
  • 이런 상황이라면 복제된 데이터들을 모두 참조로 바꿔주는게 좋다.

절차

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

예시

Before

// 현재 상황: 고객 객체는 값으로써 사용되며, 만약 고객 ID가 123인 주문을 다섯 개 생성하면 독립된
//          객체가 다섯 개 생성돼 모두 변경해줘야 하는 상황

// 목표: 저장소를 만들어 물리적으로 똑같은 고객 객체를 사용하도록 수정

class Order {
  constructor(data) {
    this._number = data.number;
    this._customer = new Customer(data.customerId); // 고객 ID
  }

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

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

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

After

  1. 고객 객체를 저장할 수 있는 저장소 객체를 생성한다.
  2. registerCustomer로 고객 정보를 등록하고 findCustomer로 찾고자 하는 고객을 반환하도록 함수를 제작한다.
let _repositoryData; // 일종의 전역 저장소

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

export function registerCustomer(id) {
  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.customerId);
  }

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

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

✏️ 매직 리터럴 바꾸기

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

ex) 9.80665, 3.14…

배경

  • 코드를 읽는 사람이 값의 의미를 모른다면 숫자(값) 자체로는 의미를 명확히 알려주지 못하므로 매직 리터럴이라 할 수 있다.
  • 코드를 읽는 사람을 위해 코드 자체의 뜻을 분명하게 드러낼 수 있도록 상수로 정의하는 것이 좋다.

절차

  1. 상수를 선언하고 매직 리터럴을 대입한다.
  2. 해당 리터럴이 사용되는 곳을 모두 찾는다.
  3. 찾은 곳 각각에 리터럴이 새 상수와 동일한 의미로 쓰였는지 확인하고 대체한다.

예시

Before

const potentialEnergy = (mass, height) => {
    return mass * height * 9.81
}

After

const STANDARD_GRAVITY = 9.81

const potentialEnergy = (mass, height) => {
    return mass * height * STANDARD_GRAVITY
}
반응형

'Programming > 13. Book' 카테고리의 다른 글

리팩터링 - 11장  (0) 2024.03.02
리팩터링 - 10장  (0) 2024.03.02
리팩터링 - 5, 6장  (0) 2024.03.02
리팩터링 - 2장  (0) 2024.03.02
모던 리액트 Deep Dive - 11장  (0) 2024.03.02

댓글