Skip to content

도서 리뷰 시리즈 - 리팩토링

출처

『리팩토링』 마틴 파울러 저 / 김지원 역 | 한빛미디어 | 2012년 11월

한 줄 리뷰

리팩토링의 모든 것을 알려주는 도서

리팩토링 이란

리팩토링은 겉으로 드러나는 기능은 그대로 둔 채, 알아보기 쉽고 수정하기 간편하게 소프트웨어 내부를 수정하는 작업이다. 소프트웨어를 더 이해하기 쉽고 수정하기 쉽게 만들어 겉으로 드러나는 기능에 거의 또는 아예 영향을 주지 않으면서 소프트웨어의 각종 기능을 추가할 수 있다. 리팩토링 수행 후에 겉으로 드러나는 기능에 영향을 주지 않기 때문에 사용자는 소프트웨어의 변화를 눈치채지 못한다.

리팩토링은 성능 최적화와 상반되는 데, 같은 점은 수행 후에 기능이 변경되지 않는 것이다. 다른 점은 성능 최적화는 성능 향상을 위해 불가피하게 필요한 성능을 내기 위해 코드를 파악하기 더 어려워질 때가 많다.

리팩토링과 기능 추가는 동시에 진행되면 안된다. 동시에 진행하게 된다면 이슈 발생시 리팩토링으로 인한 이슈인지 기능상의 이슈인지 파악하기 어렵기 때문이다. 기능 추가시에 테스트 코드를 같이 추가하여 추후에 리팩토링 시 기능상의 이슈가 발생하지 않도록 해야 한다.

리팩토링의 결과

  1. 소프트웨어 설계 개선 단기적인 목적 때문에 코드를 수정하거나 코드의 설계를 완벽히 이해하지 않고 코드를 수정하면, 코드 구조가 뒤죽박죽되어 그 코드를 보고 설계를 파악하기가 어려워져 프로그램 설계가 점점 노후 된다. 정기적으로 리팩토링을 실시하면 코드 설계가 깔끔해진다.

  2. 소프트웨어 이해 쉬워짐 기능을 추가하면서 설계한 것들을 모두 기억할 수 없기 때문에 코드를 깔끔하게 만들지 않으면 복잡한 내용을 이해할 수 없다.

  3. 버그 찾기 쉬워짐 코드 리팩토링하면 구조가 명료하게 만들어서 디버그 시 쉽게 버그를 찾을 수 있다.

  4. 프로그래밍 속도 빨라짐 리팩토링을 하지 않으면 소프트웨어 개발이 진행되면서 개발 속도가 현저히 줄어들게 된다. 설계가 정돈되지 않으면 기능 추가 시 시간이 오래 걸릴 수 밖에 없으며, 버그 찾기에 많은 시간을 낭비하게 된다. 프로그래밍 속도를 빠르게 하려면 깔끔한 설계를 유지해 설계가 노후화되지 않게 해야 한다.

메서드 정리

이 코드는 리팩터링에서 소개된 자바를 자바스크립트로 변경한 코드입니다. 좀 더 유용한 예제는 자바스크립트로 예제를 작성한 리팩터링 2판에서 볼 수 있습니다.

메서드 추출(Extract Method)

어떤 코드를 그룹으로 묶어도 되겠다고 판단될 때

javascript
// Before
const printOwing = (amount) => {
  printBanner()
  console.log(`name:${_name}`)
  console.log(`amount:${amount}`)
}

// After
const printOwing = (amount) => {
  printBanner()
  printDetails(amount)
}
const printDetails = (amount) => {
  console.log(`name:${_name}`)
  console.log(`amount:${amount}`)
}

메서드 내용 직접 삽입(Inline Method)

매서드 기능이 너무 단순해서 메서드명만 봐도 너무 뻔할 때

javascript
// Before
const moreThanFiveLateDeliveries = () => {
  return _numberOfLateDeliveries > 5
}
const getRating = () => {
  return (moreThanFiveLateDeliveries()) ? 2 : 1
}

// After
const getRating = () => {
  return (_numberOfLateDeliveries > 5) ? 2 : 1
}

임시변수 내용 직접 삽입(Inline Temp)

간단한 수식을 대입받는 임시변수로 인해 다른 리팩토링 기법 적용이 힘들 때

javascript
// Before
const basePrice = anOrder.basePrice()
return basePrice > 1000

// After
return anOrder.basePrice() > 1000

임시변수를 메서드 호출로 변환(Replace Temp with Query)

수식의 결과를 저장하는 임시변수가 있을 때

javascript
// Before
const basePrice = _quantity * _itemPrice
if (basePrice > 1000) {
  return basePrice * 0.95
} else {
  return basePrice * 0.98
}

// After
const basePrice = () => _quantity * _itemPrice
if (basePrice() > 1000) {
  return basePrice() * 0.95
} else {
  return basePrice() * 0.98
}

직관적 임시변수 사용(Introduce Explaining Variable)

사용된 수식이 복잡할 때

javascript
// Before
if (
  platform.toUpperCase().indexOf('MAC') > -1 &&
  browser.toUpperCase().indexOf("IE") > -1 &&
  wasInitialized() && resize > 0 ) {
  // 기능 코드  
}

// After
const isMacOs = platform.toUpperCase().indexOf('MAC') > -1
const isIEBrowser = browser.toUpperCase().indexOf("IE") > -1 
const wasResized = resize > 0
if ( isMacOs && isIEBrowser && wasInitialized() && wasResized ) {
  // 기능 코드  
}

임시변수 분리(Split Temporary Variable)

루프 변수나 값 누적용 임시변수가 아닌 임시변수에 여러 번 값이 대입될 때

javascript
// Before
let temp = 2 * (_height + _width)
console.log(temp)
temp = _height * _width
console.log(temp)

// After
const perimeter = 2 * (_height + _width)
console.log(perimeter)
const area = _height * _width
console.log(area)

매개변수로의 값 대입 제거(Remove Assignments to Parameters)

매개변수로 값을 대입하는 코드가 있을 때

javascript
// Before
const discount = (inputVal, quantity, yearToDate) => {
  if (inputVal > 50) {
    inputVal -= 2
  }
}

// After
const discount = (inputVal, quantity, yearToDate) => {
  let result = inputVal
  if (inputVal > 50) {
    result -= 2
  }
}

메서드를 메서드 객체로 전환(Replace Method with Method Object)

지역변수 때문에 메서드 추출을 적용할 수 없는 긴 메서드가 있을 때

그 메서드 자체를 객체로 전환해서 모든 지역변수를 객체의 필드로 만들자. 그런 다음 그 메서드를 객체 안의 여러 메서드로 쪼개면 된다.

javascript
// Before
class Account {
  gamma (inputVal, quantity, yearToDate) {
    const val1 = (inputVal * quantity) + delta()
    let val2 = (inputVal * yearToDate) + 100
    if ( (yearToDate - val1) > 100) {
      val2 -= 20
    }
    const val3 = val2 * 7
    
    return val3 - 2 * val1
  }
}

// After
class Gamma {
  constructor (source, inputVal, quantity, yearToDate) {
    this.source = source
    this.inputVal = inputVal
    this.quantity = quantity
    this.yearToDate = yearToDate
  }
  compute () {
    const val1 = (this.inputVal * this.quantity) + this.source.delta()
    let val2 = (this.inputVal * this.yearToDate) + 100
    if ( (this.yearToDate - val1) > 100) {
      val2 -= 20
    }
    const val3 = val2 * 7
    
    return val3 - 2 * val1
  }
}

class Account {
  gamma (inputVal, quantity, yearToDate) {
    return new Gamma(this, inputVal, quantity, yearToDate).compute()
  }
}

알고리즘 전환(Substitute Algorithm)

알고리즘을 더 분명한 것으로 교체해야 할 때

javascript
// Before
const foundPerson = (people) => {
  for (let i = 0; i < people.length; i++) {
    if (people[i] === 'Don') {
      return 'Don'
    }
    if (people[i] === 'John') {
      return 'John'
    }
    if (people[i] === 'Kent') {
      return 'Kent'
    }
  }
  return ''
}

// After
const foundPerson = (people) => {
  const candidates = ['Don', 'John', 'Kent']
  for (let i = 0; i < people.length; i++) {
    if (candidates.contains(people[i])) {
      return people[i]
    }
  }
  return ''
}