함수형 프로그래밍 패러다임으로 예측 가능성 높이기

상태 변경을 줄여 예측 가능성을 높이는 방법

Table Of Contents

들어가기

여러 함수가 같은 변수를 읽고 쓰면, 언제, 어디서 값이 바뀌었는지 추적하기 어려워요. 같은 함수를 같은 인자로 호출했는데 결과가 달라지거나, 한 곳을 고쳤더니 다른 곳이 깨지는 버그는 대부분 상태 변경에서 시작돼요. 함수가 외부 변수를 읽거나 바꾸거나, 네트워크 요청을 보내는 것처럼 함수 바깥 세계에 영향을 주는 동작을 부수 효과(side effect)라고 해요. 코드가 커지고 협업 인원이 많아질수록 부수 효과에서 비롯되는 문제는 기하급수적으로 늘어나요. 시스템이 커질수록 동시에 돌아가는 작업이 늘고, 부수 효과가 여러 곳에 흩어지면 언제, 어디서 뭐가 바뀌었는지 추적이 어려워져요.

함수형 프로그래밍(Functional Programming, FP)은 부수 효과를 줄이고, 프로그램을 값을 변환하는 함수의 조합으로 보는 패러다임이에요. "명령을 나열해서 상태를 바꾸는 것"이 아니라 "입력을 받아 출력을 만드는 함수를 조합하는 것"에 집중해요. 상태보다 표현식, 명령보다 선언, "어떻게"보다 무엇을에 무게를 두는 관점이에요.

이 글에서는 함수형 프로그래밍이 해결하려는 문제, 핵심 원칙, JS와 C++에서의 적용 방식을 다뤄요.

절차적, 객체지향, 함수형 패러다임 비교하기

프로그래밍 패러다임은 "프로그램을 어떤 관점으로 바라볼 것인가"에 대한 접근이에요. 같은 문제를 풀더라도 패러다임에 따라 코드의 구조와 흐름이 달라져요. 대표적인 세 가지 패러다임을 비교해 볼게요.

절차적 프로그래밍은 명령을 순서대로 나열하고, 변수의 상태를 바꾸면서 결과를 만들어요. C 언어가 대표적이에요. "이 변수에 값을 넣고, 조건을 확인하고, 반복하면서 변수를 갱신한다"는 흐름이에요.

객체지향 프로그래밍(OOP)은 데이터(상태)와 그 데이터를 다루는 행동(메서드)을 하나의 객체로 묶어 캡슐화해요. 객체끼리 메시지를 주고받으며 프로그램이 진행돼요. Java, C++이 대표적이에요.

함수형 프로그래밍(FP)은 상태를 바꾸지 않고, 입력을 받아 출력을 만드는 순수 함수를 조합해서 프로그램을 만들어요. Haskell, Erlang이 대표적이지만, JS와 C++에서도 함수형 스타일을 쓸 수 있어요.


세 패러다임은 서로 배타적이지 않아요. JS는 함수를 1급 객체로 다루면서 프로토타입 기반 객체지향도 지원하고, C++는 절차적, 객체지향에 함수형 스타일까지 섞어 쓸 수 있어요. 대부분의 프로젝트에서는 하나의 패러다임만 쓰지 않고, 문제의 성격에 따라 섞어서 사용해요. 왜 굳이 함수형 코드를 작성해야 하는지를 이해하려면, 함수형이 어떤 원칙으로 어떤 문제를 해결하는지 알아야 해요.

함수형의 핵심 원칙

함수형 프로그래밍의 핵심 원칙은 순수 함수, 불변성, 고차 함수, 합성의 4가지에요. 각 원칙을 이 구조로 이해하면 쉬워요.

순수 함수: 예측 가능한 코드 만들기

순수 함수(pure function)란, 두 가지 조건을 만족하는 함수예요.

  1. 같은 인자를 넣으면 항상 같은 값을 반환해요.
  2. 함수 바깥의 상태를 읽거나 바꾸지 않아요(부수 효과가 없어요).

함수가 외부 변수를 읽거나 바꾸면, 호출 순서나 다른 코드의 실행에 따라 결과가 달라져요. 테스트할 때도 "이 함수를 부르기 전에 외부 상태를 이렇게 맞춰 놓아야 한다"는 전제 조건이 생겨요.

// ⚠️ 비순수 함수: 외부 변수 discount에 의존해요 let discount = 0.1; function getPrice(price) { return price * (1 - discount); // 다른 함수가 discount를 바꾸면 결과가 달라져요 } // ✅ 순수 함수: 인자만 사용해요 function getPrice(price, discountRate) { return price * (1 - discountRate); }

순수 함수는 입력만 알면 출력을 예측할 수 있어요. 테스트할 때 외부 환경을 맞출 필요가 없고, 병렬로 실행해도 서로 간섭하지 않아요. 날짜, 난수처럼 호출마다 결과가 달라지는 값도 인자로 넣어주면 테스트와 재현이 쉬워져요.


이 성질을 참조 투명성(referential transparency)이라고 해요. 어떤 표현식을 그 결과 값으로 치환해도 프로그램의 동작이 바뀌지 않는 성질이에요.

// getPrice(1000, 0.1)은 항상 900을 반환해요 const total = getPrice(1000, 0.1) + getPrice(2000, 0.1); // 이렇게 바꿔도 프로그램 동작이 똑같아요 const total = 900 + 1800;

함수 호출을 결과 값으로 바꿔도 의미가 달라지지 않으니, 코드를 읽을 때 함수 안을 들여다볼 필요 없이 "이 함수는 이 값을 돌려주는구나"라고 바로 이해할 수 있어요. 같은 인자에 같은 결과가 보장되니 한 번 계산한 결과를 캐시해 두는 메모이제이션(memoization)도 안전하게 적용할 수 있어요.

불변성: 상태 변경을 추적하기 쉽게 만들기

불변성(immutability)은 데이터를 직접 바꾸지 않고, 변경이 필요하면 새 값을 만들어 반환하는 방식이에요.


여러 곳에서 같은 객체나 배열을 바꾸면, 언제, 어디서 바뀌었는지 추적하기 어려워요. 비동기 코드나 이벤트 핸들러가 같은 데이터를 동시에 건드리면 재현하기 어려운 버그가 생겨요.

// ⚠️ 가변: 원본 배열을 직접 바꿔요 function removeFirst(arr) { arr.splice(0, 1); return arr; } const items = [1, 2, 3]; removeFirst(items); console.log(items); // [2, 3] — 원본이 바뀌었어요
// ✅ 불변: 새 배열을 만들어 반환해요 function removeFirst(arr) { return arr.slice(1); } const items = [1, 2, 3]; const newItems = removeFirst(items); console.log(items); // [1, 2, 3] — 원본은 그대로예요 console.log(newItems); // [2, 3]

함수형 프로그래밍에서는 원본이 바뀌지 않으니 "이전 상태"가 그대로 남아 있어요. 디버깅할 때 "이 시점의 데이터가 무엇이었는지" 확인할 수 있고, 변경 전/후를 비교하기 쉬워요.

React에서 불변성이 중요한 이유도 여기에 있어요. React는 상태가 바뀌었는지를 참조 비교(prevState !== newState)로 판단해요. 불변 스타일로 새 객체를 만들면 참조가 달라지니까 React가 "바뀌었다"고 인식하고 다시 그려요. 반대로, 기존 객체를 직접 수정하면 참조가 그대로라서 React가 변경을 놓치고 화면이 갱신되지 않아요.


C++에서도 불변성을 const로 표현할 수 있어요. 하지만 JS와 C++에서, "새 배열을 만든다"는 동작의 비용이 크게 달라요.

JS에서 변수는 배열 자체가 아니라 배열이 있는 주소(참조)를 들고 있어요. slice(1)을 호출하면 새 배열이 힙에 만들어지고, newItems는 그 새 배열의 주소를 가리켜요. 원본 items가 가리키던 [1, 2, 3]은 그대로 남아 있어요.

function removeFirst(arr) { return arr.slice(1); } const items = [1, 2, 3]; const newItems = removeFirst(items); // items → 0xA (주소) → [1, 2, 3] ← 원본 그대로 // newItems → 0xB (주소) → [2, 3] ← 새로 만들어진 배열

이후 items를 더 이상 쓰지 않으면, [1, 2, 3]을 가리키는 변수가 없어져요. 가비지 컬렉터(GC)가 "아무도 안 쓰는 메모리"를 찾아서 자동으로 해제해요. 개발자는 새 배열을 만들기만 하면 되고, 옛 배열을 언제 지울지 생각할 필요가 없어요.


C++의 std::vector는 변수 자체가 이에요. 새 벡터를 만들어 반환하면, 원소를 하나하나 새 메모리 공간에 복사해요.

std::vector<int> removeFirst(const std::vector<int>& arr) { return std::vector<int>(arr.begin() + 1, arr.end()); } std::vector<int> items = {1, 2, 3}; std::vector<int> newItems = removeFirst(items); // items: [메모리 블록 A] → 1, 2, 3 (4바이트 × 3 = 12바이트) // newItems: [메모리 블록 B] → 2, 3 (4바이트 × 2 = 8바이트를 새로 할당하고 복사)

GC가 없으니, 메모리를 언제 해제할지도 개발자가 관리해야 해요. 원소가 3개면 8바이트 복사로 끝나지만, 10만 개짜리 벡터라면 매번 약 400KB를 새로 할당하고 원소를 하나씩 옮겨야 해요. 불변 스타일로 "매번 새 벡터를 만든다"를 반복하면, 그만큼 할당과 복사가 쌓여서 성능에 직접 영향을 줘요.

C++에서는 이 복사 비용을 줄이기 위해 이동 시맨틱(move semantics)을 사용해요. std::vector는 내부적으로 "데이터가 있는 메모리 주소(포인터), 크기, 용량" 세 값만 들고 있어요. 복사는 원소를 하나하나 새 메모리에 옮겨 담는 거예요. 이동은 원소를 복사하지 않고, 이 세 값의 소유권만 넘기는 거예요.

std::vector<int> items = {1, 2, 3}; std::vector<int> newItems = std::move(items); // 이동 전: // items: [포인터: 0x1000, 크기: 3] → [1, 2, 3] // 이동 후: // items: [포인터: null, 크기: 0] ← 비어 있음 // newItems: [포인터: 0x1000, 크기: 3] → [1, 2, 3] ← 같은 메모리

원소가 10만 개여도 포인터, 크기, 용량 세 값(각각 대략 8바이트씩, 총 24바이트)만 옮기면 끝이에요. 400KB를 복사하는 대신 24바이트만 옮기는 셈이에요. 대신 이동한 뒤에는 원본(items)이 빈 상태가 되니, 원본을 더 이상 안 쓸 때만 사용할 수 있어요.

C++에서는 const 참조로 원본을 보호하면서도, 이동 시맨틱으로 복사 비용을 줄이는 방법을 함께 사용해요. 불변성의 원칙은 유지하되 복사 비용은 현실적으로 관리하는 것이 C++에서의 함수형 스타일이에요.

고차 함수와 합성: 변환 파이프라인 만들기

고차 함수(higher-order function)는 함수를 인자로 받거나 함수를 반환하는 함수예요. 합성(composition)은 작은 함수를 조합해서 복잡한 변환을 만드는 것이에요.


데이터를 변환하는 코드를 for 루프와 중간 변수로 작성하면, "필터 → 변환 → 집계" 같은 흐름이 잘 드러나지 않아요. 루프 안에 조건문과 변수 갱신이 섞이면 로직이 복잡해질수록 읽기 어려워져요.

같은 문제를 절차적 스타일과 함수형 스타일로 비교해 볼게요. 60점을 초과하는 점수의 평균을 구하는 코드를 살펴봐요.

// ⚠️ 절차적: 루프와 중간 변수 const scores = [45, 72, 88, 55, 93, 60, 78]; let sum = 0; let count = 0; for (let i = 0; i < scores.length; i++) { if (scores[i] > 60) { sum += scores[i]; count++; } } const average = count > 0 ? sum / count : 0;
// ✅ 함수형: 변환 단계가 드러나요 const scores = [45, 72, 88, 55, 93, 60, 78]; const passed = scores.filter((s) => s > 60); const average = passed.length > 0 ? passed.reduce((sum, s) => sum + s, 0) / passed.length : 0;

filter → reduce 체인을 읽으면 "60점 초과인 것만 골라서 → 합산하고 나눈다"는 흐름이 한눈에 보여요. 각 단계가 독립적인 순수 함수이기 때문에 단계별로 테스트하기도 쉬워요.


합성은 작은 함수를 연결해서 더 큰 변환을 만드는 기법이에요.

const double = (x) => x * 2; const addOne = (x) => x + 1; const doubleThenAddOne = (x) => addOne(double(x)); console.log(doubleThenAddOne(3)); // 7

각 함수는 단순하게 유지하면서도, 합성을 통해 복잡한 변환을 만들 수 있어요.

같은 원칙, 다른 언어: JS와 C++

함수형 프로그래밍은 특정 언어에서만 적용되는 개념이 아니에요. JS와 C++은 설계 철학이 다르지만, 두 언어 모두 함수형 원칙을 적용할 수 있어요. 다만 언어의 특성 때문에 함수형 스타일이 자연스러운 정도와 주의할 점이 달라져요.

JS에서 함수형이 자연스러운 이유

JS에서는 함수형 스타일이 언어 자체에 녹아 있어요.

  1. 함수가 1급 객체예요.

    1급 객체(first-class citizen)란 변수에 담을 수 있고, 다른 함수에 인자로 넘길 수 있고, 함수에서 반환할 수도 있는 값이에요.

    JS에서는 함수가 1급 객체이기 때문에 고차 함수와 합성을 바로 쓸 수 있어요.

    // 함수를 인자로 넘기기 const apply = (fn, value) => fn(value); console.log(apply((x) => x * 2, 5)); // 10 // 함수를 반환하기 (클로저) function multiplier(factor) { return (x) => x * factor; // factor를 기억하는 함수를 반환해요 } const triple = multiplier(3); console.log(triple(4)); // 12

    위의 multiplier가 반환하는 함수는 factor 변수를 기억해요. 이렇게 함수가 자신이 정의된 스코프의 변수를 기억하는 것을 클로저(closure)라고 해요. 클로저 덕분에 "설정을 미리 고정한 함수"를 쉽게 만들 수 있어요.


    모든 언어에서 함수가 1급인 건 아니에요. C에서는 함수 자체를 값으로 다룰 수 없어서, 함수가 있는 메모리 주소(함수 포인터)를 넘기는 우회 방법을 써요.

    // C: 함수 자체가 아니라 함수의 주소(포인터)를 넘겨요 int apply(int (*fn)(int), int x) { return fn(x); }

    Java도 8 버전 전까지는 함수를 값으로 다룰 수 없어서, 함수 하나를 넘기려면 익명 클래스로 객체를 감싸야 했어요.

    // Java 7 이전: 함수를 넘기려면 객체로 감싸야 해요 Collections.sort(list, new Comparator<String>() { public int compare(String a, String b) { return a.length() - b.length(); } });
  2. Array 메서드가 함수형 변환을 기본으로 지원해요.

    map, filter, reduce가 Array 프로토타입에 내장돼 있어서, 별도 라이브러리 없이 바로 데이터를 변환할 수 있어요.

    const users = [ { name: "Kim", age: 25 }, { name: "Lee", age: 17 }, { name: "Park", age: 32 }, ]; const adultNames = users .filter((user) => user.age >= 18) .map((user) => user.name); // ["Kim", "Park"]

    "성인 사용자의 이름 목록"이라는 의도가 코드만으로 바로 읽혀요.


    다른 언어에서도 같은 작업을 할 수 있지만, 얼마나 자연스럽게 쓸 수 있느냐가 달라요. Java에서는 배열에서 바로 체이닝할 수 없고, stream()으로 변환한 뒤 다시 collect()로 모아야 해요.

    List<String> adultNames = users.stream() .filter(u -> u.age >= 18) .map(u -> u.name) .collect(Collectors.toList());

    C++에서는 C++20 전까지 체이닝 자체가 안 돼서, 단계마다 중간 변수를 만들어야 했어요.

    std::vector<User> adults; std::copy_if(users.begin(), users.end(), std::back_inserter(adults), [](const User& u) { return u.age >= 18; }); std::vector<std::string> adultNames; std::transform(adults.begin(), adults.end(), std::back_inserter(adultNames), [](const User& u) { return u.name; });

    JS에서는 .filter().map().reduce()가 Array에 바로 붙어 있어서, 변환 없이 체이닝으로 이어 쓸 수 있어요.

  3. 비동기 처리도 값 변환 체인으로 다룰 수 있어요.

    Promise 체인은 "이전 단계의 결과를 다음 단계의 입력으로 넘긴다"는 점에서 함수 합성과 비슷한 구조예요.

    fetch("/api/users") .then((res) => res.json()) .then((users) => users.filter((u) => u.active)) .then((activeUsers) => renderList(activeUsers));

    .then은 이전 값을 받아 새 값을 반환하는 변환 단계처럼 읽혀요. 네트워크 요청 자체는 부수 효과이기 때문에, "부수 효과를 체인의 시작에 두고, 나머지는 순수한 변환으로 처리한다"는 구조가 자연스럽게 나와요.


    배열 변환에서 함수형 스타일을 쓸 때 주의할 점이 있어요. Promise 체인에서는 fetch가 부수 효과이고 나머지 .then은 순수한 변환이라는 구분이 비교적 자연스럽게 잡히지만, 일반적인 배열 변환에서는 이 구분이 강제되지 않아서 변환 콜백 안에 부수 효과가 슬며시 섞이기 쉬워요.

    // ❌ 변환 안에 부수 효과가 섞여 있어요 function processOrders(orders) { return orders .filter((order) => { console.log("checking:", order.id); // 부수 효과 return order.total > 100; }) .map((order) => ({ ...order, discount: order.total * 0.1 })); }
    // ✅ 부수 효과를 바깥으로 분리해요 function getHighValueOrders(orders) { return orders.filter((order) => order.total > 100); } function applyDiscount(orders) { return orders.map((order) => ({ ...order, discount: order.total * 0.1 })); } // 호출하는 쪽에서 로깅을 명시적으로 처리해요 const highValue = getHighValueOrders(orders); console.log(`${highValue.length}건 필터링됨`); const discounted = applyDiscount(highValue);

    변환 로직과 부수 효과를 분리하면, 변환 로직은 테스트하기 쉽고 재사용할 수 있어요.

  4. 컴포넌트 기반 UI에서 함수형이 구조적으로 드러나요.

    상태를 다루는 UI 라이브러리에서 함수형 원칙이 설계에 그대로 반영돼요. 리듀서(reducer) 패턴은 "상태를 바꾸는 방법"을 순수 함수 하나로 제한해요. (이전 상태, 액션) → 다음 상태만 계산하고, 그 바깥에서만 부수 효과가 일어나게 하는 구조예요. 파생 상태(derived state)는 저장하지 않고, 기존 상태를 순수 함수로 변환해서 쓰는 패턴이에요. 부수 효과 격리는 네트워크 요청이나 구독처럼 "바깥 세계와 통신하는 코드"를 한 경계(예: useEffect) 안에 모아 두고, 나머지 렌더 로직은 순수하게 두는 방식이에요. 이렇게 "순수한 변환"과 "부수 효과"를 계층으로 나누는 구조는, 뒤에서 말할 Functional core, Imperative shell과 같은 패턴이에요.

C++에서 함수형이 필요해진 이유

C++은 절차적, 객체지향 언어로 출발했지만, 코드베이스가 커지면서 상태 관리와 리소스 관리가 복잡해지는 지점이 생겨요. 함수형 스타일은 세 가지 상황에서 이점이 커요.

  1. 데이터 변환 파이프라인에서 중간 상태를 줄일 수 있어요.

    위에서 JS로 본 "60점 초과 평균" 예제를 C++로 비교해 볼게요.

    // ⚠️ 절차적: 중간 변수와 루프 std::vector<int> scores = {45, 72, 88, 55, 93, 60, 78}; std::vector<int> passing; for (int s : scores) { if (s > 60) passing.push_back(s); } int sum = 0; for (int s : passing) sum += s; double avg = !passing.empty() ? static_cast<double>(sum) / passing.size() : 0.0;
    // ✅ 함수형 스타일: STL 알고리즘 + 람다 std::vector<int> passing; std::copy_if(scores.begin(), scores.end(), std::back_inserter(passing), [](int s) { return s > 60; }); int sum = std::accumulate(passing.begin(), passing.end(), 0); double avg = !passing.empty() ? static_cast<double>(sum) / passing.size() : 0.0;

    C++20의 std::ranges를 쓰면 JS의 체인과 비슷한 느낌으로 파이프라인을 만들 수 있어요.

    // ✅ C++20 ranges: 파이프라인 스타일 #include <ranges> auto passing = scores | std::views::filter([](int s) { return s > 60; });
  2. 멀티스레드 환경에서 불변 데이터로 경쟁 조건을 줄일 수 있어요.

    여러 스레드가 같은 데이터를 읽기만 하면 락(lock)이 필요 없어요. const로 불변성을 보장하면 "이 데이터는 누구도 바꾸지 않는다"는 것이 컴파일 시점에 확인되기 때문에, 안전하게 공유할 수 있어요.

    const std::vector<int> sharedData = {1, 2, 3, 4, 5}; // 여러 스레드가 동시에 읽어도 안전해요 — 락 불필요 auto sum = std::accumulate(sharedData.begin(), sharedData.end(), 0);
  3. 순수 함수는 객체를 만들고 초기화하는 과정 없이 입력과 출력만으로 테스트할 수 있어요.

    C++에서 함수형 스타일을 쓸 때 JS와 가장 다른 점은 값의 복사 비용과 수명(lifetime)을 항상 의식해야 한다는 것이에요. JS에서는 가비지 컬렉터가 메모리를 관리해 주지만, C++에서는 복사, 이동, 참조의 비용을 개발자가 직접 판단해야 해요.

    "불변성의 이점은 취하되, 복사 비용은 참조와 이동 시맨틱으로 관리한다"가 C++에서의 현실적인 균형이에요.

    C++의 람다와 캡처: 클로저의 다른 모습

    C++11부터 람다를 쓸 수 있어요. JS의 클로저처럼 외부 변수를 캡처할 수 있지만, 값으로 캡처할지(=) 참조로 캡처할지(&)를 명시해야 해요.

    int factor = 3; // 값 캡처: factor의 사본을 가져요 auto tripleByValue = [factor](int x) { return x * factor; }; // 참조 캡처: factor의 원본을 가리켜요 auto tripleByRef = [&factor](int x) { return x * factor; };

    참조 캡처는 원본이 이미 사라진 뒤에 람다를 호출하면 정의되지 않은 동작(undefined behavior)이 발생해요. JS에서는 클로저가 참조하는 변수가 가비지 컬렉터에 의해 유지되지만, C++에서는 수명을 개발자가 보장해야 해요. 이 차이가 C++에서 함수형을 쓸 때 가장 주의해야 할 부분이에요.

함수형 프로그래밍을 사용하기 좋은 경우

"함수형이냐 절차적이냐"는 언어의 속성이 아니라 문제의 형태가 결정해요. JS도 UI처럼 상태가 많은 영역이 있어서 모든 코드를 순수 함수로만 쓸 수 없고, C++도 함수형 도구가 충분해서 절차적으로만 고집할 이유가 없어요. 상황에 따라 패러다임을 섞어 쓰되, 이런 상황에서는 함수형 스타일의 사용을 고려해보세요.

함수형 프로그래밍에 대한 오해

함수형 프로그래밍을 말할 때, 자주 나오는 오해가 있어요.

  1. 함수형 프로그래밍은 상태를 없애는 것이다.

    함수형은 상태를 없애는 게 아니라, 상태 변화를 예측 가능하게 제어하고 격리하는 패러다임이에요. Redux는 앱 전체의 상태를 하나의 객체로 관리하지만, 상태를 바꾸는 방법을 순수 함수(reducer)로 제한해요. React도 useState로 상태를 쓰지만, 상태를 직접 수정하지 않고 새 값을 만들어 넘기는 불변 패턴을 따라요. 상태가 있되, "언제, 어떻게 바뀌는지"가 명확하게 드러나도록 설계하는 것이 함수형의 접근이에요.

  2. 함수형 프로그래밍이란, map/filter/reduce만 쓰는 것이다.

    map/filter/reduce는 함수형의 도구 중 일부예요. 함수형 프로그래밍의 핵심은 부수 효과를 줄이고, 순수 함수로 예측 가능한 코드를 만드는 것이에요. map을 쓰더라도 콜백 안에서 외부 변수를 바꾸면 함수형의 이점이 사라져요.

    // ❌ map을 쓰지만 부수 효과가 있어요 let total = 0; const prices = items.map((item) => { total += item.price; // 외부 변수를 바꿔요 return item.price * 1.1; });
  3. 불변성을 지키기 위해서는 무조건 복사해야 한다.

    JS에서는 스프레드 연산자(...)로 얕은 복사를 하는 것이 일반적이에요. 하지만 C++에서 큰 데이터를 매번 복사하면 성능에 직접적인 영향이 있어요. 불변성의 목적은 "원본을 보호하는 것"이지 "무조건 복사하는 것"이 아니에요. const 참조, 이동 시맨틱, 불변 뷰 같은 도구로 복사 없이도 불변성의 이점을 가져갈 수 있어요.

  4. 무조건 모든 함수를 추상화해서 합성해야 한다.

    작은 함수를 합성하는 것 자체는 좋지만, 과도하게 추상화하면 오히려 읽기 어려워질 수 있어요.

    커링(currying)은 인자를 여러 개 받는 함수를 "인자를 하나씩 받는 함수의 연쇄"로 바꾸는 거예요.

    // 일반 함수: 인자 두 개를 한꺼번에 받아요 const add = (a, b) => a + b; add(1, 2); // 3 // 커링된 함수: 인자를 하나씩 받아요 const add = (a) => (b) => a + b; add(1)(2); // 3

    인자를 미리 고정한 함수를 만들 수 있어서 합성에 유용하지만, 단계가 깊어지면 읽기 어려워져요.

    포인트-프리(point-free)는 함수를 정의할 때 인자를 명시하지 않고 함수 이름만 조합하는 스타일이에요.

    // 인자를 직접 써요 const getAdults = (users) => users.filter((user) => user.age >= 18); // 포인트-프리: 인자 없이 함수 이름만 조합해요 const isAdult = (user) => user.age >= 18; const getAdults = filter(isAdult);

    함수를 파이프라인처럼 연결하는 느낌이 강해지지만, 과하면 "이 함수에 뭐가 들어오는 거지?"가 안 보여요.

    합성의 목적은 "작은 단위로 나눠서 이해하기 쉽게 만드는 것"이에요. 합성한 결과가 오히려 원래 코드보다 읽기 어렵다면 합성을 줄이는 것이 좋아요.

참고 자료