[Refactoring] 리팩터링 2판 Chapter.06 part 2 (기본적인 리팩터링)
카테고리: Refactoring
변수 추출하기
어떤 수치를 계산하는 코드가 있다고 생각해 봅시다. 왼쪽의 변수와 오른쪽의 변수를 더하는 기본적인 상황에서는 해당 식이 어떤 문백인지 파악하기가 쉽지만, 계산식이 늘어날수록 그 식의 의도를 파악하기에는 어려워질것입니다. 시간이 지나거나, 다른사람이 코드를 수정할때가 되면 더욱 힘들어 지겠죠.
해당 문제를 해결하기 위해, 지역변수
를 사용합니다. 지역 변수를 활용하면 표현식을 쪼개서 관리하기 더 쉽게 만들 수 있습니다.
이과정에서 추가한 변수는 디버깅에도 유용합니다. 가령 다음과 같은 코드가 있다고 해봅시다. (예시로 짠 코드라서 저도 무슨 역할을 하는지는 모릅니다)
public int SomeCalculate()
{
return price * quantity - fee + peopleCount * month;
}
해당 코드를 작성하고 중단점을 걸고 디버깅을 하면, 저 결과를 볼 수는 없고, 각 변수에 어떤 값이 담기는지만 확인할 수 있습니다. 그래서 저는 임시방편으로 디버깅을 위한 지역변수를 만들어 디버깅 합니다.
public int SomeCalculate()
{
int a = price * quantity - fee + peopleCount * month;
return price * quantity - fee + peopleCount * month;
}
이렇게 해서 디버깅을 하고 임시로 만든 지역변수를 지웁니다. 책을 읽고나니 이렇게할 필요가 없이 더 알아보기 쉽게 지역변수를 만들어 지역변수를 반환하도록 하면 이점이 많다고 느껴지네요.
여기서 만약, 특정 함수에서만 필요한 값이라면, 함수 안에서의 지역변수
로 추출하여 사용하고, 다른곳에서도 사용한다고 하면, 함수 추출하기
를 통해 함수를 만들어 사용하도록 합니다.
절차
- 추출하려는 표현식에 부작용은 없는지 확인한다.
- 불변 변수를 하나 선언하고 이름을 붙일 표현식의 복제본을 대입한다.
- 원본 표현식을 새로 만든 변수로 교체한다.
- 테스트한다.
- 표현식을 여러 곳에서 사용한다면 각각을 새로 만든 변수로 교체한다. 하나 교체할 때마다 테스트 한다.
예시
function price(order) {
return order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100);
}
해당 함수를 보면 벌써부터 뭐가 뭐하는 식인지 알기가 힘듭니다. 해당 함수안에서 변수 추출하기
를 통해 다음과 같이 만들어 줍니다.
function price(order) {
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
이렇게 특정 계산에 변수를 추출하여 어떤 계산인지 명확하게 적어놓으니 훨씬 보기 좋아졌습니다. 그리고 어떤 부분에서 문제가 일어나는지도 명확하게 디버깅할 수 있게 되었습니다.
변수 인라인 하기
위에서 살펴본 변수 추출하기가 만능은 아닙니다. 대체로 긍정적인 효과를 주지만, 그 이름이 원래 표현식과 다를 바 없을 때도 있습니다. 또 변수가 주변 코드를 리팩터링하는데 방해가 되기도 합니다. 그때는 변수를 인라인 합니다.
절차
- 대입문의 우변(표현식)에서 부작용이 생기지는 않는지 확인한다.
- 변수가 불변으로 선언되지 않았다면 불변으로 만든 후 테스트한다. (변수에 값이 한번만 들어가는지 볼 수 있다.)
- 테스트한다.
- 변수를 사용하는 부분을 모두 교체할 때까지 이 과정을 반복한다.
- 변수 선언문과 대입문을 지운다.
- 테스트한다.
예시
다음과 같이 한눈에 의도를 파악할 수 있는 경우 굳이 지역변수를 사용하지 않고 변수를 인라인 합니다.
let price = 1000;
let money = 2000;
function canPurchase() {
const canPurchase = price < money;
return canPurchase;
}
let price = 1000;
let money = 2000;
function canPurchase() {
return price < money;
}
함수 선언 바꾸기
함수는 프로그램을 작은 부분으로 나누는 주된 수단 입니다. 함수 선언은 각 부분이 서로 맞물리는 방식을 표현하며, 실직적으로 소프트웨어 시스템의 구성 요소를 조립하는 연결부 역할을 합니다.
여기서 중요한것은 함수의 이름
입니다. 이름이 좋고, 이름에 신뢰성이 생긴다면, 흔히 쓰는 .ToString()
처럼 내부의 구현을 몰라도 무슨일을 하는 함수인지 단번에 알 수 있습니다.
하지만 여러 프로그래머들의 고민이 이름짓기
인 만큼, 좋은 이름을 단번에 짓기란 굉장히 힘듭니다. 그래서, 일단 지금 생각난 이름중에 가장 적합한 것을 적용하고, 다시 읽을 때 그 이름이 어색하거나 더 좋은 이름이 생각난다면 함수의 이름을 바꾸는 절차를 거쳐야 합니다.
그래야 나중에 그 코드를 다시 볼 때 무슨 일을 하는지 또
고민하지 않게 됩니다. (저도 제가 옛날에 짠 코드를 보면 그 함수의 로직을 다시 이해해야 해서 안보게 되는것 같습니다.)
함수의 이름과 마찬가지로 함수의 매개변수
도 마찬가지 입니다. 항상 함수를 만들 때, 이름 다음으로 고민되는것은 어디까지 매개변수로 받아야 하고, 어떤걸 매개변수로 받아야 하지? 입니다.
가령 다음과 같은 상황이 생각납니다.
public class Main
{
private List<Item> itemList = new List<Item>();
private int maxPrice = 1000;
//매개변수가 없는 함수
private List<Item> GetBuyableItems()
{
return itemList.Where(item => item.Price > maxPrice).ToList();
}
//매개변수가 있는 함수
private List<Item> GetBuableItems(List<Item> _itemList)
{
return _itemList.Where(item => item.Price > maxPrice).ToList();
}
}
물론 멤버변수 이고, 함수를 private
하게 사용하니까 그냥 매개변수를 안써도 되지 않냐 라고 할 수 있지만, 클래스 내부에서도 itemList 에 의존성이 생기는거 같아 고민이 되었습니다. (일단 지금은 매개변수 없이 사용하고 있습니다.)
이 상황과 같은 상황인지 모르겠지만 저자가 말하는 매개변수를 넣어야 하는 타이밍은 정답이 없다.
입니다. 나중에 필요하면 필요에 맞게 매개변수를 수정해라 라고 답을 합니다. 역시 한번에 미래를 예측하여 완벽한 코드를 짜는것 보다 코드를 보며 악취가 날때마다 그 악취를 없애는게 중요한것 같습니다.
절차
2가지 절차가 있습니다. 단순히 이름만 바꾸는 단순한 절차
와 상속단계에서 생기는 문제를 해결하고자 만든 마이그레이션 절차
두가지가 있습니다. 상속관계에서 서브클래스의 이름을 바꾸려면 부모클래스의 정의도 바꿔야 하기 때문에 이러한 방법을 사용하는것 같습니다.
단순한 절차
- 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인한다.
- 메서드 선언을 원하는 형태로 바꾼다.
- 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.
- 테스트한다.
마이그레이션 절차
- 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링한다.
- 함수 본문을 새로운 함수로 추출한다.
- 추출한 함수에 매개변수를 추가해야 한다면
간단한 절차
를 따라 추가한다. - 테스트한다.
- 기존 함수를 인라인 한다.
- 이름을 임시로 붙여뒀다면 함수 선언 바꾸기를 한 번 더 적용해서 원래 이름으로 되돌린다.
- 테스트한다.
예시
간단한 절차
사각형의 한 변의 길이를 가져오는 함수를 정의 합니다. 아래의 예시는 함수 이름을 너무 축약한 경우 입니다.
function line() {
return getLine();
}
이것을 좀더 구체적으로 설명하는 이름이 필요 합니다.
function getSquareLine(){
return getLine();
만약에, 해당 line()
함수가 Shape
라는 부모 클래스의 함수를 상속받아 재정의 하는거라면, 이름을 바꿀 때 부모클래스에서도 바꿔주고, 해당 함수를 상속받는 모든 클래스에 가서 이름을 바꿔줘야 합니다.
해당 작업이 필요할때가 있지만, 필요가 없을 때, 마이그레이션 절차를 수행 합니다.
마이그레이션 절차
line() 을 상속받아 재정의된 함수라고 생각하고, 함수를 하나 더 만들어서 해당 함수를 감싸줍니다.
function line() {
return getLine();
}
function getSquareLine() {
return line();
}
댓글 남기기