함수형 코딩

#book#functional-programming#javascript
• • •

Chapter 1

함수형 프로그래밍의 사전적 정의는 다음과 같다.

  1. 수학 함수를 사용하고 부수 효과를 피하는 것이 특징인 프로그래밍 패러다임
  2. 부수 효과 없이 순수 함수만 사용하는 프로그래밍 스타일

이 내용은 실용적이지 않다. 실제 세계에서 부수 효과는 소프트웨어를 사용하는 이유가 되기도 한다. 마치 순수 함수만 사용하는 것이 함수형 프로그래밍인 것처럼 오해할 수 있지만 함수형 프로그래머는 순수하지 않은 함수를 잘 다룰 줄도 알아야 한다. 사전적 정의는 함수형 프로그래밍을 시작하려는 사람에게 혼란을 줄 수 있기 때문에 저자는 함수형 프로그래밍을 학문적 지식이 아닌 실용적이고 유익한 기술과 개념으로 소개한다.

부수효과는 함수가 리턴값 이외에 하는 모든 일을 말한다. 이메일 보내기, 파일 읽기 등...

순수 함수는 인자에만 의존하고 부수 효과가 없는 함수를 말한다.

함수형 프로그래머는 코드를 세 분류로 나눈다.

  1. 액션 - eg. sendEmail() saveUser() getCurrentTime()
  2. 계산 - eg. sum() string_length()
  3. 데이터 - eg. [12, 13, 14, 15]

액션은 호출하는 시점과 횟수에 의존하기 때문에 부를 때 신중해야 한다. 반면 계산이나 데이터는 그렇지 않기 때문에 상대적으로 다루기 쉽고 이해하기에도 쉽다.

Chapter 2

함수형 사고에서 가장 먼저 해야 할 것은 액션계산, 데이터를 구분하는 것이다. 쉽게 다룰 수 있는 부분과 조심히 다뤄야 할 부분을 명확하게 나눌 수 있기 때문이다.

변경 가능성에 따라 코드를 나눠보자.

  • 자주 바뀌는 것 - 비지니스 규칙 - eg. 이번 주 한정 이벤트
  • 덜 자주 바뀌는 것 - 도메인 규칙 - eg. 피자 레시피
  • 자주 바뀌지 않는 것 - 기술 스택 - eg. 자바스크립트의 객체와 배열

각 계층은 아래에 있는 계층에 기반한다. 이런 구조로 소프트웨어를 만들면 코드를 쉽게 변경할 수 있다. 가장 위에 있는 코드는 의존성이 거의 없기 때문에 쉽게 바꿀 수 있다. 이러한 형태가 일반적인 계층형 설계의 모습이다. 계층형 설계로 만든 코드는 테스트, 재사용, 유지보수가 쉽다.

순차적인 프로그램을 분산 시스템으로 바꾸는 것은 쉽지 않다. 각 시스템의 타임라인이 다르기 때문인데 여러 타임라인이 동시에 진행될 때 서로 순서를 맞추는 방법을 커팅이라 한다. 커팅은 액션을 올바른 순서로 실행할 수 있도록 보장한다.

Chapter 3

모든 개발 과정에서 액션과 계산, 데이터를 구분하는 기술을 적용할 수 있다. 코딩을 시작하기 전 구상 단계에서 요구 사항을 액션과 계산, 데이터로 나눠 생각해 볼 수 있다. 이렇게 하면 구현하도 전에 특별히 주의해야할 부분을 미리 알 수 있다. 코드를 읽거나 작성할 때도 이 방법을 적용하면 더 좋은 코드를 만들기 위한 방법을 찾을 수 있다.

액션의 또 다른 이름은 순수하지 않은 함수(impure function), 부수 효과 함수(side-effecting function)다. 액션은 다루기 힘들고 액션의 실행 결과는 코드 전체로 퍼질 수 있기 때문에 주의해서 다루어야 한다. 액션을 부르는 함수가 있다면 그 함수도 액션이 된다. 액션을 전혀 사용하지 않을 수 없기 때문에 액션은 가능한 적게 사용하고 가능한 작게 만드는 것이 좋다. 액션에서 액션과 관련이 없는 코드가 있다면 제거하거나 분리하자.

계산의 또 다른 이름은 순수 함수(pure function)다. 계산은 액션보다 사용하기 쉽고 이해하기에도 쉽다. 액션보다 테스트하기에 좋고 기계적인 분석이 쉽다. 계산은 조합하기도 좋다. 계산을 조합하면 더 큰 계산을 만들 수 있다. 계산을 사용할 때는 실행 시점이나 횟수에 신경쓰지 않아도 되는 장점이 있다. 다만 계산도 액션과 마찬가지로 실행하기 전에는 어떤 일이 발생하지 알 수 없다는 단점이 있기 때문에 이러한 단점이 싫다면 데이터를 사용해야 한다.

데이터는 이벤트에 대한 사실이다. 사실은 변하지 않기 때문에 영구적으로 기록할 수 있다.

Chapter 4

함수에는 입력과 출력이 있고 입력과 출력은 명시적이거나 암묵적일 수 있다.

아래 함수에서 입력과 출력, 그리고 명시적인 것과 암묵적인 것을 구분하자면,

const add = (amount) => { // amount는 명시적 입력
	console.log(amount); // console은 암묵적 출력
	total += amount; // total를 갱신하는 것도 암묵적 출력
	return total; // 리턴 값은 명시적 출력
}

함수에 암묵적 입력과 출력이 있으면 액션이다. 반대로 암묵적인 것 없이 모두 명시적이라면 계산이다. 따라서 add()는 액션이다. 함수형 프로그래밍에서 암묵적 입력과 출력을 부수 효과라고 부른다.

add()를 계산으로 바꾸자면,

const add = (total, amount) => {
	return total + amount;
}

이제 암묵적 입력과 출력 모두 없기 때문에 add()는 계산이 되었다. 액션을 계산으로 바꾸거나 액션에서 계산을 빼내는 것은 코드의 재사용성을 높이고 테스트하기 좋은 코드를 만드는 방법이다.

Chapter 5

일반적으로 암묵적 입력과 출력은 인자와 리턴값으로 바꿔서 걷어내는 것이 좋다.

함수를 사용하면 관심사를 자연스럽게 분리할 수 있다.

함수는 작으면 작을수록 재사용하기 쉽다.

작은 함수는 쉽게 이해할 수 있고 유지보수하기 쉽다.

작은 함수는 테스트하기 좋다.

Chapter 6

카피온라이트는 원본 데이터를 바꾸지 않고 복사본을 변경하는 원칙이다.

  1. 복사본 만들기
  2. 복사본 변경하기
  3. 복사본 리턴하기

카피온라이트를 하면 쓰기 함수를 읽기 함수로 바꿀 수 있다. 쓰기는 데이터를 변경하기 때문에 쓰기가 없다면 데이터는 불변형이라고 할 수 있다. 변경 가능한 데이터를 읽는 것은 액션이고 변경 불가능한 데이터를 읽는 것은 계산이다. 바뀔 때마다 복사를 하는 것이 너무 비효율적이라고 말할 수도 있다. 하지만 최적화는 언제든지 가능하기 때문에 필요할 때 하면 된다. 현대 언어의 가비지 콜렉터 성능은 매우 빠른 편이고 대부분의 복사는 얕게 이루어지기 때문에 생각보다 많이 복사되지 않는다. 만약 여러 객체를 원소로 갖는 원본 배열을 얕게 복사하여 일부 원소만 변경한 배열이 있다면 원본 배열과 나머지 원소를 공유하고 있는 셈이다. 이를 구조적 공유라고 한다.

Chapter 7

하지만 현실에서는 불변성을 지키기 어려운 경우가 많다. 불변성을 확신할 수 없는 오래된 레거시 코드를 갖다 써야 하거나 외부 라이브러리를 사용해야 하는 경우가 그렇다. 데이터 불변성이 지켜지는 영역을 안전지대라고 한다면 안전지대 밖으로 나가는 데이터나, 안전지대 외부로부터 들어오는 데이터는 언제든지 변경될 수 있다. 얕은 복사를 수행하는 카피온라이트 패턴만으로는 데이터의 변경 가능성을 완전히 배제시킬 수 없다. 이 책에서는 데이터가 바뀌는 것을 완벽하게 막아주는 원칙인 방어적 복사(defensive copy)를 소개한다. 사실 특별한 것은 아니고 데이터가 안전지대를 넘나들 때 깊은 복사(deep copy)를 하는 방식이다.

불변성을 보장할 수 없는 레거시 함수를 사용한다고 가정해본다.

const do_sth_with_item = (item) => {
	// ...
	return someLegacyFn(item);
}

이 함수에 원본 데이터나 얕은 복사만 수행된 데이터를 전달하는 행위는 의도하지 않은 데이터 변경을 초래할 수 있다. 이를 미연에 방지하기 위해 데이터를 깊은 복사하여 함수에 전달한다.

const do_sth_with_item = (item) => {
	// ...
	const item_copy = deepCopy(item);
	const result = someLegacyFn(item_copy);
	return deepCopy(result);
}

데이터가 안전지대에서 나가는 경우 뿐만 아니라 들어올 때도 깊은 복사를 수행해야 한다. 이 두 규칙을 지키면 불변성 원칙을 지키면서 신뢰할 수 없는 코드와 상호작용할 수 있다.

안전하지 않은 함수를 감싸는 wrapper 함수를 만드는 것도 재사용성이나 가독성 측면에서 좋다.

const someLegacyFnSafe = (item) => {
	const item_copy = deepCopy(item);
	return deepCopy(someLegacyFn(item_copy));
}

const do_sth_with_item = (item) => {
	// ...
	return someLegacyFnSafe(item);
}

깊은 복사는 중첩된 데이터 전체를 복사하기 때문에 얕은 복사보다 더 많은 비용이 든다. 따라서 안전지대의 경계를 넘나드는 경우에만 사용하는 것이 효율적이다. 방어적 복사와 카피온라이트를 적절히 사용하면 안전하면서도 효율적인 코드를 작성할 수 있다.

Chapter 8

소프트웨어 설계: 코드를 만들고, 테스트하고, 유지보수하기 쉬운 프로그래밍 방법을 선택하기 위해 미적 감각을 사용하는 것

미적 감각이라니. 재밌는 정의라고 생각한다. 이 장에서는 계층형 설계에 대해 다루고 있는데 좋은 내용을 잘 요약할 수 있을지 모르겠다. 그렇다고 너무 많은 내용을 담는 것은 요약의 취지에서 벗어나기 때문에 강한 인상을 느꼈던 내용 위주로 남기려고 한다.

각 계층을 정확히 구분하기는 어렵다. 아래는 목적에 따라 계층을 구분한 하나의 예시다.

계층의 목적예시
비지니스 규칙get_free_shipping() cartTax()
장바구니를 위한 동작들add_item() calc_total()
보다 더 일반적인 함수들removeItems() add_element_last()
언어에서 지원하는 함수들.slice(), .map()

계층형 설계 감각을 키우기 위해서는 코드를 읽고 다양한 단서를 찾는 훈련이 필요하다. 가령 함수의 길이, 복잡성, 내부에서 호출하는 다른 함수, 함수의 시그니처 등이 그 단서가 될 수 있다. 이러한 단서들은 함수를 분리하거나 그 구현을 바꾸거나, 또는 데이터의 구조를 바꾸는 근거가 될 수 있다.

저자는 계층형 설계 구조를 만드는데 중요한 네 가지 패턴을 소개한다.

  • 직접 구현
  • 추상화 벽
  • 작은 인터페이스
  • 편리한 계층

이 장에서는 직접 구현에 대해 살펴본다. 함수를 읽을 때 함수 시그니처(함수명, 인자 이름, 인잣값, 리턴값)가 나타내고 있는 것들을 함수 본문에서 적절히 구현하고 있는지 검토한다. 만약 특정 기간에만 진행하는 프로모션을 위한 somePromotion()이라는 함수가 있는데 이 함수가 장바구니가 배열이라는 사실을 알고 있고 장바구니를 직접 순회해야 한다면 너무 구체적이라고 볼 수 있다. 만약 코드 안에 여러 구체화 레벨을 다루는 함수가 많아진다면 읽거나 고치기 어려워진다. 직접 구현 패턴을 적용한 함수는 한 단계의 구체화 레벨에 관한 문제만 해결하기 때문에 함수의 책임이 명확해지고 계층을 자연스럽게 구성한다.

호출 그래프를 그려 코드를 시각화 하는 것은 계층형 설계를 위한 단서를 찾는데 도움을 준다.

함수 somePromotion()은 자바스크립트의 언어 기능을 직접 사용하는 구체적인 함수다.

before

직접 구현 패턴을 사용하여 같은 추상화 수준을 하나의 계층으로 묶고 각 계층이 하나의 구체화 레벨만을 다루도록 수정하면 아래와 같은 모양을 생각해볼 수 있다.

after

이처럼 함수가 더 구체적인 내용을 다루지 않도록 일반적인 함수를 추출하면 함수가 더 명확해지고 테스트하기도 좋다. 일반적인 함수는 재사용하기에도 좋다. 계층형 설계에서 모든 계층은 바로 아래 계층에 의존해야 한다. 더 낮은 구체화 수준을 가진 일반적인 함수를 만들면서 직접 구현 패턴을 적용할 수 있다.

Chapter 9

중요한 세부 구현을 감추고 인터페이스를 제공하는 것에는 장점이 있다. 인터페이스 사용자는 구현에 대해 몰라도 API를 사용할 수 있고 인터페이스 개발자는 디테일한 내용을 사용자에게 알려주지 않아도 된다. 이러한 추상화 벽은 필요하지 않은 것은 무시할 수 있도록 해준다.

추상화 벽은 언제 사용하면 좋을까?

  • 쉽게 구현을 바꾸기 위해(단, 만약을 대비한 오버 프로그래밍은 조심해야 한다.)
  • 코드를 읽고 쓰기 쉽게 만들기 위해
  • 팀 간에 조율할 것을 줄이기 위해
  • 주어진 문제에 집중하기 위해

추상화된 인터페이스는 작게 구현해야 한다. 추상화 벽의 장점은 작은 인터페이스로 구현될 때 더 크게 누릴 수 있다. 어떤 마케팅 정책을 위한 함수가 필요하다고 가정해보자. 이 함수를 어떤 계층에 두어야 할까? 추상화 벽에 인터페이스를 하나 더 추가해야할까? 아니면 추상화 벽보다 상위 계층에 두어 추상화 벽에 있는 인터페이스를 가져다 쓰도록 하는 것이 좋을까? 추상화 벽에 함수를 둔다면 같은 계층의 함수를 사용할 수는 없다. 보다 아래 계층에 있는 낮은 수준의 코드를 사용해야 한다. 추상화 벽보다 상위 계층에 함수를 둔다면 추상화 벽에 있는 함수를 가져도 사용할 수 있어 코드가 더 단순해지고 이해하기 쉽다. 이 방법이 더 작은 인터페이스를 구현할 수 있는 방법이다. 일반적으로 상위 계층에 함수를 만들 때 현재 계층에 있는 함수로 구현하는 것이 작은 인터페이스를 실천하는 방법이 된다.

개발자는 소프트웨어를 빠르고 고품질로 제공하는 데 도움이 되는 계층에 시간을 투자해야 한다. 언제나 거대한 추상 계층을 만들 여유는 없다. 추상화를 실천하는 것은 고민이 필요한 테마다. 저자는 편리한 계층이라는 패턴으로 그 기준을 제시하고 있다. 어차피 완벽한 코드는 존재하기 어렵다. 코드의 계층이 조금 무너져도 불편한 정도가 아니라면 당장은 그대로 두어도 된다.

Chapter 10

일급

일급 값은 변수에 저장될 수 있고 어떤 함수의 인자로 전달될 수도, 함수가 값으로서 반환할 수도 있는 값을 의미한다. 프로그래밍 언어에 국한된 개념은 아니다. 언어마다 일급을 다루는 방식에는 차이가 있다. 일급을 사용하여 암묵적 인자를 명시적으로 드러낼 수 있다.

setPriceByNameprice를 포함한 여러 인자를 받아 가격을 정한다.

const setPriceByName = (cart, name, price) => {...}

이런 함수도 있다. 내부 코드는 거의 동일하다고 가정해보자. 차이는 함수의 이름과 세번째 인자뿐이다.

const setQuantityByName = (cart, name, quantity) => {...}

만약 비슷한 형태의 함수가 여럿 있다고 가정한다면 코드에서 나는 냄새를 맡을 수 있을 것이다. 이럴 때 함수의 이름에 있는 암묵적인 인자를 명시적으로 꺼내보자.

const setFieldByName = (cart, name, field, value) => {...}

함수가 받아야 하는 인자는 늘어났지만 유사하게 생긴 여러 함수를 제거할 수 있었다. 실제 호출은 이렇게 한다.

const setFieldByName(cart, "shoes", "price", 20)

field는 일급 값이다. 함수로 인자로 넘겨져 값으로 취급되기 때문에 다양한 상황에 대응할 수 있다. 만약 내부 인터페이스에서 price가 아닌 다른 이름으로 코드 변경이 필요해졌다고 하더라도 setFieldByName에는 그대로 price를 전달하여 사용할 수 있다. 내부에서 테이블 이름만 바꿔주면 된다.

const setFieldByName = (cart, name, field, value) => {
  field = fieldMap[field]; // eg. fieldMap['price'] = 'new_price_name'
  // ...
}

함수가 일급이라는 것은 함수를 인자로 전달할 수 있다는 의미다. 함수를 함수의 인자로 전달할 수 있다는 말은 고차 함수를 구현할 수 있다는 의미다. 바꾸어 말하면 고차 함수는 일급 함수가 있어야 만들 수 있다. 함수가 일급 값으로서 전달될 수 있다면 해당 함수를 배열이나 객체와 같은 자료 구조에 보관할 수도, 그냥 호출할 수도 또는 조건에 따라 선택적으로 호출할지 말지 결정할 수도 있다.

published over 2 years ago · last updated about 10 hours ago