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

function base(aReading) {...}
function taxableCharge(aReading) {...}
function enrichReading(argReading) {
	const aReading = _.cloneDeep(argReading);
	aReading.baseCharge = base(aReading);
	aReading.taxableCharge = taxableCharge(aReading);
	return aReading;
}

 

배경

소프트웨어는 데이터를 입력받아서 여러 가지 정보를 도출하곤 한다.

이렇게 도출된 정보는 여러 곳에서 사용될 수 있는데, 그러다 보면 이 정보가 사용되는 곳마다 같은 도출 로직이 반복되기도 한다.

  • 저자는 이런 도출작업들을 한데로 모아두길 좋아한다고 한다.
    • 모아두면 검색과 갱신을 일관된 장소에서 처리할 수 있다.
    • 로직 중복을 막을 수 있다.

 

 

이렇게 하기 위한 방법으로 변환 함수를 사용한다.

 

변환 함수란?

변환 함수는 원본 데이터를 입력 받아서 필요한 정보를 모두 도출한 뒤, 각각을 출력 데이터의 필드에 넣어 반환한다.

이렇게 해두면 도출 과정을 검토할 일이 생겼을 때 변환 함수만 살펴보면 된다.

 

절차

  1. 변환할 레코드를 입력받아서 값을 그대로 반환하는 변환 함수를 만든다.
    • 이 작업은 대체로 깊은 복사로 처리해야 한다. 변환 함수가 원본 레코드를 바꾸지 않는지 검사하는 테스트를 마련해두면 도움될 때가 많다.
  2. 묶을 함수 중 함수 하나를 골라서 본문 코드를 변환 함수로 옮기고, 처리 결과를 레코드에 새 필드로 기록한다. 그런 다음 클라이언트 코드가 이 필드를 사용하도록 수정한다.
    • 로직이 복잡하면 함수 추출하기부터 한다.
  3. 테스트한다.
  4. 나머지 관련 함수도 위 과정에 따라 처리한다.

 

예시

매달 사용자가 마신 차의 양을 측정하는 예시

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. 우선 입력 객체를 그대로 복사해 반환하는 변환 함수를 만든다.
function enrichReading(original) {
	const result = _.cloneDeep(original);
	return result;
}

 

 

2.이제 변경하려는 계산 로직 중 하나를 고른다.

먼저 이 계산 로직에 측정값을 전달하기 전에 부가 정보를 덧붙이도록 수정한다.

// 클라이언트 3
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;

calculateBaseCharge()를 부가 정보를 덧붙이는 코드 근처로 옮긴다(함수 옮기기)

 

 

function enrichReading(original) {
	const result = _.cloneDeep(original);
	result.baseCharge = calculateBaseCharge(result);  // <-- 미가공 측정값에 기본 소비량을 부가 정보로 덧붙임
	return result;
}

변환 함수 안에서는 결과 객체를 매번 복제할 필요 없이 마음껏 변경해도 된다.

이어서 이 함수를 사용하던 클라이언트가 부가 정보를 담은 필드를 사용하도록 수정한다.

 

 

// 클라이언트 3
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;

calculateBaseCharge()를 호출하는 코드를 모두 수정했다면, 이 함수를 enrichReading() 안에 중첩시킬 수 있다. 그러면 ‘기본요금을 이용하는 클라이언트는 변환된 레코드를 사용해야 한다’는 의도를 명확히 표현할 수 있다.

 

 

주의할 점

enrichReading()처럼 정보를 추가해 반환할 때 원본 측정값 레코드는 변경하지 않아야 한다. ⇒ 따라서 이를 확인하는 테스트를 작성해두는 것이 좋다.

if('check reading unchanged', function() {
	const baseReading = {customer: "ivan", quantity: 15, month: 5, year: 2017};
	const oracle = _.cloneDeep(baseReading);
	enrichReading(baseReading);
	assert.deepEqual(baseReading, oracle);
});

그런 다음 클라이언트 1도 이 필드를 사용하도록 수정

 

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

 

 

 

3. 이제 세금을 부과할 소비량 계산으로 넘어간다. 가장 먼저 변환 함수부터 끼워 넣는다.

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

여기서 기본요금을 계산하는 부분을 앞에서 새로 만든 필드로 교체할 수 있다.

계산이 복잡하다면 함수 추출하기부터 하겠으나, 여기서는 복잡하지 않으니 한 번에 처리하겠다.

 

 

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = aReading.baseCharge;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

 

테스트해서 문제가 없다면 base 변수를 인라인한다.

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));

 

그런 다음 계산 코드를 변환 함수로 옮긴다.

function enrichReading(original) {
	const result = _.cloneDeep(original);
	result.baseCharge = calculateBaseCharge(result);
	result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
	return result;
}

 

이제 새로 만든 필드를 사용하도록 원본 코드를 수정한다.

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.taxableCharge;

테스트에 성공하면 taxableCharge 변수를 인라인한다.

 

 

 

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

  • 변환함수를 작성 할 때도 함수명이 상당히 중요하다고 느꼈습니다.
  • 좀 더 추상화 기법에 근접한 내용 같다고 생각했습니다.
  • 지금까지 작성한 코드를 보면서 예시처럼 작성된 코드를 상당히 많이 봤습니다.

 

 

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

  • lodash 라이브러리란?
    • 자바스크립트의 인기 라이브러리 중 하나
    • 편리하게 코드를 작성하거나 웹 표준에 효율적으로 맞출 수 있게 해주는 Node.js 패키지

 

 

반응형

+ Recent posts