'스터디캠프'라는 스터디를 통해 '함수형 코딩'이라는 개발 도서를 읽고 있습니다. 계획을 세우고 매주 내용을 정리해서 개발 블로그를 연재해 가는 것이 목표인데요. 이번에는 1 ~ 6 챕터를 읽고 핵심이라고 생각되는 내용들을 저의 생각을 녹여 정리했습니다.
목차
- '함수형 코딩' 독서 스터디 시작
- 실용적인 측면에서 함수형 프로그래밍
- 액션에서 계산을 분리해낸다.
- 계산도 다 같은 계산이 아니다. 계산을 분류하다.
- 계산을 만들기 위한 'Copy On Write' 기법
- 계산에 집착하는 이유
- 마무리
'함수형 코딩' 독서 스터디 시작
개발 학습 스터디를 시작했습니다. '함수형 프로그래밍'을 깊게 학습하기 위해 여러 단계로 학습을 계획했고, 그 첫 번째 학습이 '함수형 코딩' 책을 읽고 개념을 정립하는 것입니다.
스터디가 관심 있으신 분들은 '스터디캠프 첫 오픈' 글을 참고해 주세요.
실용적인 측면에서 함수형 프로그래밍
이 책은 기존 함수형 프로그래밍의 한계점을 지적하면서, 실용적인 측면에서 함수형 프로그래밍을 재해석하고 있습니다. 그리고 기존에 어렵게 느껴지는 함수형의 용어들을 훨씬 직관적인 용어로 재정의하기도 합니다.
그중 가장 핵심 개념은 함수형 프로그래밍에서는 크게 '액션', '계산', '데이터'가 있습니다.
액션
액션은 호출 시점, 횟수에 의존하고 영향을 받는 코드입니다. 부수효과를 가지고 있는 함수로 이해했습니다.
계산
계산은 입력값을 계산해 출력 값으로 리턴해주는 코드입니다. 저는 순수함수 개념과 동일하게 이해했습니다.
데이터
데이터는 용어 그대로 이벤트에 대한 사실입니다.
함수형 프로그래밍의 핵심은
- 🍕액션에서 계산을 분리해 낸다.
- 계산도 다 같은 계산이 아니다. ➗ 계산을 분류한다.
- 계산을 만들기 위한 📸 'Copy on Write' 기법
- 액션을 더 나은 💥 액션으로 만든다.
이로 간추려집니다. 계산을 최대한 늘리는 게 중요합니다. 하지만 이 책이 참 실용적이라고 느낀 점이 있습니다. 액션(부수효과)은 필요하다고 말합니다. 중요한 건 액션과 계산을 분리해 내고, 액션을 더 나은 액션으로 개선해서 사용하는 것이죠.
액션에서 계산을 분리해 낸다.
액션과 계산을 분리해 내기 위해서는 계산이 아닌 지점을 먼저 찾아야 합니다. 그리고 이것을 계산으로 빼낼 수 있는지, 실제로 계산으로 빼냈을 시 이득을 볼 수 있을지 등등을 생각해야 합니다. 책에서 나오는 간단한 예제를 가져와 보겠습니다.
function calc_cart_total() {
shopping_cart_total = 0;
for(let i = 0; i < shopping_cart.length; i++) {
let item = shopping_cart[i];
shopping_cart_total += item.price;
}
... (생략) ...
}
우선 이 함수는 너무 많은 일을 하고 있습니다. 분리시켜볼게요.
function calc_cart_total() {
calc_total();
... (생략) ...
}
function calc_total() {
shopping_cart_total = 0;
for(let i = 0; i < shopping_cart.length; i++) {
let item = shopping_cart[i];
shopping_cart_total += item.price;
}
}
이제 여기서 calc_total() 함수는 액션 덩어리입니다. 계산으로 바꿔보겠습니다.
function calc_cart_total() {
shopping_cart_total = calc_total(shopping_cart);
... (생략) ...
}
function calc_total(cart) {
let total = 0;
for(let i = 0; i < cart.length; i++) {
let item = cart[i];
total += item.price;
}
return total
}
어떤 점이 바뀐 거 같나요. 바뀐 점을 정리해 보면 아래와 같습니다.
개선된 점
- 부수효과를 제거했습니다. 전역변수를 제거하고, 지역변수 변경했기 때문이죠.
- 명시적인 입력인 shopping_cart를 제거하고, cart라는 인자로 넘겨줬습니다. 책에서는 암묵적인 입력/출력을 명시적인 입력/출력으로 교체해 준 것이라고 설명하고 있습니다.
계산을 만든 다는 것이 어렵지 않죠?!
계산도 다 같은 계산이 아니다. 계산을 분류한다.
계산을 만들었다고 끝이 아닙니다. 계산도 개선의 여지가 있습니다. 코드를 계층으로 설계할 수 있습니다.
유틸리티 > 일반적인 규칙 > 비즈니스 규칙 > 레이아웃
유틸리티에 가까울수록 재사용성이 커지고, 변경이 덜합니다. 유틸리티성 계산이 많아질수록 생산성에 유리할 것 같지만, 그건 현실적으로 불가능합니다. 우리가 해야 할 일은 적절한 계층에 적절한 계산 함수가 위치할 수 있도록 설계하는 것입니다.
마찬가지로 책에서 예제를 가져와 보겠습니다.
function add_item(cart, name, price) {
let new_cart = cart.slice();
new_cart.push({
name: name,
price: price,
});
return new_cart;
}
위에 기존 함수는 계산입니다. 이 계산 함수를 분류해보겠습니다.
function add_element_last(array, elem) {
let new_array = array.slice();
new_array.push(elem);
return new_array;
}
function add_item(cart, item) {
return add_element_last(cart, item)
}
function make_cart_item(name, price) {
return {
name: name,
price: price
}
}
function add_item_to_cart(...) {
let item = make_cart_item(name, price);
shopping_cart = add_item(shopping_cart, item);
...(생략)...
}
어떻게 분류된 거 같나요?
개선된 점
- add_element_last()라는 유틸성 함수를 분리해 냈습니다. 이 유틸성 함수는 다른 곳에서도 재사용할 수 있습니다.
- cart에 대한 계산과 item에 대한 계산을 분리했습니다. 서로 다른 관심사는 분리시키고, 같은 관심사는 응집시켰습니다. 때문에 각 코드에 대한 가독성이 올라갔습니다.
막상 까보니 어렵지 않습니다. 우린 이미 실전에서 이런 작업들을 하고 있을 겁니다. 😙
계산을 만들기 위한 'Copy on Write' 기법
이 글을 보시는 여러분들은 값의 복사(Call By Value)와 주소의 복사(Call by Reference) 개념을 아실 겁니다. Javascript에서 함수 인자는 원시데이터(순자, 문자 등등) 경우 값의 복사가 일어나지만, 객체의 경우 주소의 복사가 일어납니다. 이 둘이 어떤 영향을 줄 수 있는지는 구글링을 통해 알아보시기 바랍니다. 여기서는 굳이 설명하지 않겠습니다.
우리는 계산을 만들기 위해서는 이 주소의 복사(Call By Reference) 경우를 대비해야 합니다. 대비방법은 함수에서 인자를 넘겨받는 그 즉시 사본을 만드는 겁니다. 이를 📸 Copy on Write 기법이라고 명칭 합니다.
사본을 만든다는 건 아래와 같은 형태입니다.
const getItemsSorted = (items, orderBy) => {
const itemsCopied = items.slice();
// const itemsCopied = [...items];
return itemsCopied
}
두 가지 경우가 있습니다.
- 배열은 slice(), 분해할당 [...]을 해주면 됩니다.
- 객체는 Object.assign({ }, object), 분해할당{...}을 해주면 됩니다.
여기서 좀 더 살펴볼 개념은 바로 얕은 복사입니다. 객체에 대한 복사는 기본적으로 깊은 복사가 아닌 얕은 복사입니다. 가장 최상에 있는 데이터는 복사가 되지만, 그 아래 참조데이터들은 여전히 기존 참조값을 가리킵니다.
구조적 공유를 적극 활용할 겁니다.
구조적 공유 : 두 중첩된 데이터 구조에서 안쪽 데이터가 같은 데이터를 참조
변경이 필요한 부분만 복사해 가는 기법을 소개합니다.
onst setPriceByName = (cart, name, price) => {
let copiedCart = cart.slice();
for (const i in copiedCart) {
if (copiedCart[i].name === name) {
copiedCart[i] = setPrice(copiedCart[i], price);
}
}
return copiedCart
}
// 2차 구조에서도 카피온 라이트 패턴을 활용하고, 변경이 필요한 부분만 변경한다.
const setPrice = (item, newPrice) => {
let copiedItem = { ...item };
copiedItem.price = newPrice;
return itemCopied
}
여기서 핵심은 바로 setPrice() 함수입니다. cart를 복사했지만 cart 안에 담긴 item은 여전히 기존 참조값을 가리키고 있습니다. 이때 item의 가격(price)을 바로 바꾸면, 이는 기존 item 의 가격을 변경하게 되어서 부수효과를 만들게 됩니다.
여기서는 굳이 부수효과를 만드는 것보다 제거하는 것이 이득입니다.
이런 부수효과를 제거하려면, item 도 사본을 만들고 그 사본의 가격(price)를 변경한 다음 이 사본을 cart 사본에 반영하는 겁니다. 순서를 정리하자면 아래와 같습니다.
cart 사본을 만든다 ➡️
item 사본을 만든다 ➡️
item 사본의 price를 변경한다 ➡️
item 사본을 사본 cart의 기존 item자리에 교체시킨다.
이런 식으로를 구조적 공유를 활용하면 장점이 있습니다. 바로 메모리의 사용을 최소화할 수 있습니다. 깊은 복사는 모든 데이터의 사본을 일일이 만드는 작업입니다. 데이터의 크기가 크면 클수록 이 작업은 컴퓨팅 자원을 잡아먹습니다.
하지만 이렇게 필요한 부분만 그때그때 얕은 복사를 하는 전략은 컴퓨팅 자원을 적게 소비합니다.
계산에 집착하는 이유
함수형 프로그래밍은 계산을 액션에서 분리시키고 분류하고, 늘려가는 작업이라고 해도 좋을 것 같습니다. 물론 더 높은 난도로 가면 갈수록 더 복잡한 기법들이 있겠죠. 중요한 건 계산이 뭐가 그리 좋길래 계산을 만드는 게 중요하냐는 겁니다. 계산은 다음과 같은 장점이 있습니다.
- 테스트하기 쉽다.
- 재사용하기 좋다
- 동시성에 유리하다
- 설계에 유리하다
- 데이터 모델링에 유리하다
저는 이 중에서도 테스트, 재사용성, 동시성이라는 키워드가 핵심이라고 생각합니다. 계산의 장점은 함수형 프로그래밍의 장점이라고 이야기해도 무방해 보입니다.
마무리
이제 막 스터디를 시작했고, 1~6 챕터까지 학습을 했는데요. 책 내용을 그대로 옮기지 않고 보다는 제가 생각하는 핵심, 실무에 사용 가능한 지식 등등을 재가공해서 정리했습니다. 다음은 7 ~ 12 챕터를 읽어 볼 건데요. 조만간 이어서 연재해 보겠습니다.
그리고 좀 더 실무적인 내용과 연관 지어서 지식을 확장하고자 하는데요. 특히 프런트 진영에서 React의 18 버전부터 동시성에 대한 기능이 강화되었습니다. 이 내용을 좀 더 살펴보고 추가로 정리해 보겠습니다.
스터디가 관심 있으신 분들은 '스터디캠프 첫 오픈' 글을 참고해 주세요.
'Dev.' 카테고리의 다른 글
함수형 프로그래밍 직접 해본 경험 (5) | 2023.11.24 |
---|---|
함수형 코딩 - 방어적 복사 Vs. Copy On Write (0) | 2023.07.31 |
Playwright, Auth 자동화와 API Mocking (0) | 2023.06.13 |
테스트도 전략이다 (0) | 2023.05.27 |
E2E 테스트로 왜 Playwright 선택했는가? (0) | 2023.05.27 |