기본적인 리팩터링

niyu·2022년 1월 12일

리팩터링 2판

목록 보기
5/11
post-thumbnail

앞으로 나열할 리팩터링 기법들은 가장 기본적이고 많이 사용하는 리팩터링들이다. 그중에서도 가장 많이 사용하는 리팩터링인 경우에는 제목 옆에 별기호(⭐)를 추가해놨다.


함수 추출하기 ⭐

example-code1

코드 조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙인다.

📜 절차

  1. 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙인다.
  2. 추출할 코드를 원본 함수에서 복사해 새 함수에 붙여넣는다.
  3. 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사하고, 있다면 매개변수로 전달한다.
  4. 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꾼다.
  5. 다른 코드에 방금 추출한 것과 똑같거나 비슷한 코드가 없는지 살펴, 있다면 방금 추출한 새 함수를 호출하도록 바꿀지 검토한다.

📚 유효범위를 벗어나는 변수가 없을 때
해당 코드를 잘라내 새 함수에 붙이고, 원래 자리에 새 함수 호출문을 넣는다.

example-code1-1

📚 지역 변수를 사용할 때
지역 변수들을 매개변수로 넘긴다.

example-code1-2

📚 지역 변수의 값을 변경할 때
그 지역 변수를 쪼개서 임시 변수를 새로 하나 만들어 그 변수에 대입하게 한다.

example-code1-3

💻 함수 추출하기 코드


함수 인라인하기

example-code2

함수 본문이 이름만큼 명확한 경우일 때는 그 함수를 제거한다. 쓸데없는 간접 호출은 하지 않는다.

📜 절차

  1. 다형 메서드인지 확인한다. 서브클래스에서 오버라이드하는 메서드는 인라인 대상이 아니다.
  2. 인라인할 함수를 호출하는 곳을 모두 찾아, 각 호출문을 함수 본문으로 교체한다.
  3. 원래 함수를 제거한다.

💻 함수 인라인 하기 코드


변수 추출하기 ⭐

example-code3

표현식이 너무 복잡한 경우 지역 변수를 활용하여 표현식을 쪼개, 관리하기 쉽도록 한다.

📜 절차

  1. 추출하려는 표현식에 부작용은 없는지 확인한다.
  2. 불변 변수를 하나 선언하고 이름을 붙일 표현식의 복제본을 대입한다.
  3. 원본 표현식을 새로 만든 변수로 교체한다.
  4. 표현식을 여러 곳에서 사용한다면 각각을 새로 만든 변수로 교체한다.

함수 안에서만 의미가 있다면 변수로 추출하고, 함수를 벗어난 넒은 문맥에서까지 의미가 된다면 변수가 아닌 함수로 추출한다.


💻 변수 추출하기 코드


변수 인라인하기

example-code4

변수가 원래 표현식과 다를 바가 없을 경우 해당 변수를 인라인하도록 한다.

📜 절차

  1. 대입문의 우변(표현식)에서 부작용이 생기진 않는지 확인한다.
  2. 변수가 불변으로 선언되지 않았다면 불변으로 만든다.
  3. 이 변수를 가장 처음 사용하는 코드를 찾아 대입문의 우변의 코드로 바꾼다.
  4. 변수를 사용하는 부분을 모두 교체할 때까지 이 과정을 반복한다.
  5. 변수 선언문과 대입문을 지운다.

💻 변수 인라인하기 코드


함수 선언 바꾸기

example-code5

함수의 이름이 좋으면 함수의 구현 코드를 살펴볼 필요 없이 호출문만 보고도 무슨 일을 하는지 파악할 수 있다.

💻 함수 선언 바꾸기 코드


변수 캡슐화하기

example-code6

데이터로의 접근을 독점하는 함수를 만들어, 그 함수를 통해서만 접근할 수 있도록 한다.

데이터는 참조하는 모든 부분을 한 번에 바꿔야 코드가 제대로 작동한다. 데이터의 유효 범위가 넓어질수록 다루기 어렵기 때문에 데이터의 유효범위가 넓을수록 반드시 캡슐화해야 한다.

📜 절차

  1. 변수로의 접근과 갱신을 담당하는 캡슐화 함수들을 만든다.
  2. 변수를 직접 참조하던 부분을 모두 캡슐화 함수 호출로 바꾼다.
  3. 변수 값이 레코드라면 레코드 캡슐화하기를 적용할지 고려해본다.

게터의 경우 데이터의 복제본을 반환하도록 수정하여, 원본을 변경해서 발생할 사고를 미연에 방지하도록 한다.


💻 변수 캡슐화하기 코드


변수 이름 바꾸기

example-code7

명확한 프로그래밍의 핵심은 이름짓기다.

📜 절차

  1. 폭넓게 쓰이는 변수라면 변수 캡슐화하기를 고려한다.
  2. 이름을 바꿀 변수를 참조하는 곳을 모두 찾아서, 하나씩 변경한다.

💻 변수 이름 바꾸기 코드


매개변수 객체 만들기

example-code8

데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해지고, 같은 데이터 구조를 사용하는 모든 함수가 원소를 참조할 때 항상 똑같은 이름을 사용하기 때문에 일관성도 높아진다.

📜 절차

  1. 적당한 데이터 구조가 아직 없다면 새로 만든다.
  2. 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가한다.
  3. 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다.
  4. 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾸고 기존 매개변수를 제거한다.

다음의 예제는 온도 측정값 증 정상 작동 범위를 벗어난 것이 있는지 검사하는 코드다. 코드를 살펴봤을 때 min, max 매개변수는 범위라는 하나의 객체로 묶어서 리팩토링할 수 있다.

example-code8-1

NumberRange 클래스를 선언해 데이터를 묶는다. 객체가 아닌 클래스로 선언한 이유는 해당 객체로부터 관련 동작까지 클래스 안에서 작성할 수 있기 때문이다.

example-code8-2

이렇게 온도가 허용 범위 안에 있는지 검사하는 메서드를 클래스에 추가할 수 있다.

example-code8-3

💻 매개변수 객체 만들기 코드


여러 함수를 클래스로 묶기

example-code9

공통 데이터를 중심으로 긴밀하게 엮어 작동하는 함수 무리가 있다면, 클래스로 묶는다.

📜 절차

  1. 함수들이 공유하는 공통 데이터 레코드를 캡슐화한다.
  2. 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮긴다.
  3. 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮긴다.

다음의 예제를 보자. rawReading 데이터를 가지고 비슷한 연산을 수행하는 부분은 다음과 같다.

const rawReading = { customer: "ivan", quantity: 10, month: 5, year: 2017 };

const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
const taxableCharge = Math.max(0, baseCharge - taxThreshold(reading.year));

먼저, rawReading 객체를 클래스로 변환하는 리팩터링을 거친다.
example-code9-1

공통 데이터 객체를 사용하는 함수 각각을 새 클래스로 옮긴다.

example-code9-2

이렇게 클래스로 묶으면 이 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있고, 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있다.


💻 여러 함수를 클래스로 묶기 코드


여러 함수를 변환 함수로 묶기

example-code10

한 데이터를 입력받아 도출된 여러 도출 로직들은 한군데로 묶는다.

📜 절차

  1. 변환할 레코드를 입력받아서 값을 그대로 반환하는 변환 함수를 만든다.
  2. 본문 코드를 변환 함수로 옮기고, 처리 결과를 레코드에 새 필드로 기록한다. 그런 다음 클라이언트 코드가 이 필드를 사용하도록 수정한다.

여러 함수를 클래스로 묶기와의 차이는, 원본 데이터가 코드 안에서 갱신되는 경우에는 클래스로 묶는 것이 낫다. 변환 함수로 묶으면 가공한 데이터를 새로운 레코드에 저장하기 때문에 원본 데이터가 수정되면 일관성이 깨질 수 있기 때문이다. 여러 함수를 변환 함수로 묶기의 경우는 그래서 읽기 전용 데이터를 다룰 때 특히 좋다.


단계 쪼개기

example-code11

서로 다른 두 대상을 한꺼번에 다루는 코드를 발견한다면, 각각을 별개 모듈로 나누도록 한다.

📜 절차

  1. 두 번째 단계에 해당하는 코드를 독립 함수로 추출한다.
  2. 중간 데이터 구조를 만들어서 앞에서 추출한 함수의 인수로 추가한다.
  3. 추출한 두 번째 단계 함수의 매개변수를 하나씩 검토한다. 그중 첫 번째 단계에서 사용되는 것은 중간 데이터 구조로 옮긴다.
  4. 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조를 반환하도록 만든다.

다음의 예제는 상품의 결제 금액을 계산하는 예제다.

코드를 살펴봤을 때, 상품 가격을 계산하는 단계와 배송비를 계산하는 단계로 나눌 수 있다. 나중에 상품 가격과 배송비 계산을 더 복잡하게 만드는 변경이 생긴다면, 이처럼 두 단계로 나누는 것이 좋다.

export const priceOrder = (product, quantity, shippingMethod) => {
  const basePrice = product.basePrice * quantity;
  
  const discount = Math.max(quantity - product.discountThreshold, 0) 
    * product.basePrice * product.discountRate;
  
  const shippingPerCase = basePrice > shippingMethod.discountThreshold
      ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  
  const shippingCost = quantity * shippingPerCase;
  
  const price = basePrice - discount + shippingCost;
  
  return price;
};

먼저 배송비 계산 부분을 추출해 applyingShippingCost 함수를 생성하고, 첫 번째 단계와 두 번째 단계가 주고받을 중간 데이터 priceData를 만든다. 이때 첫 번째 단계에서 사용되는 데이터 basePricediscount, quantitypriceData로 옮긴다.

const applyingShippingCost = (priceData,  shippingMethod) => {
  const shippingPerCase = priceData.basePrice > shippingMethod.discountThreshold
    ? shippingMethod.discountedFee : shippingMethod.feePerCase;

  const shippingCost = priceData.quantity * shippingPerCase;

  return priceData.basePrice - priceData.discount + shippingCost;
};

export const priceOrder = (product, quantity, shippingMethod) => {
  // ...
  const priceData = {basePrice, quantity, discount};
  
  const price = applyingShippingCost(priceData, shippingMethod);
  
  return price;
};

이제 첫 번째 단계 코드를 함수로 추출하고 priceData로 반환한다.

const calculatePriceData = (product, quantity) => {
  const basePrice = product.basePrice * quantity;

  const discount = Math.max(quantity - product.discountThreshold, 0) 
    * product.basePrice *  product.discountRate;

  return { basePrice, quantity, discount };
};

// ...

export const priceOrder = (product, quantity, shippingMethod) => {
 const priceData = calculatePriceData(product, quantity);
  return applyingShippingCost(priceData, shippingMethod);
};

💻 단계 쪼개기 코드

0개의 댓글