[리팩터링 2판] - 기능 이동

Lee Jeong Min·2022년 9월 1일
0

리팩터링 2판

목록 보기
8/12
post-thumbnail

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

요소를 다른 컨텍스트로 옮기는 일 역시 리팩터링의 중요한 축이며 이에 대한 리팩터링 기법들을 알아보자.

함수 옮기기

배경

좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐를 뜻하는 모듈성이다. 모듈성이란 프로그램의 어딘가를 수정하려 할 때 해당 기능과 깊이 관련된 작은 일부만 이해해도 가능하게 해주는 능력이다.

이러한 모듈성은 프로그램의 이해도에 따라 구체적인 방법이 달라진다.
ex) 함수를 어디(어떤 맥락에)에 둘지 결정하는 것은 사람마다 다르다. (이러한 것들은 많이 해보면서 적절한 위치를 찾게된다.)

절차

  1. 선택한 함수가 현재 컨텍스트에서 사용 중인 모든 프로그램 요소를 살펴본다. 이 요소들 중에도 함께 옮겨야 할 게 있는지 고민해본다.
  2. 선택한 함수가 다형 메서드인지 확인한다.
  3. 선택한 함수를 타깃 컨텍스트로 복사한다. 타깃 함수가 새로운 터전에 잘 자리 잡도록 다듬는다.
  4. 정적 분석을 수행한다.
  5. 소스 컨텍스트에서 타깃 함수를 참조할 방법을 찾아 반영한다.
  6. 소스 함수를 타깃 함수의 위임 함수가 되도록 수정한다.
  7. 테스트한다.
  8. 소스 함수를 인라인할지 고민해본다.

예시

책에서는 2개의 예시로 중첩 함수를 최상위로 옮기기, 다른 클래스로 옮기기가 나온다. 예제들이 간단하기 때문에 책 참고.

필드 옮기기

배경

프로그램의 진짜 힘은 데이터 구조에서 나온다.
→ 주어진 문제에 적합한 데이터 구조를 활용하면 코드는 자연스럽게 단순하고 직관적으로 짜여지지만, 그렇지 않으면 데이터를 다루기 위한 코드로 범벅이 되기 때문

구조체 여러 개에 정의된 똑같은 필드들을 갱신해야 한다면 한 번만 갱신해도 되는 다른 위치로 옮기라는 신호다 → 필드 옮기기 리팩터링 적용 필요

절차

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

예시

리팩터링 전의 코드

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.3;
    // other things...
  }
  applyDiscount(amount) {
    return amount.subtract(amount.multiply(this._discountRate));
  }
}

class CustomerContract {
  constructor(startDate) {
    this._startDate = startDate;
  }
}

리팩터링 이후 코드

  1. 가장 먼저 할 일: 필드를 캡슐화하기(변수 캡슐화 하기)
class Customer {
  constructor(name, discountRate) {
    this._name = name;
    this._setDiscountRate(discountRate);
    this._contract = new CustomerContract(dateToday());
  }

  get discountRate() { return this._discountRate;}
  _setDiscountRate(aNumber) {this._discountRate = aNumber;}
  becomePreferred() {
    this._setDiscountRate(this.discountRate + 0.03);
    // other things...
  }
  applyDiscount(amount) {
    return amount.subtract(amount.multiply(this.discountRate));
  }
}
  1. CustomerContract 클래스에 필드 하나와 접근자들을 추가한다.
class CustomerContract {
  constructor(startDate, discountRate) {
    this._startDate = startDate;
    this._discountRate = discountRate;
  }

  get discountRate() { return this._discountRate; }
  set discountRate(arg) { this._discountRate = arg; }
}
  1. Customer의 접근자들이 새로운 필드를 사용하도록 수정한다.
class Customer {
  constructor(name, discountRate) {
    this._name = name;
    this._contract = new CustomerContract(dateToday());
    this._setDiscountRate(discountRate);
  }

  get discountRate() { return this._contract.discountRate;}
  _setDiscountRate(aNumber) {this._contract.discountRate = aNumber;}
}

두 번쨰 예제로 공유 객체로 이동하는 리팩터링이 등장하는데 이러한 리팩터링의 경우 겉보기 동작이 달라지는지 잘 확인하고 리팩터링을 해야함에 유의하자. 예시는 간단하여 따로 첨부 X

문장을 함수로 옮기기

반대 리팩터링: 문장을 호출한 곳으로 옮기기

배경

특정 함수를 호출하는 코드가 나올 때마다 그 앞이나 뒤에 똑같은 코드가 추가로 실행된다면, 반복되는 부분을 피호출 함수로 합쳐보자.

반복되는 부분에서 무언가 수정할 일이 생겼을 때 단 한곳만 수정하면 된다.

절차

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

예시

예시에서는 절차의 과정에 따라 변하는 과정을 보여주는데, 간단하여 리팩터링 전과 후의 코드를 보자.

리팩터링 전

function renderPerson(outStream, person) {
  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(p) {
  return ['<div>', `<p>제목: ${p.title}</p>`, emitPhotoData(p), '</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 renderPerson(outStream, 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(p) {
  return ['<div>', emitPhotoData(p), '</div>'].join('\n');
}

function emitPhotoData(aPhoto) {
  return [
    `<p>제목: ${person.photo.title}</p>`, // 안으로 가져옴
    `<p>위치: ${aPhoto.location}</p>`,
    `<p>날짜: ${aPhoto.date.toDateString()}</p>`,
  ].join('\n');
}

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

반대 리팩터링: 문장을 함수로 옮기기

배경

코드베이스의 기능 범위가 달라지면 추상화의 경계도 움직이게 된다.

이러한 경우 호출자와 호출 대상의 경계를 완전히 다시 정하고, 새로운 추상화를 위해 문장을 호출한 곳으로 리팩터링을 진행해보자.

절차

  1. 호출자가 한두 개 뿐이고 피호출 함수도 간단한 단순한 상황이면, 피호출 함수의 처음줄을 잘라내어 호출자로 복사해 넣는다. 테스트만 통과하면 이번 리팩터링은 여기서 끝이다.
  2. 더 복잡한 상황에서는, 이동하지 '않길' 원하는 모든 문장을 함수로 추출한 다음 검색하기 쉬운 임시 이름을 지어준다.
  3. 원래 함수를 인라인한다.
  4. 추출된 함수의 이름을 원래 함수의 이름으로 변경한다.

예시

listRecentPhotos()가 위치 정보를 다르게 렌더링하도록 만들어야 한다고 가정해보자.

리팩터링 전

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

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

리팩터링 후

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

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

배경

함수의 이름이 코드의 동작 방식보다는 목적을 말해주기 때문에 함수를 활용하면 코드를 이해하기가 쉬워진다.
→ 인라인된 코드보다 제대로 작성된 함수이름을 호출하는 편이 가독성이 좋다는 의미

절차

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

예시

책에 따로 존재 X

문장 슬라이드하기

배경

관련된 코드들이 가까이 모여 있다면 이해하기가 더 쉽다.

코드를 관련된 것끼리 묶자.

절차

  1. 코드 조각을 이동할 목표 위치를 찾는다. 코드 조각의 원래 위치와 목표 위치 사이의 코드들을 훑어보면서, 조각을 모으고 나면 동작이 달라지는 코드가 있는지 살핀다.
  2. 코드 조각을 원래 위치에서 잘라내어 목표 위치에 붙여 넣는다.
  3. 테스트한다.

예시

리팩터링 전

const pricingPlan = retrievePricingPlan();
const order = retrieveOrder();
let charge;
const chargePerUnit = pricingPlan.unit();

리팩터링 후

const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retrieveOrder();
let charge;

반복문 쪼개기

배경

하나의 반복문 안에서 두 가지 이상의 일을 수행하는 모습을 볼 수 있다. 그러나 반복문을 수정해야 할 때마다 두 가지 일 모두를 잘 이해하고 진행해야하기 때문에 반복문을 분리해보자.

반복문 쪼개기를 통해 최적화와 멀어지는 경우도 있지만, 다른 더 강력한 최적화를 적용할 수 있는 길을 열어주기도 한다.

절차

  1. 반복문을 복제해 두 개로 만든다.
  2. 반복문이 중복되어 생기는 부수효과를 파악해서 제거한다.
  3. 테스트한다.
  4. 완료됐으면, 각 반복문을 함수로 추출할지 고민해본다.

예시

리팩터링 전

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

리팩터링 후

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 `최연소: ${youngest}, 총 급여: ${totalSalary}`;

더 가다듬기 적용(함수 추출후 반복문을 파이프라인과 알고리즘 교체하기 적용)

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

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

return `최연소: ${youngestAge()}, 총 급여: ${totalSalary()}`;

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

배경

파이프라인을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다.
→ 반복문 대신 map, filter 등을 사용하는 리팩터링

절차

  1. 반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.
  2. 반복문의 첫 줄부터 시작해서, 각각의 단위 행위를 적절한 컬렉션 파이프라인 연산으로 대체한다. 이때 컬렉션 파이프라인 연산은 1에서 만든 반복문 컬렉션 변수에서 시작하여, 이전 연산의 결과를 기초로 연쇄적으로 수행된다. 하나를 대체할 때마다 테스트한다.
  3. 반복문의 모든 동작을 대체했다면 반복문 자체를 지운다.

예시

인도에 자리한 사무실을 찾아서 도시명과 전화번호를 반환하는 함수

리팩터링 전

function acquireDate(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;
}

리팩터링 후

function acquireDate(input) {
  const lines = input.split('\n');
  const result = lines
    .slice(1)
    .filter((line) => line.trim() !== '')
    .map((line) => line.split(','))
    .filter((record) => record[1].trim() === 'india')
    .map((record) => ({ city: record[0].trim(), phone: record[2].trim() }));

  return result;
}

죽은 코드 제거하기

배경

사용되지 않는 코드가 있다면 다른 누군가가 그 코드를 보았을 때, 무슨 의도로 그 코드가 작성되었는지 헷갈리기 때문에 지워버리자.

절차

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

예시

따로 존재 X

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글