함수형 프로그래밍은 저번 포스팅에서 설명했던 선언형 프로그래밍에 속하며, 말 그대로 순수 함수를 조합해 프로그램을 만듭니다. 함수형 프로그래밍의 특징을 요약하자면 다음과 같습니다.
부수 효과가 없는 순수 함수를 1급 객체로 간주하여 파라미터나 반환값으로 사용할 수 있으며, 참조 투명성을 지킬 수 있다.
과연 이게 어떤 뜻일까요? 모르는 개념들을 하나씩 짚어 보면서 설명해 보겠습니다.
부수 효과(Side Effect)가 없는 순수 함수(Pure Function)란?
부수 효과를 발생하는 함수
부수 효과의 영단어인 Side Effect의 뜻은 "부작용"입니다. 즉, 부수 효과가 있는 함수는 프로그램에 "부작용"을 일으켜 원래의 목적과 다른 결과를 만듭니다.
간단한 예시를 들어 보겠습니다.
let numbers = [1, 2, 3, 4, 5];
function doubleNumbers() {
for (let i = 0; i < numbers.length; i++) {
numbers[i] = numbers[i] * 2;
}
}
function sumNumbers() {
let sum = 0;
for (let number of numbers) {
sum += number;
}
return sum;
}
// 배열 원소에 2를 곱하고, 모두 더하고 싶을 때
doubleNumbers();
console.log(sumNumbers()); // 예상 결과: 30, 실제 결과: 30
... 중략
// 원래 배열의 원소를 더하고 싶을 때
console.log(sumNumbers()); // 예상 결과: 15, 실제 결과: 30
숫자를 가진 number 배열이 있고, 두 함수는 이 배열에 접근해 기능을 수행합니다. 배열 원소에 2를 곱하고, 그 합을 구하는 프로그램을 작성해 보겠습니다.
짠! 우리가 원하는 결과를 얻었습니다. 하지만 이제 sumNumber의 반환 값은 2배가 되었고, doubleNumbers의 호출 횟수마다 계속 배로 늘어날 것입니다.
즉, doubleNumbers는 프로그램에 "부작용"을 일으킨 것이고, 이를 부수 효과(Side Effect)가 있는 함수라고 합니다.
불변성을 지키지 못하는 함수
function doubleNumbers(numbers) {
for (let i = 0; i < numbers.length; i++) {
numbers[i] = numbers[i] * 2;
}
return numbers;
}
function sumNumbers(number) {
let sum = 0;
for (let number of numbers) {
sum += number;
}
return sum;
}
const originalNumbers = [1, 2, 3, 4, 5];
// 배열 원소에 2를 곱하고, 모두 더하고 싶을 때
const doubledNumbers = doubleNumbers(number);
console.log(sumNumbers()); // 예상 결과: 30, 실제 결과: 30
... 중략
// 원래 배열의 원소를 더하고 싶을 때
console.log(sumNumbers()); // 예상 결과: 15, 실제 결과: 30
위 코드에서는 number 배열을 직접 참조해 "부수 효과"가 발생했습니다. 그렇다면 파라미터로 직접 참조를 피한다면 부수 효과를 없앨 수 있을까요?
배열은 참조 자료형이기 때문에, 파라미터로 주소를 전달하는 Call-By-Reference 방식입니다. 그래서 두 함수는 originalNumber 배열에 직접 접근해, 값을 바꿔 버립니다.
그래서 또 doubleNumbers의 호출 횟수에 따라 sumNumbers의 결괏값이 달라져 버렸습니다!
부수 효과가 없는 순수 함수
function doubleNumbers(numbers) {
return numbers.map(number => number * 2);
}
function sumNumbers(numbers) {
return numbers.reduce((sum, number) => sum + number, 0);
}
// 함수 호출
const originalNumbers = [1, 2, 3, 4, 5];
const doubledNumbers = doubleNumbers(originalNumbers);
console.log(sumNumbers(doubledNumbers)); // 예상 결과: 30, 실제 결과: 30
내부적으로 새 배열을 생성해 반환하는 map과 reduce 함수로 대체했습니다. 그러므로 이제originalNumbers의 값을 변경하지 않을 것이고, 드디어 "부수 효과가 없는 함수"가 될 수 있습니다.
이처럼, "부수 효과가 없는 함수"를 순수 함수(Pure Function)이라고 합니다. 순수 함수는 오직 함수 내의 상황에만 의존하기 때문에, 다음과 같은 장점이 있습니다.
- 입력 값이 같다면 출력 값 또한 같습니다. 결과를 예측할 수 있어 유지 보수성이 향상됩니다.
- 프로그램에 어떤 영향도 주지 않아, 병렬 처리에서 발생하는 동기화 문제에서 자유롭습니다.
함수를 1급 객체로 간주한다란?
함수형 프로그래밍에서는 함수 자체를 다른 함수의 파라미터나 반환 값으로 사용할 수 있습니다. 이게 어떤 뜻이고, 어떤 의미를 가질까요?
이 조건은 "선언형 프로그래밍"을 만족하기 위해 만들어졌습니다. 함수의 주요 동작(WHAT)을 잘 파악하기 위해, 구체적인 방법(HOW)을 추상화한 것이 "선언형 프로그래밍"입니다. 함수에게 동작을 설명해 주려면 어떤 정보를 넘겨줘야 할까요? 바로 "함수"입니다.
함수를 파라미터로 전달하는 경우
말이 좀 어렵죠? 사실, 바로 위 예제에서 함수를 1급 객체로 파라미터로 전달해 주었습니다.
function doubleNumbers(numbers) {
return numbers.map(number => number * 2);
}
map은 "배열의 각 요소를 순회하고, 결과 값을 새 배열로 반환"하는 동작을 추상화한 함수입니다. 그런데 우리는 "원소에 2를 곱하고" 싶습니다. 그래서 (number => number * 2)라는 함수를 넘겨준 것이지요.
함수를 반환 값으로 전달하는 경우
그렇다면 함수를 반환 값으로 사용하는 경우는 무엇일까요? 여러 가산기(Adder)가 필요한 상황이라고 가정해 보겠습니다.
function addFive(number) {
return number + 5;
}
function addTen(number) {
return number + 10;
}
위 코드처럼 필요할 때마다 새로운 가산기를 만들 수 있습니다. 하지만 가산기가 100개가 필요할 경우, 일일이 함수 100개를 만들어야 할까요? 그중 하나는 실수할 수 있지 않을까요?
function createAdder(addend) {
return function(number) {
return number + addend;
};
}
// 사용 예
const addFive = createAdder(5);
const addTen = createAdder(10);
console.log(addFive(3)); // 8 (3 + 5)
console.log(addTen(3)); // 13 (3 + 10)
"가산기를 만드는 함수"인 createAdder 함수를 만들었습니다. 이제 숫자에 맞는 가산기를 원하는 대로 활용할 수 있습니다. 이처럼, 고차 함수를 반환하는 것은 함수의 재사용성과 캡슐화를 더욱 향상합니다.
참조 투명성(Referential Transparency)이란?
참조 투명성(Referential Transparency)의 의미는 다음과 같습니다.
- 함수의 입력 값이 동일하다면, 반환하는 값 또한 항상 동일하다.
- 함수 수행 이후에도 입력 값은 변경되지 않는다.
어디서 많이 들어본 것 같죠? 우리가 아까 "순수 함수"로 부수 효과를 제거한 것과 같습니다. 즉, 참조 투명성을 만족한다는 것은, "부수 효과"를 제거해 개발자가 결과를 쉽게 예측할 수 있다는 의미입니다.
'IT > SW Engineering' 카테고리의 다른 글
명령형 vs 선언형 프로그래밍 (1) | 2023.12.30 |
---|