[Refactoring] Ch 8. 기능 이동

Bard·2021년 10월 22일
5

Refactoring Summary

목록 보기
1/1
post-thumbnail

8.1 함수 옮기기

좋은 소프트웨어 설계의 핵심은 모듈성이다.
어떤 함수가 자신이 속한 모듈 A보다 다른 모듈 B의 요소들을 더 많이 참조한다면 모듈 B로 옮겨줘야 마땅하다.
\Rarr 소프트웨어의 나머지 부분이 모듈 B의 세부사항에 덜 의존하게 된다.

함수를 옮길지 말지 결정하는 법
: 함수의 현재 컨텍스트와 후보 컨텍스트를 둘러보면 도움이 된다.

  • 대상 함수를 호출하는 함수는 무엇인지
  • 대상 함수가 호출하는 함수는 무엇인지
  • 대상 함수가 사용하는 데이터는 무엇인지

절차

  1. 선택한 함수가 현재 컨텍스트에서 사용 중인 모든 프로그램 요소를 살펴본다. 이 요소들 중에도 함께 옮겨야 할 게 있는지 고민해본다.
    호출되는 함수 중 함께 옮길 게 있다면 대체로 그 함수를 먼저 옮기는 게 낫다. 얽혀있는 함수가 여러 개라면 다른 곳에 미치는 영향이 적은 함수부터 옮기도록 하자.
    하위 함수들의 호출자가 고수준 함수 하나뿐이면 먼저 하위 함수들을 고수준 함수에 인라인한 다음, 고수준 함수를 옮기고, 옮긴 위치에서 개별 함수들로 다시 추출하자.
  2. 선택한 함수가 다형 메서드인지 확인한다.
    객체 지향 언어에서는 같은 메서드가 슈퍼클래스나 서브클래스에도 선언되어 있는지까지 고려해야 한다.
  3. 선택한 함수를 타깃 컨텍스트로 복사한다. (이 때, 원래의 함수를 소스함수source function라 하고 복사해서 만든 새로운 함수를 타깃 함수target function라 한다.) 타깃 함수가 새로운 터전에 잘 자리잡도록 다듬는다.
    함수 본문에서 소스 컨텍스트의 요소를 사용한다면 해당 요소들을 매개변수로 넘기거나 소스 컨텍스트 자체를 참조로 넘겨준다.
    함수를 옮기게 되면 새로운 컨텍스트에 어울리는 새로운 이름으로 바꿔줘야 할 경우가 많다. 필요하면 바꿔준다.
  4. 정적 분석을 수행한다.
  5. 소스 컨텍스트에서 타깃 함수를 참조할 방법을 찾아 반영한다.
  6. 소스 함수를 타깃 함수의 위임 함수가 되도록 수정한다.
  7. 테스트한다.
  8. 소스 함수를 인라인할지 고민해본다.
    소스 함수는 언제까지라도 위임 함수로 남겨둘 수 있다. 하지만 소스 함수를 호출하는 곳에서 타깃 함수를 직접 호출하는 데 무리가 없다면 중간 단계(소스 함수)는 제거하는 편이 낫다.

Before 😥

class Account {
    get overdraftCharge() {
    // ...
    }
}

After 😀

class Account {
    get overdraftCharge() {
    return this.type.overdraftCharge()
    }
}

class AccountType {
    get overdraftCharge() {
    // ...
    }
}

8.2 필드 옮기기

한 레코드를 변경하려 할 때 다른 레코드의 필드까지 변경해야만 한다면 필드의 위치가 잘못되었다는 신호다.
\Rarr 구조체 여러 개에 정의된 똑같은 필드들을 갱신해야 한다면 한 번만 갱신해도 되는 다른 위치로 옮기라는 신호다.

이어지는 설명에서는 클래스를 사용한다고 가정한다.

절차

  1. 소스 필드가 캡슐화되어 있지 않다면 캡슐화한다.
  2. 테스트한다.
  3. 타깃 객체에 필드를 생성한다.
  4. 정적 검사를 수행한다.
  5. 소스 객체에서 타깃 객체를 참조할 수 있는지 확인한다.
    기존 필드나 메서드 중 타깃 객체를 넘겨주는 게 있을지 모른다. 없다면 이런 기능의 메서드를 쉽게 만들 수 있는지 살펴본다. 간단치 않다면 타깃 객체를 저장할 새 필드를 소스 객체에 생성하자.
    이는 영구적인 변경이 되겠지만, 더 넒은 맥락에서 리팩터링을 충분히 하고 나면 다시 없앨 수 있을 때도 있다.
  6. 접근자들이 타깃 필드를 사용하도록 수정한다.
    여러 소스에서 같은 타깃을 공유한다면, 먼저 세터를 수정하여 타깃 필드와 소스 필드 모두를 갱신하게 하고, 이어서 일관성을 깨뜨리는 갱신을 검출할 수 있도록 어서션을 추가하자. 모든 게 잘 마무리되었다면 접근자들이 타깃 필드를 사용하도록 수정한다.
  7. 테스트한다.
  8. 소스 필드를 제거한다.
  9. 테스트한다.

Before 😥

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

    get discountRate() {
        return this._discountRate
    }
}

After 😀

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

    get discountRate() {
        return this.plan.discountRate
    }
}

날 레코드 변경하기

이 리팩터링은 대체로 객체를 활용할 때가 더 수월하다. 캡슐화 덕에 데이터 접근을 메서드로 자연스럽게 감싸주기 때문이다. 반면, 여러 함수가 날 레코드를 직접 사용하는 경우라면 이 리팩터링은 훨씬 까다롭다.

이럴 때는 접근자 함수들을 만들고, 날 레코드를 읽고 쓰는 모든 함수가 접근자를 거치도록 고치면 된다. 옮길 필드가 불변이라면 값을 처음 설정할 때 소스와 타깃 필드를 한꺼번에 갱신하게 하고, 읽기 함수들은 점진적으로 마이그레이션하자.

8.3 문장을 함수로 옮기기

문장들을 옮기려면 피호출 함수의 일부라는 확신이 있어야 한다.

피호출 함수와 한 몸은 아니지만 여전히 함께 호출돼야 하는 경우라면 단순히 해당 문장들과 피호출 함수를 통째로 또 하나의 함수로 추출한다.
이 방법도 절차는 똑같다. 단 마지막 인라인과 이름 바꾸기 단계만 제외하면 된다.

절차

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

Before 😥

result.push(`<p>제목: ${person.photo.title}</p>`)
result.concat(photoData(person.photo))

function photoData(aPhoto) {
    return [
    `<p>위치: ${aPhoto.location}</p>`,
    `<p>날짜: ${aPhoto.date.toDateString()}</p>`,
    `<p>태그: ${aPhoto.tag}</p>`,
    ]
}

After 😀

result.concat(photoData(person.photo))

function photoData(aPhoto) {
    return [
    `<p>제목: ${aPhoto.title}</p>`,
    `<p>위치: ${aPhoto.location}</p>`,
    `<p>날짜: ${aPhoto.date.toDateString()}</p>`,
    `<p>태그: ${aPhoto.tag}</p>`,
    ]
}

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

8.3의 반대 리팩터링

여러 곳에서 사용하던 기능이 일부 호출자에게는 다르게 동작하도록 바뀌어야 한다면 함수가 어느 새 둘 이상의 다른 일을 수행하게 바뀔 수 있다.

그렇다면 개발자는 달라진 동작을 함수에서 꺼내 해당 호출자로 옮겨야 한다. 이런 상황에 맞닥뜨리면 우선 문장 슬라이드하기를 적용해 달라지는 동작을 함수의 시작 혹은 끝으로 옮긴 다음, 바로 이어서 문장을 호출한 곳으로 옮기기 리팩터링을 적용하면 된다.

절차

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

Before 😥

emitPhotoData(outStream, person.photo)

function emitPhotoData(outStream, photo) {
    outStream.write(`<p>제목: ${photo.title}</p>\n`)
    outStream.write(`<p>위치: ${photo.location}</p>\n`)
}

After 😀

emitPhotoData(outStream, person.photo)
outStream.write(`<p>위치: ${person.photo.location}</p>\n`)

function emitPhotoData(outStream, photo) {
    outStream.write(`<p>제목: ${photo.title}</p>\n`)
}

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

똑같은 코드를 반복하는 대신 함수 호출로 변경하는 방법이다.
해당 기능을 하는 함수가 이미 존재한다면 이 방법을 사용하면 된다.

동일한 목적의 같은 코드가 반복 사용되고, 해당 목적의 함수가 존재할 때 같은 코드라도 만약 인라인 코드와 목적이 다르다면 이 방법을 사용하면 안된다.

절차

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

Before 😥

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

After 😀

appliesToMass = states.includes('MA')

8.6 문장 슬라이드하기

서로 관련있는 코드가 흩어져있을 때, 한 곳으로 모으는 리팩터링이다.

절차

  1. 코드 조각(문장들)을 이동할 목표 위치를 찾는다. 코드 조각의 원래 위치와 목표 위치 사이의 코드들을 훑어보면서, 조각을 모으고 나면 동작이 달라지는 코드가 있는지 살핀다. 다음과 같은 간섭이 있다면 이 리팩토링을 포기한다.
    코드 조각에서 참조하는 요소를 선언하는 문장 앞으로는 이동할 수 없다.
    코드 조각을 참조하는 요소의 뒤로는 이동할 수 없다.
    코드 조각에서 참조하는 요소를 수정하는 문장을 건너뛰어 이동할 수 없다.
    코드 조각이 수정하는 요소를 참조하는 요소를 건너뛰어 이동할 수 없다.
  2. 코드 조각을 원래 위치에서 잘라내어 목표 위치에 붙여 넣는다.
  3. 테스트한다.

Before 😥

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

After 😀

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

8.7 반복문 쪼개기

반복문을 하나의 일만 하는 각각의 반복문으로 쪼개는 리팩터링

만약 이게 병목이라 밝혀지더라도 다시 합치기는 식은 죽 먹기이다.
게다가 그런 경우는 매우 드물고, 반복문 쪼개기는 더 강력한 최적화를 적용할 수 있는 길을 열어준다.

절차

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

Before 😥

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

After 😀

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. 반복문의 모든 동작을 대체했다면 반복문 자체를 지운다.
    반복문이 결과를 누적 변수에 대입했다면 파이프라인의 결과를 그 누적 변수에 대입한다.

Before 😥

const names = []
for (const person of input) {
    if (person.job === 'programer') {
    names.push(person.name)
    }
}

After 😀

const names = input
.filter((person) => person.job === 'programer')
.map((person) => person.name)

8.9 죽은 코드 제거하기

사용하지 않는 코드는 제거한다. 남겨놓으면 복잡하며 나중에 다시 사용하게 된다면 버전 관리 시스템을 이용한다.

절차

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

Before 😥

if(false) {
  doSomethingThatUsedToMatter();
}

After 😀

profile
The Wandering Caretaker

0개의 댓글