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