😀 책에서 기억하고 싶은 내용을 써보세요.

리팩터링 전

function base(aReading) {...}
function taxableCharge(aReading) {...}
function calculateBaseCharge(aReading) {...}

리팩터링 후

class Reading {
	base() {...}
	taxableCharge() {...}
	calculateBaseCharge() {...}
}

 

 

배경

  • 공통 데이터를 중심으로 긴밀하게 엮여 작동하는 함수 무리를 발견하면 클래스 하나로 묶는다
    • 클래스로 묶으면 이 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있다.
    • 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있다.
    • 이런 객체를 시스템의 다른 부분에 전달하기 위한 참조를 제공할 수 있다.

이 리팩터링은 이미 만들어진 함수들을 재구성할 때는 물론, 새로 만든 클래스와 관련하여 놓친 연산을 찾아서 새 클래스의 메서드로 뽑아내는 데도 좋다.

클래스를 지원하지 않는 언어를 사용할 때는 같은 기능을 ‘함수를 객체처럼(Function As Object)패턴을 이용해 구현하기도 한다.

 

 

절차

  1. 함수들이 공유하는 공통 데이터 레코드를 캡슐화한다.
    • 공통 데이터가 레코드 구조로 묶여 있지 않다면 먼저 매개변수 객체 만들기로 데이터를 하나로 묶는 레코드를 만든다.
  2. 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮긴다(함수 옮기기)
    • 공통 레코드의 멤버는 함수 호출문의 인수 목록에서 제거한다.
  3. 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮긴다.

 

 

예시

사람들은 매달 차 계량기를 읽어서 측정값을 다음과 같이 기록한다고 하자.

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

이런 레코드를 처리하는 코드를 훑어보니 이 데이터를 비슷한 연산을 수행하는 부분이 상당히 많았다. 그래서 기본요금을 계산하는 코드를 찾아봤다.

 

// 클라이언트 1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;

기본적인 차 소비량만큼은 면세가 되도록 한다.

 

// 클라이언트 2
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

여기서도 기본요금 계산 공식이 똑같이 등장하는 것을 발견했다.

저자와 성향이 같다면 곧바로 함수로 추출 하려 시도할 것이다.

 

// 클라이언트 3
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);

function calculateBaseCharge(aReading) {   // <--- 기본 요금 계산 함수
	return baseRate(aReading.month, aReading.year) * aReading.quantity;
}

이런 코드를 보면 저자는 본능적으로 앞의 두 클라이언트(클라이언트 1, 2)도 이 함수를 사용하도록 고친다고 한다. 하지만 이렇게 최상위 함수로 두면 못 보고 지나치기 쉽다는 문제가 있다.

저자라면 이런 함수를 데이터 처리 코드 가까이에 둔다고 한다, 그러기 위한 좋은 방법으로, 데이터를 클래스로 만든다.

 

 

  1. 먼저 레코드를 클래스로 변환하기 위해 레코드를 캡슐화한다.
class Reading {
	constructor(data) {
		this._customer = data.customer;
		this._quantity = data.quantity;
		this._month = data.month;
		this._year = data.year;
	}
	get customer() {return this._custoemr;}
	get quantity() {return this._quantity;}
	get month()    {return this._month;}
	get year()     {return this._year;}
}
  1. 이미 만들어져 있는 calculateBaseCharge()부터 옮기자.

 

  • 새 클래스를 사용하려면 데이터를 얻자마자 객체로 만들어야 한다.
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);

 

그런 다음 calculateBaseCharge()를 새로 만든 클래스로 옮긴다(함수 옮기기).

// Reading 클래스
get calculateBaseCharge() {
	return baseRate(this.month, this.year) * this.quantity;
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.calculateBaseCharge;

 

이 과정에서 메서드 이름을 원하는대로 바꾼다.(함수 이름 바꾸기)

get baseCharge() {
	return baseRate(this.month, this.year) * this.quantity;
}
// 클라이언트 3
const rawReading = acquireRading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.baseCharge;

이렇게 이름을 바꾸고 나면 Reading 클래스의 클라이언트는 baseCharge가 필드인지,

계산된 값(함수 호출)인지 구분할 수 없다.

이는 단일 접근 원칙(Uniform Access Principle)을 따르므로 권장하는 방식이다.

이제 첫 번째 클라이언트에서 중복된 계산 코드를 고쳐 앞의 메서드를 호출하게 한다.

 

// 클라이언트 1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseCharge;

 

새로 만든 기본요금 메서드를 사용하도록 수정한다.

// 클라이언트 2
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));

 

 

  1. 이어서 세금을 부과할 소비량을 계산하는 코드를 함수로 추출한다.
function taxableChargeFn(aReading) {
	return Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = taxableChargeFn(aReading);

 

그런 다음 방금 추출한 함수를 Reading 클래스로 옮긴다(함수 옮기기)

// Reading 클래스
get taxableCharge() {
	return Math.max(0, this.baseCharge - taxThreshold(this.year));
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = aReading.taxableCharge;
  • 파생 데이터 모두를 필요한 시점에 계산되게 만들었으니 저장된 데이터를 갱신하더라도 문제가 생길 일이 없다.
  • 프로그램의 다른 부분에서 데이터를 갱신할 가능성이 꽤 있을 때는 클래스로 묶어두면 큰 도움이 된다.

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 클래스와 캡슐화가 긴밀한 관계라는 이유를 알 것같습니다.
    • 변수와 함수를 클래스로 묶어서 공통 관리한다.
    • 함수들을 서로 공유하면서 더 명확하게 의도를 표현하게된다.

 

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 함수를 객체처럼 패턴은 어떤 패턴인지?
    • 함수형 프로그래밍
    • 프로그래밍에서 함수를 값으로 다룰 수 있는 것(함수 스스로 객체 취급) 즉, 함수를 변수에 담아 원할 때 평가(함수 호출)하는 것

 

 

 

반응형

+ Recent posts