쏙쏙 들어오는 함수형 코딩 Chapter 7~12
Chapter 7 : 신뢰할 수 없는 코드를 쓰면서 불변성 지키기
앞장에서 본 Copy-on-write 코드는 신뢰할 수 없는(안전지대 밖)의 코드와도 상호작용해야 한다.
안전지대 밖으로 나가는 데이터는 잠재적으로 바뀔 수 있다. 그럼 불변성을 지키면서 어떻게 데이터를 주고받을까?
방어적 복사
방어적 복사는 원본이 바뀌는 것을 막아준다.
데이터를 전달하기 전후에 깊은 복사를 해서 불변성을 지키는 것이다.
규칙
- 데이터가 안전한 코드에서 나갈 때 복사하기
- 안전한 코드로 데이터가 들어올 때 복사하기
예시
원래 코드
function add_item_to_cart(name, price) {
var item = make_cart_item(name, price);
shopping_cart = add_item(shopping_cart, item);
var total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
black_friday_promotion(shopping_cart);
}
안전하게 분리한 버전
function add_item_to_cart(name, price) {
var item = make_cart_item(name, price);
shopping_cart = add_item(shopping_cart, item);
var total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
shopping_cart = black_friday_promotion_safe(shopping_cart);
}
function black_friday_promotion_safe(cart) {
var cart_copy = deepCopy(cart);
black_friday_promotion(cart_copy);
return deepCopy(cart_copy);
}
방어적 복사가 익숙할 수도..?
웹 API는 암묵적으로 방어적 복사를 한다.
얼랭, 엘릭서 등 함수형 프로그래밍 언어도 방어적 복사를 잘 구현했다.
깊은 복사는 얕은 복사보다 비싸다
당연하게도 원본과 어떤 데이터 구조도 공유하지 않고 모든 중첩된 객체나 배열을 복사하니 얕은 복사보다 비용은 비싸다. 그래서 모든 곳에 깊은 복사를 쓰지는 않는다. Copy-on-write를 사용할 수 없는 곳에서만 사용하자.
Chapter 8 : 계층형 설계 1
계층형 설계
계층형 설계는 소프트웨어를 계층으로 구성하는 기술이다.
계층을 잘 구분하는 방법은 연습 뿐이다. 반복에서 패턴을 찾아보자.
계층형 설계 패턴
책에서는 네 가지 패턴을 알려준다.
- 직접 구현
- 추상화 벽
- 작은 인터페이스
- 편리한 계층
패턴 1: 직접 구현
예시
개선 전 코드
function freeTieClip(cart) {
var hasTie = false
var hasTieClip = false;
for(var i = 0; i < cart.length; i++) {
var item = cart[i];
if(item.name === "tie")
hasTie = true;
if(item.name === "tie clip")
hasTieClip = true;
}
if(hasTie && !hasTieClip) {
var tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip);
}
return cart;
}
freeTieClip 함수는 array index, for loop등 언어 그 자체의 기능도 사용하며 함수 make_item, add_item 도 사용하고 있다.
서로 다른 추상화 단계에 있는 기능을 사용하는 직접 구현 패턴이 아니다.
개선 후 코드
function freeTieClip(cart) {
var hasTie = isInCart(cart, "tie");
var hasTieClip = isInCart(cart, "tie clip");
if(hasTie && !hasTieClip) {
var tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip);
}
return cart;
}
function isInCart(cart, name) {
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
return true;
}
return false;
}
개선된 freeTieClip 함수는 정확히 같은 추상화 단계라도 확신할 수는 없지만 비슷한 추상화 단계를 사용한다.
이처럼 함수가 모두 비슷한 계층에 있다면 직접 구현했다고 할 수 있다.
Chapter 9 : 계층형 설계 2
패턴 2: 추상화 벽
추상화 벽은 세부 구현을 감춘 함수로 이루어진 계층이다. 추상화 벽에 있는 함수를 사용할 때는 구현을 전혀 몰라도 함수를 쓸 수 있다.
예를 들면 개발팀에서 추상화 벽을 사용해 마케팅 팀에게 API를 제공하면 마케팅 팀은 내부 구현을 신경쓰지 않고 함수 이름만 보고 어떤 일을 하는 지 알 수 있기 때문에 문제없이 사용할 수 있다.
예시
배열로 만든 장바구니
function add_item(cart, item) {
return add_element_last(cart, item);
}
function calc_total(cart) {
var total = 0;
for(var i = 0; i < cart.length; i++) {
var item = cart[i];
total += item.price;
}
return total;
}
function setPriceByName(cart, name, price) {
var cartCopy = cart.slice();
for(var i = 0; i < cartCopy.length; i++) {
if(cartCopy[i].name === name)
cartCopy[i] = setPrice(cartCopy[i], price);
}
return cartCopy;
}
function remove_item_by_name(cart, name) {
var idx = indexOfItem(cart, name);
if(idx !== null)
return splice(cart, idx, 1);
return cart;
}
function indexOfItem(cart, name) {
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
return i;
}
return null;
}
function isInCart(cart, name) {
return indexOfItem(cart, name) !== null;
}
객체로 만든 장바구니
function add_item(cart, item) {
return objectSet(cart, item.name, item);
}
function calc_total(cart) {
var total = 0;
var names = Object.keys(cart);
for(var i = 0; i < names.length; i++) {
var item = cart[names[i]];
total += item.price;
}
return total;
}
function setPriceByName(cart, name, price) {
if(isInCart(cart, name)) {
var item = cart[name];
var copy = setPrice(item, price);
return objectSet(cart, name, copy);
} else {
var item = make_item(name, price);
return objectSet(cart, name, item);
}
}
function remove_item_by_name(cart, name) {
return objectDelete(cart, name);
}
function isInCart(cart, name) {
return cart.hasOwnProperty(name);
}
배열을 순서대로 검색하는 것은 성능이 떨어지기 때문에 장바구니를 객체로 바꾸었다.
데이터 구조가 완전히 바뀌었지만 마케팅팀은 구조가 바뀌었는 지 알지도 못하며, 사용하고 있는 코드를 고치지 않았고 그대로 사용할 수 있다.
이 예제에서 추상화 벽이 의미하는 것은 추상화 벽 위에 있는 함수가 데이터 구조를 몰라도 된다는 것이다. 추상화 벽에 있는 함수만 사용하면 되고 장바구니 구현에 대해서는 신경쓰지 않아도 된다.
패턴 3: 작은 인터페이스
작은 인터페이스 패턴은 새로운 코드를 추가할 위치에 관한 내용이다.
인터페이스를 최소화하면 하위 계층에 불필요한 기능이 쓸데없이 커지는 것을 막을 수 있다.
마케팅팀이 장바구니에 제품을 담을 때 로그를 남기려고 한다.
function add_item(cart, item) {
logAddToCart(global_user_id, item);
return objectSet(cart, item.name, item);
}
위 코드에 위치한 logAddToCart 가 좋은 위치일까?
당장의 문제는 없다. 요구 사항을 맞출 수 있는 가장 쉬운 방법으로 보인다.
하지만 logAddToCart 는 액션이라 add_item 도 액션이 된다. 이로 인해 테스트하기가 어려워진다.
add_item 는 추상화 벽에 있는 함수이기 때문에 다른 코드들에 영향을 많이 미친다. 그래서 logAddToCart 는 추상화 벽 위에 있는 계층에서 호출하는 것이 좋다.
function add_item_to_cart(name, price) {
var item = make_cart_item(name, price);
shopping_cart = add_item(shopping_cart, item);
var total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
logAddToCart();
}
추상화 벽 위에 있는 add_item_to_cart 에서 호출하여 해결 ~
정리
추상화 벽에 만든 함수는 인터페이스라고 할 수 있다. 이 인터페이스로 어떤 값에 접근하거나 값을 조작할 수 있다.
추상화 벽을 작게 만들어야하는 이유
- 추상화 벽에 인터페이스가 많으면 알아야할 것이 많다.
- Low Level의 코드는 이해하기 어렵다.
- 추상화 벽에 인터페이스가 많으면 구현이 변경 되었을 때 고쳐야 할 것이 많다.
패턴 4: 편리한 계층
마지막으로 알아볼 편리한 계층 패턴에서는 조금 더 현실적으로 실용적인 측면을 다루고 있다.
호출 그래프로 알 수 있는 비기능적 요구사항
- 유지보수성
- 테스트성
- 재사용성
그래프의 가장 위에 있는 코드가 고치기 가장 쉽다. 가장 아래에 있는 코드가 고치기 가장 어렵다.
위에 있는 코드는 자주 바뀌기 때문에 테스트 코드를 작성하여도 금세 바뀌어야 한다. 아래 있는 코드는 자주 바뀌지 않기 때문에 테스트도 자주 바뀌지 않는다. 아래에 있는 함수를 테스트해보자!
Chapter 10 : 일급 함수 1
코드 냄새: 함수 이름에 있는 암묵적 인자
function setPriceByName(cart, name, price) {
var item = cart[name];
var newItem = objectSet(item, 'price', price);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setShippingByName(cart, name, ship) {
var item = cart[name];
var newItem = objectSet(item, 'shipping', ship);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setQuantityByName(cart, name, quant) {
var item = cart[name];
var newItem = objectSet(item, 'quantity', quant);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setTaxByName(cart, name, tax) {
var item = cart[name];
var newItem = objectSet(item, 'tax', tax);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function objectSet(object, key, value) {
var copy = Object.assign({}, object);
copy[key] = value;
return copy;
}
모든 함수가 비슷하게 생겼다. 이 코드들은 코드 냄새로 가득하다. 이 냄새를 함수 이름에 있는 암묵적 인자라고 한다. 값을 명시적으로 전달하지 않고 함수 이름의 일부로 전달하고 있다.
리팩터링: 암묵적 인자를 드러내기
단계
- 함수 이름에 있는 암묵적 인자를 확인
- 명시적 인자를 추가
- 함수 본문에 하드 코딩된 값을 새로운 인자로 바꾼다.
- 함수를 부르는 곳을 고친다.
예시
개선 전 코드
function setPriceByName(cart, name, price) {
var item = cart[name];
var newItem = objectSet(item, 'price', price);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
cart = setPriceByName(cart, "shoe", 13);
cart = setQuantityByName(cart, "shoe", 3);
cart = setShippingByName(cart, "shoe", 0);
cart = setTaxByName(cart, "shoe", 2.34);
개선 후 코드
function setFieldByName(cart, name, field, value) {
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
cart = setFieldByName(cart, "shoe", 'price', 13);
cart = setFieldByName(cart, "shoe", 'quantity', 3);
cart = setFieldByName(cart, "shoe", 'shipping', 0);
cart = setFieldByName(cart, "shoe", 'tax', 2.34);
리팩터링으로 필드명을 일급 값으로 만들었다.
위 문장이 무슨 말이지..?
→ 함수 이름에 암묵적으로 있던 것을 인자로 넘길 수 있는 값이 되었다.
→ 일급 값이 되었다.
일급인 것과 아닌 것 구별하기
자바스크립트에서 일급이 아닌 것
- 수식 연산자
- 반복문
- 조건문
- try/catch 블록
일급으로 할 수 있는 것 (중요!)
- 변수에 할당
- 함수의 인자로 넘기기
- 함수의 리턴값으로 받기
- 배열이나 객체에 담기
고차 함수
인자로 함수를 받거나 리턴 값으로 함수를 리턴할 수 있는 함수
리팩터링: 함수 본문을 콜백으로 바꾸기
예시
개선 전 코드
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProduct(productId);
} catch (error) {
logToSnapErrors(error);
}
위 코드처럼 에러 시 로깅 시스템으로 에러를 전송하고 싶을 경우가 있다. 위 코드는 try/catch가 두 개 뿐이지만 만약 수천 개라면..? 손목이 남아나지 않을 것이다.
개선 후 코드
function withLogging(f) {
try {
f();
} catch (error) {
logToSnapErrors(error);
}
}
withLogging(function() {
saveUserData(user);
});
withLogging(function() {
fetchProduct(productId);
});
함수 본문을 콜백으로 바꿈으로써 중복을 많이 줄였다!
Chapter 11 : 일급 함수 2
10장에 있는 리팩터링: 함수 본문을 콜백으로 바꾸기를 이어서 좀 더 연습해보자!
Copy-on-write 리팩터링하기
As-is
function arraySet(array, idx, value) {
var copy = array.slice();
copy[idx] = value;
return copy;
}
array.slice(); 로 copy한 후 write하고 있다.
function arraySet(array, idx, value) {
return withArrayCopy(array);
}
function withArrayCopy(array) {
var copy = array.slice();
copy[idx] = value; // 아직 바꾸는 중
return copy;
}
배열을 복사하고 값을 할당하는 함수를 먼저 빼냈다.
To-be
function arraySet(array, idx, value) {
return withArrayCopy(array, function(copy) {
copy[idx] = value;
});
}
function withArrayCopy(array, modify) {
var copy = array.slice();
modify(copy);
return copy;
}
값을 할당하는 로직을 콜백으로 빼냈다.
원래 코드보다 줄 수는 늘었지만 이제 똑같은 코드를 여기저기 만들지 않아도 된다.
함수를 리턴하는 함수
10장에서 개선해본 withLogging 함수를 다시 리팩터링 해보자!
As-is
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProduct(productId);
} catch (error) {
logToSnapErrors(error);
}
try {
saveUserDataNoLogging(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProductNoLogging(productId);
} catch (error) {
logToSnapErrors(error);
}
// ->
function saveUserDataWithLogging(user) {
try {
saveUserDataNoLogging(user);
} catch (error) {
logToSnapErrors(error);
}
}
function fetchProductWithLogging(productId) {
try {
fetchProductNoLogging(productId);
} catch (error) {
logToSnapErrors(error);
}
}
이름을 명확하게 바꾸고 함수로 뺀다.
function(arg) {
try {
saveUserDataNoLogging(arg);
} catch (error) {
logToSnapErrors(error);
}
}
function(arg) {
try {
fetchProductNoLogging(arg);
} catch (error) {
logToSnapErrors(error);
}
}
// ->
function wrapLogging(f) {
return function(arg) {
try {
f(arg);
} catch (error) {
logToSnapErrors(error);
}
}
}
var saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging);
var saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging);
var fetchProductWithLogging = wrapLogging(fetchProductNoLogging);
이름을 없애고 익명 함수로 만들었다. 그 다음 함수 본문을 콜백으로 바꾼다.
이제 로그를 남기지 않는 버전을 로그를 남기는 버전으로 쉽게 바꿀 수 있다!
Chapter 12 : 함수형 반복
이번 장은 자바스크립트에서 많이 사용하는 map, filter, reduce 를 직접 구현해보며 연습한다.
함수형 도구: Map()
function map(array, f) {
var newArray = [];
forEach(array, function(element) {
newArray.push(f(element)); // 원래 배열 항목으로 새로운 항목을 만들기 위해 f() 호출
});
return newArray; // 새로운 배열 return
}
적용 예시
function emailsForCustomers(customers, goods, bests) {
var emails = [];
for(var i = 0; i < customers.length; i++) {
var customer = customers[i];
var email = emailForCustomer(customer, goods, bests);
emails.push(email);
}
return emails;
}
// ->
function emailsForCustomers(customers, goods, bests) {
return map(customers, function(customer) {
return emailForCustomer(customer, goods, bests);
});
}
함수형 도구: filter()
function filter(array, f) {
var newArray = [];
forEach(array, function(element) {
if(f(element)) // f()를 호출해 항목을 결과 배열에 넣을지 확인
newArray.push(element); // 조건을 만족하면 원래 항목을 결과 배열에 넣는다
});
return newArray;
}
적용 예시
function selectCustomersAfter(customers, date) {
var newArray = [];
forEach(customers, function(customer) {
if(customer.signupDate > date)
newArray.push(customer);
});
return newArray;
}
// ->
function selectBestCustomers(customers) {
return filter(customers, function(customer) {
return customer.purchases.length >= 3;
});
}
함수형 도구: reduce()
function reduce(array, init, f) {
var accum = init;
forEach(array, function(element) {
accum = f(accum, element);
});
return accum;
}
적용 예시
function countAllPurchases(customers) {
var total = 0;
forEach(customers, function(customer) {
total = total + customer.purchases.length;
});
return total;
}
// ->
function countAllPurchases(customers) {
return reduce( customers, 0, function(total, customer) {
return total + customer.purchase.length;
});
}