함수형 프로그래밍이란?
선언적 프로그래밍 스타일 중 하나며, 순수 함수 구성에 중점을 둔 프로그래밍 방식
언제 함수형 프로그래밍이 나왔을까?
첫 번째 함수형 프로그래밍 언어가 나온 시기는 첫 번째 객체지향 프로그래밍 언어라고 할 수 있는 Simula(1962), Smalltalk(1972)보다도 더 먼저인 1958년에 LISP가 등장했습니다.
왜 이전에는 함수형 프로그래밍이 주목받지 못했을까?
기존에 함수형 프로그래밍이 주목받지 못했던 이유는 배우는데 시간이 좀 걸리고 어렵다고 느껴졌기 때문이었습니다. 이러한 이유로 사람의 사고방식과 가까운 절차 지향, 객체지향 프로그래밍이 많이 사용되었습니다. 하지만 요즘 AI, IoT, 빅데이터, 블록체인 등이 떠오르면서 많은 데이터를 빠르게 병렬적으로 안정적으로 처리할 필요성이 생겼고 명령형 프로그래밍의 한계를 느껴서 이에 대한 대안으로 함수형 프로그래밍이 주목받게 되었습니다.
왜 함수형 프로그래밍이 주목을 받게 되었을까?
현재 여러 패러다임이 존재하지만 함수형 프로그래밍이 주목받게 된 원인인 명령형 프로그래밍과 비교해보겠습니다.
명령형 프로그래밍 | 함수형 프로그래밍 | |
정의 | 무엇(What)을 할 것인지 나타내기보다 어떻게 할 건지(How)를 설명하는 방식 | 어떻게 할건지(How)를 나타내기보다 무엇(What)을 할 건지를 설명하는 방식 |
중점적인 시각 | 어떻게(How to)에 초점 | 무엇(What)에 초점 |
상태 변경 | 중요 | 없음 |
이론적 배경 | 튜링 머신 | 람다 계산식 |
실행 순서 | 중요 | 낮은 중요도 |
주요 흐름 제어 | 제어 구문(반복문, 조건문 등)함수(메서드) 호출 | 순환(재귀) 함수 호출 등의 함수 호출로 제어 |
주요 조작 단위 | 클래스나 구조체의 인스턴스 | 함수 |
주요 프로그래밍 언어 | C, C++, Java 등 대부분의 언어 | Scheme, Haskell, Erlang |
명령형 프로그래밍과 함수형 프로그래밍 비교한 표
현재의 컴퓨터는 멀티 코어, 멀티 스레드가 거의 기본적으로 탑재되어 있기 때문에 좀 더 안전하게 효율적으로 멀티 쓰레딩 하는 방식의 프로그래밍 설계를 선호하게 되었습니다. 따라서 부수적으로 발생되는 문제들의 해결책이 함수형 프로그래밍의 패러다임과 잘 맞물려 있기 때문이리라 생각됩니다.
함수형 프로그래밍의 핵심 개념
일급 함수 (First-Class Function)
- 함수를 변수처럼 취급할 수 있고 결과값으로 반환할 수 있으며 다른 함수에 인수로 전달될 수 있는 함수
- 일급 함수의 특징 덕분에 여러 방법들을 사용하여 코드를 더 간결하고 재사용성이 높은 코드 작성이 가능합니다.
// 화살표 함수를 사용한 함수 선언
const addNum = (a,b) => a + b;
const resultAdd = addNum(4, 6);
// 함수 키워드를 사용한 함수 선언
function squareNum(a,b) {
return a * b
}
const resultSquare = squareNum(3, 3)
// 인수로 함수를 다른 함수로 전달
const totalCalc = (resultAdd, resultSquare) => {
return resultAdd + resultSquare
}
고차 함수 (High-Order Function)
- 다른 함수를 인수로 취하거나 결과 값으로 함수를 반환할 수 있는 함수
- 고차 함수와 일급 함수 모두 함수를 다른 함수의 인수 및 결과로 허용한다는 점에서 일급 함수와 밀접하게 관련되어 있습니다.
- 둘 사이의 구분은 미묘합니다. "고차"는 다른 기능에서 작동하는 함수의 수학적 개념을 설명하는 반면 "일급"은 사용에 제한이 없는 프로그래밍 언어 개체에 대한 컴퓨터 과학 용어입니다.
// 고차 함수 미적용 예
const numbers = [1, 2, 3, 4, 5];
function power(array) {
for (let i = 0; i < array.length; i++) {
array[i] = array[i] * array[i];
}
}
power(numbers)
console.log(numbers); //[1,4,9,16,25]
// 고차 함수 적용 예
const numbersV2 = [1, 2, 3, 4, 5];
numbersV2.forEach((number, index) => numbersV2[index] = number * number);
console.log(numbersV2); //[1,4,9,16,25]
순수 함수(Pure Function)
- 입력을 받아 범위 밖의 데이터를 수정하지 않고 값을 반환하는 함수
- 주어진 입력값만 사용하여 반환 값을 계산하기에 범위 밖의 데이터에 "부작용(Side Effects)"이 없습니다.
// 순수 함수 예
// 부수효과를 발생시키지 않음
function sayGreeting(name) {
return `Hello ${name}`;
}
// 순수하지 않은 함수의 예
// greeting 값은 변경될 수 있으므로 비순수 함수
var greeting = "Hello";
function sayGreeting(name) {
return `${greeting} ${name}`;
}
// greeting 값이 "Hello" 일 경우
sayGreeting('Alex'); // "Hello Alex"
// greeting 값이 "Hola" 일 경우
sayGreeting('Alex'); // "Hola Alex"
부수효과(Side Effects) 제거
- 함수가 실행되는 과정에서 외부의 상태(외부 or 전역 변수)를 사용 or 수정하는 것을 부수효과라고 합니다.
- 부수효과가 있는 함수는 아래와 같은 단점들이 존재합니다.
- 테스트를 신뢰하기 어려움
- 디버깅하기 어려움
- 외부의 상태를 사용하는 다른 모든 메서드들에 대하여 예외 처리가 필요하며 불확실성이 많아짐
지연 연산(Lazy Evaluation)
- 어떤 값이 실제로 쓰이기 전까지 그 값의 계산을 최대한 미루는 것
- 값을 미리 계산하여 저장하지 않기 때문에 공간을 절약할 수 있고, 값이 꼭 필요할 때만 계산하기 때문에 프로그램의 성능에도 긍정적인 영향을 줍니다.
- 지연 연산은 주로 메모이제이션과 함께 사용됩니다. 값이 필요할 때 계산을 수행한 후, 다음에 해당 값이 필요할 때는 계산하지 않고 캐싱해 놓았던 값을 재사용하는 것입니다.
// 커링 함수를 사용하여 add기능을 구현하는 예
// 커링 : 모든 매개변수가 충족될 때까지 함수를 계속 반환하는 함수
// 아래 함수를 커링을 사용하여 변환한 add 함수 생성
// const add = (a, b) => a + b
const add = (a) => (b) => a + b
console.log(add) // (a) => (b) => a + b
// 첫 번째 매개변수를 전달한 add 함수를 저장하는 addOne 함수 생성
const addOne = add(2) // (b) => 2 + b
console.log(addOne) // (b) => a + b
// addOne 함수에 두 번째 매개변수를 전달하여 결과 값 출력
console.log(addOne(2)) // 4
console.log(add(2)(3)) // 5
참조 투명성(Referential Transparency)
- 프로그램 변경 없이 함수 자체를 해당 값으로 대체할 수 있는 것
- 함수 호출은 응용 프로그램의 동작을 변경하지 않고 반환하는 데이터로 직접 대체될 수 있는 경우 참조 투명으로 간주됩니다.
- 따라서 참조 투명성이 높다는 뜻은 함수가 언제 어디서 호출되더라도 입력이 같으면 항상 얻는 결과도 같다는 의미입니다.
불변성(Immutable)
- 모든 객체는 상수로 선언되어 선언된 객체의 값이나 상태가 변하지 않는 것.
- 함수형 프로그래밍에서는 한 번 초기화한 변수는 변하지 않습니다.
- 데이터 변경이 필요한 경우, 원본 데이터 구조를 변경하지 않고 그 데이터를 복사본을 만들어 그 일부를 변경하고, 변경한 복사본을 사용해 작업을 진행합니다.
- 이렇게 작업하는 이유는 결정론, 참조 투명성 및 부작용 제거와 같은 FP의 다른 속성과 매우 얽혀 있으며, 불변성을 유지함으로써 부수효과(Side Effects)를 없애고 객체의 값을 신뢰할 수 있게 하기 위함입니다.
- 그래서 함수형 프로그래밍에서 항상 함수에 대한 값에 의한 전달 원칙으로 작업합니다
const allValues = [11,23,23,34,55,22];
// slice() → 배열의 일부를 선택하고 새 배열을 반환합니다.
const subSet = allValues.slice(1, 3);
console.log(subSet); // [23,23]
// splice() → 배열에서 요소를 추가/제거하고, 제거된 배열을 반환하고, 원래 배열을 수정합니다.
const removedSet = allValues.splice(2, 3, 24,25);
console.log(removedSet); // [23, 34, 55]
console.log(allValues);//[11, 23, 24, 25, 22]
//array.splice(index, howmanyToRemove, newitem1, ….., newitemX)
const someObject = {
a: 1, b: 2
};
const newObject = {
...someObject, a: 10
};
console.log(someObject);
console.log(newObject);
장점
- 동시성 프로그램을 작성하기 쉽다 : 부작용 없이 변경할 수 없는 코드를 작성하도록 하기 때문에 병렬 프로그래밍에 자연스러운 이점을 제공합니다.
- 프로그램의 검증이 쉽다 : 함수는 항상 결정적이기 때문에 FP에서 테스트를 작성하는 것은 정말 간단합니다. 또한 기능이 서로 독립적이기 때문에 프로그램의 상태를 모방하기 위해 복잡한 시스템을 설정할 필요가 없습니다.
- 코드가 간결하다. : 함수는 상태를 변경하지 않고 주어진 입력에만 의존하기 때문에 쉽게 이해할 수 있습니다. 그들이 만들어내는 어떤 출력도 그들이 주는 반환 값입니다.
단점
- 고가의 메모리 : 데이터 스트림을 생성하고 함수로 처리하는 것은 기존 애플리케이션보다 메모리가 더 비쌉니다.
- 학습 곡선 : 개발자에게는 패러다임의 기본 아키텍처와 동작을 이해하고 더 기능적인 프로그래밍으로 생각을 전환하는 데 가파른 학습 곡선이 있습니다.
마무리
오늘은 함수형 프로그래밍에 대해서 공부한 내용을 정리해봤습니다.
여러 블로그 글들을 보다 보니 함수형 vs 객체지향 프로그래밍 관련된 글이 많이 보였습니다. 그래서 어떤 방식이 더 나은 방법일까? 궁금하면서 공부를 진행했었습니다. 공부를 해보니 구현할 기능이 복잡하거나 클 경우 객체지향이 좋은 방식이었고 기능이 단순하거나 작을 경우 함수형이 좋은 방식이라고 생각이 들었습니다.
이를 통해 느낀 건 구현에 있어서 무조건 한 가지 방식만 생각하는 건 위험할 수도 있겠다고 생각이 들었습니다. 왜냐면 다르게 생각하면 쉽게 구현할 수 있는 것을 한 가지만 고집하면 어렵게 구현할 수 있겠다고 생각이 들었기 때문이겠네요. 오랜만에 포스팅이라 마무리 글이 길어졌네요.
아직 부족하거나 틀린 부분이 있을 수도 있으니 주의하시면 좋을 거 같습니다.
이번 포스팅은 마무리하면서 다음 포스팅에서 뵙겠습니다.
참고
함수형 프로그래밍(Functional Programming, FP) 개요
'CS 지식 > 개발 방법론' 카테고리의 다른 글
모노레포(monorepo) 란? (0) | 2023.02.19 |
---|---|
TDD(Test Driven Development)란? (0) | 2021.11.02 |