리팩터링 2판
- [명사] 소프트웨어의 겉보기 동작은 그대로 유지한채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법
- [동사] 소프트웨어의 겉보기 동작은 그대로 유지한채, 여러가지 리팩터링 기법을 적용해서 소프트웨어를 재구성하다.
전체 작업 시간이 10분이라도 이렇게 한다.
- 기능 추가 모자
- 기존 코드는 절대 건드리지 않고 새 기능을 추가
- 진척도는 테스트를 추가해서 통과하는지 확인
- 리팩터링 모자
- 기능추가는 절대 하지 않기도 다짐한 뒤 오로지 코드 재구성에만 전념한다.
- 테스트도 새로 만들지 않는다. (놓친 케이스를 발견하지 않은한)
- 부득이 인터페이스를 변경해야 할 때 기존 테스트를 수정한다.
리팩터링하지 않으면 소프트웨어의 내부 설계(아키텍처)가 썩기 쉽다. 아키텍처를 충분히 이해하지 못한채 단기 목표만을 위해 코드를 수정하다 보면 기반 구조가 무너지기 쉽다. 그러면 코드만 봐서는 설계를 파악하기 어려워진다.
한 시스템을 오래 개발 중인 개발자들과 얘기하다 보면 초기에는 진척이 빨랐지만 현재는 새 기능을 하나 추가하는 데 훨씬 오래 걸린다는 말을 많이 한다. 새로운 기능을 추가할수록 기존 코드베이스에 잘 녹여낼 방법을 찾는 데 드는 시간이 늘어난다는 것이다. 기능을 추가하고 나면 버그가 발생하는 일이 잦고, 해결하는 시간이 더 걸리고, 패치에 패치가 덧붙여지면서 프로그램의 동작을 파악하기가 더욱 어려워진다.
내부 설계가 잘 된 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있다.
내부 설계에 심혈을 기울이면 소프트웨어의 지구력이 높아져서 빠르게 개발할 수 있는 상태를 더 오래 지속할 수 있다. 수많은 뛰어난 프로그래머들의 경험이 이를 뒷받침하지만 증명할 수 없어서 '가설'이라고 표현한다.
비슷한 일을 세 번째 하게 되면 리팩터링한다.
기능을 추가하기 전에 구조를 살짝 바꾸면 다른 작업을 하기가 훨씬 쉬워질 만한 부분을 찾아 리팩터링한다.
코드를 분석할 때 리팩터링을 해보면, 그렇지 않았더라면 도달하지 못했을 더 깊은 수준까지 이해하게 된다.
제곧내
데이터 항목 서너 개가 여러 곳에서 항상 함께 뭉쳐 다니는 모습을 흔히 목격할 수 있다. 클래스로 만들어 클래스로 옮기면 좋을 동작은 없는지 살핀다. 향후 개발을 가속하는 유용한 클래스를 탄생시킬 수 있다.
주어진 문제에 딱 맞는 기초 타입(화페, 좌표, 구간 등)을 직접 정의하기를 꺼려해서 이런 자료형들을 문자열로만 표현하는 악취를 의미한다. 기본형을 객체로 바꾸거나 클래스로 추출해서 문명사회로 이끌어줄 수 있다.
// AS-IS
function base(aReading) {...}
function taxableCharge(aReading) {...}
function calculateBaseCharge(aReading) {...}
// TO-BE
class Reading {
base() {...}
taxableCharge() {...}
calculateBaseCharge() {...}
}
공통 데이터를 중심으로 긴밀하게 엮여 작동하는 함수 무리를 발견하면 클래스로 묶는다. 클래스는 객체 지향 언어의 기본인 동시에 다른 패러다임 언어에도 유용하다.
// AS-IS
function base(aReading) {...}
function taxableCharge(aReading) {...}
// TO-BE
function enrichReading(argReading) {
const aReading = _.cloneDepp(argReading)
aReading.baseCharge = base(aReading)
aReading.taxableCharge = taxableCharge(aReading)
return aReading
}
소프트웨어는 데이터를 입력받아서 여러 가지 정보를 도출하곤 한다. 이렇게 도출된 정보는 여러 곳에서 사용될 수 있는 데, 그러다 보면 이 정보가 사용되는 곳마다 같은 도출 로직이 반복되기도 한다.
작업들을 변환 함수로 모아서 로직 중복을 막을 수 있다. 변환 함수는 원본 데이터를 입력받아서 필요한 정보를 모두 도출한 뒤, 각각을 출력 데이터의 필드에 넣어 반환한다.
// AS-IS
const orderData = orderString.split(/\s+/)
const productPrice = priceList[orderData[0].split("-")[1]]
const orderPrice = parseInt(orderData[1]) * productPrice
// TO-BE
const orderRecord = parseOrder(order)
const orderPrice = price(orderRecord, priceList)
function parseOrder(aString){
const values = aString.split(/\s+/)
return ({
productIDL values[0].split("-")[1],
quantity: parseInt(values[1])
})
}
function price(order, priceList){
return order.qauntity * priceList[order.productID]
}
코드를 수정해야 할 때 두 대상을 동시에 생각할 필요 없이 하나에만 집중하기 위해 서로 다른 두 대상을 한꺼번에 다루는 코드를 발견하면 각각을 별개 모듈로 나눈다.