본문 바로가기
핌팩토링

8. 기능 이동

by AsCE_hyunseung 2022. 4. 27.

8. 기능 이동

8.1 함수 옮기기

배경

  • 좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐를 뜻하는 모듈성 이다
  • 모듈성을 높이려면 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야 한다.
  • 어떤 함수가 자신이 속한 모듈 A의 요소들보다 다른 모듈 B의 요소들을 더 많이 참조한다면 모듈 B로 옮겨줘야 마땅하다 (캡슐화 측면에서, 의존성을 최대한 제거)

절차

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

예시

class Account {
    get overdraftCharge() {...}
}
-----------------------------------
class AccountType {
    get overdraftCharge() {...}
}

 

8.2 필드 옮기기

배경

  • 주어진 문제에 적합한 데이터 구조를 활용하면, 동작 코드는 자연스럽게 단순하고 직관적으로 짜여진다.
  • 데이터 구조가 이상하다 싶으면 바로 수정해야한다. 지금 수정 안하면 나중에 더 큰 스노우볼이 굴려질 수 있다.
  • 함수에 항상 함께 건네지는 데이터 조각들은 상호 관계가 명확하게 드러나도록 한 레코드에 담는 게 가장 좋다.

절차

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

예시

class Customer {
    get plan() {return this._plan}
    get discountRate() {return this._discountRate;}
}
--------------------------------
class Customer {
    get plan() {return this._plan}
    get discountRate() {return this._plan._discountRate;}
}

8.3 문장을 함수로 옮기기

배경

  • 중복 제거는 코드를 건강하게 관리하는 가장 효과적인 방법 중 하나다.
    • 추후 반복되는 부분에서 무언가 수정할 일이 생겼을 때 한 곳만 수정하면 되기 때문이다.

절차

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

예시

result.push(`제목: ${person.photo.title}`);
result.concat(photoData(person.photo));

function photoData(aPhoto) {
    return [
        `위치: ${aPhoto.location}`,
        `날짜: ${aPhoto.date.toDateString()}`
    ];
}
------------------------------

result.concat(photoData(person.photo));

function photoData(aPhoto) {
    return [
        `위치: ${aPhoto.location}`,
        `날짜: ${aPhoto.date.toDateString()}`,
        `제목: ${aPhoto.photo.title}`
    ];
}

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

배경

  • 함수는 프로그래머가 쌓아 올리는 추상화의 기본 빌딩 블록이다.
  • 추상화라는 것은 그 경계를 항상 올바르게 긋기가 어렵다. 그래서 코드베이스의 기능 범위가 달라지면 추상화의 경계도 움직이게 된다.
  • 작은 변경이라면 문장을 호출한 곳으로 옮기는 것으로 충분하지만, 호출자와 호출 대상의 경계를 완전히 다시 그어야 할 때도 있다.

절차

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

예시

emitPhotoData(outStream, person.photo);

function emitPhotoData(outStream, photo) {
    outStream.write(`제목: ${photo.title}`);
    outStream.write(`위치: ${photo.location}`);
}

-----------------------------------------

emitPhotoData(outStream, person.photo);
outStream.write(`위치: ${person.photo.location}`);

function emitPhotoData(outStream, photo) {
    outStream.write(`제목: ${photo.title}`);
}

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

배경

  • 함수는 여러 동작을 하나로 묶어준다.
  • 함수의 이름이 코드의 동작 방식보다는 목적을 말해주기 때문에 함수를 활용하면 코드를 이해하기가 쉬워진다.
  • 함수는 중복을 없애는 데도 효과적이다.

절차

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

예시

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

------------------------------
appliesToMass = state.includes("MA")

8.6 문장 슬라이드하기

배경

  • 관련된 코드들이 가까이 모여 있다면 이해하기가 더 쉽다.
  • 하나의 데이터 구조를 이용하는 문장들은 한데 모여 있어야 좋다.

절차

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

예시

  • 코드 조각을 슬라이드할 때는 두 가지를 확인해야한다.
    1. 무엇을 슬라이드 할지
    2. 슬라이드할 수 있는지
  • 코드 조각을 슬라이드하기로 했다면, 그 다음 단계로는 그 일이 실제로 가능한지를 점검해야 한다.
    • 슬라이드할 코드 자체와 그 코드가 건너뛰어야 할 코드를 모두 살펴야한다.
const pricingPlan = retrievePricingPlan();
const order = retrieveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;

----------------------------------------

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

 

8.7 반복문 쪼개기

배경

  • 두가지 일을 수행하는 하나의 반복문에서 수정이 필요할때, 각각의 반복문으로 분리해두면 수정할 동작 하나만 이해하면 된다.
  • 반복문을 분리하면 사용하기도 쉬워진다.
  • 반복문 쪼개기는 서로 다른 일들이 한 함수에서 이뤄지고 있다는 신호일 수 있고, 반복문 쪼개기와 함수 추출하기는 연이어 수행하는 일이 잦다

절차

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

예시

let averageAge = 0;
let totalSalary = 0;
for (const p of people) {
    averageAge += p.age;
    totalSalary += p.salary;
}
averageAge = averageAge / people.length;

----------------------------------

let totalSalary = 0;
for (const p of people) {
    totalSalary += p.salary;
}

let averageAge = 0;

for (const p of people) {
    averageAge += p.age;
}
averageAge = averageAge / people.length;

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

배경

  • 컬렉션 파이프라인을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다.
  • 논리를 파이프라인으로 표현하면 이해하기 훨씬 쉬워진다.
    • 객체가 파이프라인을 따라 흐르며 어떤 흐름으로 처리되는지 읽기 편해지기 떄문이다

절차

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

예시

const names = [];

for (const i of input) {
    if (i.job === "programmer")
        names.push(i.name)
}
--------------------------------------------------------

const names = input
    .filter(i => i.job === "programmer")
    .map(i => i.name)

8.9 죽은 코드 제거하기

배경

  • 최신 컴파일러들은 안쓰는 코드를 알아서 제거해주긴하지만, 소프트웨어 동작을 이해하는 데 걸림돌이 될 수 있으니 제거하는것이 좋다.

절차

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

'핌팩토링' 카테고리의 다른 글

10. 조건문 로직 간소화  (0) 2022.04.28
9. 데이터 조직화  (0) 2022.04.28
7. 캡슐화  (0) 2022.04.27
6. 기본적인 리팩터링  (0) 2022.03.26
4. 테스트 구축하기  (0) 2022.03.12