액션 : 실행값이나 횟수에 의존
계산 : 같은 입력 값에는 같은 출력
데이터 : 이벤트에 대한 사실. 영구적
액션을 포함한 함수는 액션이다.
=> 여러개로 중첩되거나 연달하 호출하는 함수안에 단 1개의 액션만 있더라도 그 모든 함수는 액션이 된다.
입력과 출력은 암묵적일 수 있다.
함수에 암묵적 입출력이 있으면 액션이 된다.
함수에서 암묵적 입출력을 제거하면 계산이 된다.
서브루틴 추출하기 : 코드의 일부를 별도의 서브루틴(또는 메서드)으로 분리.
원래 코드에서는 분리한 메서드를 호출하여 사용
공유변수(전역변수 등)를 포함한 암묵적 입력은 인자로, 암묵적출력은 리턴값으로 변경하여 추상화
액션의 인자와 출력을 명시적으로 하더라도, 해당 인자들이 요구사항에 잘 부합하는지 (결과가 맞아 떨어지도록 만드는 것이 아니라, 의미적으로 흐름이 맞는지)
과한 함수의 분리?? 기준이 뭘까? 다른 함수에서 재사용 될 가능성?
그렇다면 액션에서 계산을 분리하여 함수로 만드는 것은 다른 곳에서 재사용될 가능성이 높은 것들로 추출해야 할 듯. 코드가 길어보인다고 해서, 액션에 계산이 포함되어 있다고 해서 무작정 분리하는 건 좋지 못할 듯.
의미있는 계층
관심사에 따른 분리 ( cart, item, user ...)
자신과 연관되어 있는 걸로만 구성하고, 나머지는 명시적 입력과 출력에 의해서.
유틸리티 함수 생성
특정 데이터명으로 쓰이는 것이 아니라 일반적인 네이밍으로 하여 도구로서 가치를 가지도록 함. ( add_item(cart,item) --> add_element(arr,el) )
비즈니스 로직 : 내 서비스에서 운영되는 특별한 규칙. 정책이나 계획수립에 따라 회사마다 운영되는 논리적 흐름을 일컫는 듯.
설계 : 엉커있는 것을 푼다. 함수가 하나의 일만 하도록 작성하면 개념을 중심으로 쉽게 구성할 수 있다. ( 클린코드에서도 강조한 내용이었다. )
리턴값이 있다 == 읽기동작 == 불변성
'쓰는 동작' 은 이미 존재하는 데이터에 작성하는 것
카피온 라이트는 이미 존재하는 데이터를 '읽고' 복사하여, 새로운 저장소에 초기화 함.
즉, 쓰는 동작 (== 액션) 에서 읽기 (==계산)으로 변경
(121p) 장바구니에 물건을 추가하는 동작이 있을 때,
장바구니 배열에 아이템을 push 하는 것이 아니라,
장바구니 배열을 복사하여, 새로운 배열에 push하고 그 새로운 배열을 원래 배열을 가리키던 변수에 대입하는 것이다. (즉 새로운 배열의 주소로 변경한다)
최종상태는 장바구니는 새로운 배열을 가리키고, 원래의 배열도 그대로 남아 있다.
shift() 와 같이 (1)원본배열을 변경하며, (2)첫번째 요소를 반환 하는 메소드는 현재 읽기와 쓰기가 결합되어 있다. 리턴값이 있으므로, 이미 이 메소드는 "읽기" 작업이다. => 배열을 복사하여 활용하는 함수를 만들고, 첫번재 값이 리턴하거나 복사한 배열 중 첫번째 값이 없어진 새로운 배열을 리턴하는 함수를 각각 만들어 분리하여 사용한다.
액션 : 변경 가능한 값을 읽는 것 ( 고정적이지 않는 데이터..)
계산 : 변경 불가능한 값을 읽는 것
서비스를 개발하다보면 예측 불가능한 요소들이 많으므로, 불변데이터 구조로 작성하되, 속도가 느린부분에 대해서만 최적화를 하는게 낫다. 또한 불변데이터 구조를 취한다 하더라도, 얕은 복사가 대부분이므로 주소값만 복사되어 걱정보다 많이 메모리를 차지하지 않는다.메모리 관리를 하는 가비지 컬렉터도 성능이 좋으므로 불변데이터는 전체적인 서비스성능을 저하시키지 않는다.
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 setPrice(item, new_price){
return objectSet(item, "price", new_price)
}
function objectSet(object, key, value){
var new_obj = Object.assign({}, object);
copy[key] = value //-----------------------------------②
return copy
}
functionadd_item_to_cart(name, price){
...
var cart_copy = deepCopy(shopping_cart); // -----①
black_friday_promotion(cart_copy) // -----②
shopping_cart = deepCopy(cart_copy); // -----③
}
① 레거시 코드인 black_friday_promotion
함수에 데이터를 넘기기 전, 데이터의 주소를 줘버리면 내것도 어떻게 바뀔 지 모르므로, 깊은 복사해서 사본을 줌.
② black_friday_promotion
는 데이터를 받았지만,
functionadd_item_to_cart
가 가진 원래의 shopping_cart
의 주소는 알 수 없음.
③ 아무튼 복사받은 데이터가 함수를 통과해서 다시 돌아왔을 때, 그걸 그대로 쓰면 그 데이터의 주소가 외부에 공유되었기 때문에 언제 어떻게 바뀔지 모르므로, 값 자체만 가져다 쓸 수 있도록 깊은 복사해서 저장함.
유지보수성
: 아래에 있는 함수일 수록 이 함수에 의존하는 함수가 많아 수정하기 어렵다. 즉 자주바뀌는 코드일 수록 위쪽 계층으로 간다.테스트 가능성
: 아래쪽에 있는 함수는 기반이 되는 함수들이기 때문에 거의 바뀌지 않는다. 따라서 아래쪽에 있는 함수를 테스트 하는 것이 비용대비 얻는 게 많다.재사용성
: 아래쪽에 있는 함수는 다른 함수의 기반이 될 수 있는 함수들이다. 낮은수준의 단계는 재사용성이 높다.냄새나는 코드를 암묵적 인자-> 명시적 인자로 바꾸어 리팩토링 한다는 것은, 내부에 구현된 구체적 요소를 밖으로 빼어 인풋으로 만드는 것을 말한다.
암묵적 인자는 함수내부에 인풋이 아닌 미리 박혀있는 '일급 값' 이라고 생각하면 된다. 보통은 그 함수의 구체적인 기능에 관련이 되어 있기 때문에 함수의 이름과 관련 지어두었을 가능성이 크다. 따라서 이 미리 박혀있는 요소를 빼어 그것을 인풋값으로 받아두도록 하면 (일급이므로 인자로 받을 수 있다. JS에서는 함수도 일급함수이므로 함수도 받을 수 있다는 말이 됨), 그 함수는 더이상 그 암묵적 요소에 얽메이지 않게된다. 비슷한 형태의 함수들은 이런 과정을 거치면 모두 서로 공통적인 함수를 만들어 낼 것이다. 이것이 추상화를 통해 코드에서 나는 냄새를 없앨 수 있는 방법이다.
일급이 아닌 요소들도 일급처럼 다룰 수 있다.
'+' 와 같은 더하기 연산자를 인자 a,b를 받아 a+b를 리턴하는 형태의 함수를 만들면 된다.
이처럼 암묵적 인자를 명시적 인자로 바꾸는 리팩토링과, 일급이 아닌 요소를 일급으로 만드는 과정을 토대로, 함수가 함수를 인자로하여 고차함수를 만들어 낼 수 있는 준비가 된다.
- 일급 값은 변수에 저장할 수 있고, 인자로 전달하거나 함수의 리턴값으로 활용이 가능하다.
- 일급이 아닌 기능은 함수로 감싸 일급으로 만들 수 있다.
- 고차함수는 다른 함수에 인자로 넘기거나 리턴 값으로 받을 수 있는 함수다.
- 함수에서 나는 냄새를 없애기 위해, 암묵적 인자를 드러내고 알맹이를 콜백으로 바꾸는 과정을 사용할 수 있다.
function arraySet(arr, idx, val){
var copy = arr.slice();
copy[idx] = value;
return copy;
}
⬇︎
function withArrCopy(arr, f){
var copy = arr.slice();
f(copy);
return copy;
function arraySet(arr, idx, val){
return withArrCopy(arr, f(copy){
copy[idx] = value;
});
}
f(copy){copy[idx] = value;}
를 받은withArrCopy(arr, f)
를 실행시켜(평가하여) 그 결과값을 반환한다.try{ saveuserData(user); }
catch (error){ logErrors(error); }
⬇︎
function wrapLoggingError(f){
return function (args){
try{ f(args); }
catch (error){ logErrors(error); }
}
saveuserDataWithLogging = wrapLoggingError(saveuserData(user))
wrapLoggingError
가 알맹이 함수saveuserData
를 받아 함수 자체를 평가(생성)하여 반환하고 있다.팩토리 함수
라고 한다.
- 고차함수로 패턴이나 원칙을 코드로 만들 수 있다.
- 고차함수로 함수를 리턴하는 함수를 만들어 낼 수도 있다.
- 고차함수로 코드의 중복을 상당히 줄일 수 있다. 그러나, 고차함수의 남용은 코드의 역할이 무엇인지 이해하기 어렵게 만들 수 있다. 적절하게 사용해야 한다.
function curry(f) {
return (a, ..._) => (_.length ? f(a, ..._) : (..._) => f(a, ..._));
}
같은 함수가 계속 중첩되는경우 (nested) 재귀를 통해 정리할 수 있다.
재귀함수를 생성하게 될 경우, 일반적인 재귀 호출과정과 맨 끝단(더 이상 nested 되지 않는 시점) 을 정의한다.
함수의 이름에 구체적인 내용이 들어가 있는경우와 함수의 이름에서 냄새나는 것을 구분하기 : 추상화 된 함수를 활용하는 경우, 변수와 자료구조를 보고 어떤 동작인지 이해하기 어려울 수 있다. 추상화된 함수를 호출하는 함수를 만들어서, 특정 조건의 인자를 미리 인풋값에 넣어 만들어 둘 수 있다.
function updatePostById(category, id, modifyPost){
return nestedUpdate(category, ['posts', id], modifyPost)
}
function nestedUpdate(object, keys, modify){
if (keys.length === 0)
return modify(object)
var key1 = key[0]
var restOfkeys = drop_first(keys);
return update(object, keys, function(value1){
return nestedUpdate(valie1, restOfkeys, modify)
})
}
- 배열을 다루는 고차함수 활용하기
- 중첩된 데이터를 변경할 때, 특정 층의 데이터까지 내려가는 동안의 모든 데이터를 복사하여야 한다. (카피-온-라이트, 액션이 아닌 계산)
- 함수가 일급임을 이용하여, 어떤 원칙을 코드로 표현하거나 어떤 함수를 다른 행동이 추가된 함수로 바꿀 수 있다. (후자는 데코레이터와 같은역할이라고 볼 수 있을 것 같다.)
function get_pets_ajax() {
var pets = 0;
return dogs_ajax(function (dogs) {
cats_ajax(function (cats) {
pets = dogs + cats;
return pets;
});
});
}
위와 같은 코드를 실행하면 정상적으로 pets를 받을 수 있지 않을까?
하지만, dogs_ajax는 undefined 를 반환한다.
그렇다면 비동기적으로 계산되는 pets를 어떻게 활용할 수 있을까?
function get_pets_ajax(callback) {
var pets = 0;
dogs_ajax(function (dogs) {
cats_ajax(function (cats) {
pets = dogs + cats;
callback(pets);
});
});
}
내가 pets를 계산하려는 것은, 어디선가 활용하려고 하기 때문이다.
활용할 어딘가를 함수자체에 넘겨주면 된다. 함수는 일급이므로 인자로 넘겨줄 수 있고,
위 코드에서는 cats_ajax
가 실행되어 pets를 계산하고 나면 그 때 비로소 callback 부분을 실행할 것이다.
이렇게 보니 프로미스가 나오기 전, 콜백으로 비동기를 어떻게 다루었는지 이해가 된다.
안전하게 공유 : 올바른 순서대로 자원을 사용하고 반환한다.
queue를 통해서 자원에 대해 한번에 하나의 작업만 사용이 가능하게 만들 수 있다.
굳이 큐가 아니더라도 다양한 방식으로 구현 가능함.
동시성 기본형
: 자원을 안전하기 공유할 수 있는 재사용 가능한 코드. 직접 만들어서 쓸 수도.
function add_item_to_cart(item){
cart = add_item(cart,item)
// add_item은 객체를 새로 생성해서 반환함.
calc_cart_total(cart, update_total_dom);
}
function calc_cart_total (cart, callback){
var total = 0;
cost_ajax(cart, function(cost){
total += cost
shipping_ajax(cart, function(shipping){
total += shipping
callback(total);
});
});
}
function DroppingQueue(max, worker) {
var queue_items = [];
var working = false;
function runNext() {
if (working) { return }
if (queue_items.length == 0) { return }
working = true
var item = queue_items.shift();
worker(item.data, function (val) { //<----- 근데 이러려면 worker의 형태가
//미리 지정되어 있으니, 잘 맞춰서 써야할 듯.
//(타입스크립트에서는 타입을 지정해버리면 좀 나을듯?)
working = false;
setTimeout(item.callback, 0, val)
runNext()
})
return function (data, callback) {
queue_items.push({
data: data,
callback: callback || function () { }
});
while (queue_items.length > max) {
queue_items.shift();
}
setTimeout(runNext, 0);
}
}
}
function calc_cart_worker(cart, done) {
calc_cart_total(cart, function (total) {
update_total_dom(total);
done(total);
});
}
var update_total_queue = DroppingQueue(1, calc_cart_worker)
// cart에 아이템을 추가하고, cart 전체의 금액을 계산하는 함수.
// 따라서 여러번 클릭해도 최신 cart 만 받아서 실행하면 되므로, 중간 것들은 버려도 됨.
function Cut(num, callback){
var num_finished = 0;
return function(){
num_finished += 1;
if (num_finished == num)
callback();
};
}
function JustOnce(action){
var alreadyCalled = false;
return function(a,b,c){
if (alreadyCalled) return;
alreadyCalled = true;
return action(a,b,c);
}
}
이처럼 프로그램에서는 여러 작업이 난무하여 진행되는데, 이 질서를 유지시킬 수 있도록 시간모델과 동시성 기본형을 만들어 사용해야 한다.
🙋 항상 반응형 아키텍쳐가 더 좋을까?
- 그렇지 않다. 적합한 아키텍쳐는 프로그램에 따라 다르다.
- 문서를 검증하고 서명 후 보관함에 저장하는 시스템은 원인과 효과가 잘 나타나지 않는다. 이런경우는 순차적인 아키텍쳐가 더 적합할 수 있다.
- 반면에 고객의 행동에 따라 다른 서비스를 제공해야하는 프로그램의 경우, 원인과 효과가 일어나기 충분하므로 반응형 아키텍쳐가 적합할 수 있다.
var 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)
}
모든 액션이 뭉쳐있는 레거시 코드 예제
(제품추가를 눌러, 쇼핑카트를 업데이트하고, 연달아 아이콘과 DOM을 업데이트 하는 것)
제품추가
, 제품삭제
, 장바구니 비우기
, 수량변경
, 할인코드 적용
배송 아이콘 업데이트
, 세금 표시
, 합계 표시
, 장바구니 개수 업데이트
function ValueCell(initialValue) {
var currentValue = initialValue;
var watchers = []
return {
val: function () {
return currentValue
},
update: function (f) {
var oldValue = currentValue
var newValue = f(oldValue)
if (oldValue !== newValue) {
currentValue = newValue
forEach(watchers, function (watchers) {
watchers(newValue);
})
}
},
addWatcher: function (f) {
watchers.push(f)
}
}
}
function FormulaCell(upstreamCell, f) {
var myCell = ValueCell(f(upstreamCell.val()))
upstreamCell.addWatcher(function (newUpStreamValue) {
myCell.update(function (oldValueOfmyCell) {
return f(newUpStreamValue)
})
})
return {
val: myCell.val,
addWatcher: myCell.addWatcher
}
}
셀을 만들어 반응형으로 관리.
ValueCell
FormulaCell
이제 셀을 활용하여 레거시 코드를 고쳐보자.
var shopping_cart = ValueCell({});
var cart_total = FormulaCell(shopping_cart, calc_total)
function add_item_to_cart(name, price) {
var item = make_cart_item(name, price)
shopping_cart.update(function (cart) {
return add_item(cart, item);
})
}
shopping_cart.addWatcher(update_shipping_icons)
cart_total.addWatcher(set_cart_total_dom)
cart_total.addWatcher(update_tax_dom)
코드 설명
set_cart_total_dom
과 update_tax_dom
이 실행됨.인터렉션 계층
: 바깥세상에 영향을 주거나 받는 액션도메인 계층
: 비즈니스 규칙을 정의하는 계산언어 계층
: 언어 유틸리티와 라이브러리전통적인 계층형 아키텍쳐는 데이터베이스가 최하단에 존재하여, 모든 것이 액션이 된다.
그러나 어니언 아키텍쳐는 함수형 프로그래밍에서 액션과 계산을 분리하는 방법과 같이,
외부세계에 영향을 주고 받는 층과, 주어진 값에 대해 특정 동작을 한 후 다시 반환해주는 도메인 층으로 구분할 수 있다.
도메인 계층을 액션이 아닌 계산으로 유지하는 것이 중요함.