함수형 프로그래밍 이해하기
이 글을 쓰는 저 역시 함수형 프로그래밍에 대하여 들어만 보았을 뿐 실제로 코드를 작성해서 프로젝트에 적용해본 적은 없는 것 같습니다. 그럼에도 프로젝트에서 실제로 사용하기도 하고 어떠한 것을 알고 있음과 없음은 큰 차이가 있고 누군가는 이 글을 읽고 함수형 프로그래밍의 매력을 느껴 새롭게 함수형 프로그래밍을 배우는 계기가 될지도 모릅니다.
📖 프로그래밍 패러다임(Programming Paradigm)]
함수형 프로그래밍이 무엇인지 이해하기 전에 프로그래밍 패러다임에 대해 잠시 알아보려고 합니다.
프로그래밍 패러다임(Programming Paradigm)은 프로그래머에게 프로그래밍의 관점을 갖게 하고 코드를 어떻게 작성할지 결정하는 역할을 하는데 새로운 프로그래밍 패러다임을 통해서는 새로운 방식으로 생각하는 법을 배우게 되고, 이를 바탕으로 코드를 작성하게 됩니다.
프로그래밍 패러다임은 크게 아래와 같이 2가지로 구분할 수 있습니다.
- 명령형 프로그래밍: 무엇(What)을 할 것인지 나타내기보다 어떻게(How) 할 건지를 설명하는 방식
- 절차지향 프로그래밍: 수행되어야 할 순차적인 처리 과정을 포함하는 방식 (C, C++)
- 객체지향 프로그래밍: 객체들의 집합으로 프로그램의 상호작용을 표현 (C++, Java, C#)
- 선언형 프로그래밍: 어떻게 할 건지(How)를 나타내기보다 무엇(What)을 할 건지를 설명하는 방식
- 함수형 프로그래밍: 순수 함수를 조합하고 소프트웨어를 만드는 방식 (클로저, 하스켈, 리스프)
- 명령형 프로그래밍 예시
for문을 사용해서 배열의 각 요소에 multiplier 곱해주는 함수입니다. 함수형 프로그래밍에서는 if, switch, for 등의 명령문을 사용하지 않고 함수형 코드로 사용해야 합니다.
let numbers = [1, 2, 3];
function multiply(numbers, multiplier) {
for (let i = 0; i < numbers.length; i++) {
numbers[i] = numbes[i] * multiplier;
}
}
- 선언형 프로그래밍 예시
위의 명령형 프로그래밍에서 사용한 for문을 map으로 대치했습니다. Javascript 에서는 filter(if 대용), map(for 대용), take(break 대용), reduce(축약 및 합산) 등의 함수형 코드를 사용합니다.
// 선언형 프로그래밍
function multiply(number, multiplier) {
return number.map((num) => num * multiplier);
}
📖 함수형 프로그래밍이란?
함수형 프로그래밍은 하나의 프로그래밍 패러다임으로 정의되는 코딩 접근 방식으로, 자료처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임을 의미합니다.
함수형 프로그래밍 언어로 설계된 클로저, 스칼라, 하스켈 등의 언어가 있고, 자바스크립트, 코틀린, 파이썬 등에도 최근 버전에 함수형 프로그래밍 문법이 추가되었습니다.
😉 함수형 프로그래밍을 배워야 하는 이유
잠시 당신이 나무꾼이라고 가정해보자. 당신은 숲에서 가장 좋은 도끼를 가지고 있고, 그래서 가장 일 잘하는 나무꾼이다. 그런데 어느 날 누가 나타나서 나무를 자르는 새로운 패러다임인 전기톱을 알리고 다닌다. 이 사람이 무척 설득력이 있어서 당신은 사용하는 방법도 모르면서 전기톱을 사게 된다. 당신은 여태껏 했던 방식대로 시동을 걸지도 않고 전기톱으로 나무를 마구 두들겨댄다. 곧 당신은 이 새로운 전기톱은 일시적인 유행일 뿐이라고 단정하고 다시 도끼를 쓰기 시작한다. 그때 누군가 나타나서 전기톱의 시동 거는 법을 가르쳐 준다.
— 닐포드 , “함수형 사고”
함수형 프로그래밍은 기존 절차적 프로그래밍과 객체 지향형 프로그래밍과는 다른 새로운 방식으로 함수형 프로그래밍을 배우는 것은 스칼라나 클로저에 관심이 없고 앞으로 쓰지 않을 것 같다고 해도 다른 방식으로 사고하는 법을 배우는 것을 의미합니다. 새로운 계산방법을 배우는 것처럼 사고의 전환을 필요로 하며 다양한 사고방식으로 프로그래밍을 바라보면 유연한 문제 해결이 가능하게 됩니다.
💡 함수형 프로그래밍(Functional Programming)의 특징
함수형 프로그래밍의 특징을 정의하면 부수 효과가 없는 순수 함수를 1급 객체로 간주하여 파라미터나 반환 값으로 사용할 수 있으며, 참조 투명성을 지킬 수 있습니다.
✅ 부수효과(Side Effect)
여기서 부수효과(Side Effect)란 다음과 같은 변화 또는 변화가 발생하는 작업을 의미합니다.
- 변수의 값이 변경됨
- 자료 구조를 제자리에서 수정함
- 객체의 필드 값을 설정함
- 예외나 오류가 발생하며 실행이 중단됨
- 콘솔 또는 파일 I/O가 발생함
✅ 순수 함수(Pure Function)
부수 효과(Side Effect)들이 제거된 함수를 순수 함수(Pure Function)라고 부르며, 함수형 프로그래밍에서 사용하는 함수는 순수 함수입니다.
- 동일한 입력에는 항상 같은 값을 반환해야 하는 함수
- 함수의 실행이 프로그램의 실행에 영향을 미치지 않아야 하는 함수
- 함수 내부에서 인자의 값을 변경하거나 프로그램 상태를 변경하는 Side Effect가 없는 것
- 프로그램의 변화 없이 입력 값에 대한 결과를 예상할 수 있어 테스트가 용이
아래의 예제에서는 add라는 함수 안에서 전역으로 선언된 변수인 num을 참조하기 때문에 순수 함수라고 볼 수 없습니다.
let num = 1;
function add(a) {
return a + num;
}
- 순수함수 예제
add의 함수가 프로그램 실행에 영향을 미치지 않고 입력 값에 대해서만 값의 변환이 있으므로 순수 함수입니다.
// 순수함수
function add(a, b) {
return a + b;
}
const result = add(2, 3);
👉 순수 함수(Pure Function)의 장점
순수 함수(Pure Function)를 이용하면 얻을 수 있는 효과는 다음과 같습니다.
- 함수 자체가 독립적이며 Side-Effect가 없기 때문에 Thread에 안전성을 보장받을 수 있음.
- Thread에 안정성을 보장받아 병렬 처리를 동기화 없이 진행할 수 있음.
✅ 참조 투명성(Referential Transparency)
참조 투명성이란 함수를 실행하여도 어떠한 상태의 변화 없이 항상 동일한 결괏값을 반환하여 항상 동일하게(투명하게) 실행 결과를 참조(예측)할 수 있다는 것을 의미합니다. 부수효과를 제거하고 프로그램의 동작을 이해하며 예측을 용이하게 하는 것은 함수형 프로그래밍으로 개발하려는 핵심 동기 중 하나입니다.
정리 :
- 함수형 프로그래밍에서 데이터는 참조 투명성을 통해 기존의 값은 변경되지 않는 불변성(Immutability)을 유지해야 함.(Immutable Data)
- 데이터의 변경이 필요한 경우, 원본 데이터 구조를 변경하지 않고 그 데이터의 복사본을 만들어서 작업을 진행.
let person = { name: "jongmin", age: "26" };
function increaseAge(person) {
person.age = person.age + 1;
return person;
}
위의 예제에서는 increaseAge 함수에서 전역으로 선언된 person의 age 속성을 변경하므로 불변성 유지를 만족하지 못합니다.
// 비상태, 불변성 만족
const person = { name: "jongmin", age: "26" };
function increaseAge(person) {
return { ...person, age: person.age + 1 };
}
✅ 1급 객체와 고차 함수 (Fist-class Object, Higher-order functions)
함수형 프로그래밍에서는 함수가 1급 객체가 됩니다. 1급 객체의 특징은 다음과 같습니다.
- 변수나 데이터 구조안에 담을 수 있음.
- 파라미터로 전달할 수 있음.
- 반환 값(return value)으로 사용할 수 있음.
- 할당에 사용된 이름과 관계없이 고유한 구별이 가능.
- 동적으로 프로퍼티 할당이 가능.
- 1급 객체 예제
// 1급 객체
const addTwo = (num) => num + 2;
const multiplyTwo = (num) => num * 2;
const transform = (numbers) => numbers.map(addTwo).map(multiplyTwo);
console.log(transform([1, 2, 3, 4])); // [6, 8, 10, 12]
위의 예시에서는 함수를 변수에 할당하거나 반환하는 1급 객체로서의 특징을 보여 줍니다.
함수형 프로그래밍에서는 고차 함수를 사용합니다. 고차 함수의 특징은 다음과 같습니다.
- 함수를 인자로써 전달할 수 있어야 함.
- 함수의 반환 값으로 또 다른 함수를 사용할 수 있음
- 고차 함수 예제
함수의 반환 값으로 다른 함수를 사용하거나, 함수의 반환 값으로 또 다른 함수를 사용할 수 있어야 합니다.
// 고차 함수
const addInform = (name) => (age) => age + name;
const jongmin = addInform("종민");
console.log(jongmin("96")); // 96종민
💻 함수형 프로그래밍의 장단점
장점
- 높은 수준의 추상화를 제공함
- 함수 단위의 코드 재사용이 수월
- 불변성을 지향하기 때문에 프로그램의 동작을 예측하기 쉬워짐
단점
- 순수 함수를 구현하기 위해서는 코드의 가독성이 좋지 않을 수 있음
- 함수형 프로그래밍에서는 반복이 for문이 아닌 재귀를 통해 이루어지는데 (deep copy), 재귀적 코드 스타일은 무한 루프에 빠질 수 있음
- 순수함수를 사용하는 것은 쉬울 수 있지만 조합하는 것은 쉽지 않음
참고 :