Function Composition
함수 합성은 여러 개의 작은 함수를 이어 붙여 하나의 함수로 만드는 기법입니다. 첫 함수의 출력이 다음 함수의 입력이 되고, 다음 함수의 출력이 그 다음으로 흘러가면서 데이터가 파이프라인처럼 이동합니다. 각 단계를 작은 함수로 쪼개 두면 재사용과 테스트가 쉬워지고, 합성된 결과 함수는 '이 단계를 이 순서로 적용한다'는 의도를 코드에 그대로 드러냅니다.
▶아키텍처 다이어그램
📊 데이터 흐름 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
사용자 입력 문자열을 처리하려고 할 때, 공백 제거 → 소문자 변환 → 유효성 검사 → 포맷팅 같은 여러 단계를 거쳐야 하는 상황이 흔합니다. 보통은 중간 변수에 결과를 담고 다음 함수에 넘기는 식으로 작성하게 됩니다. 단계가 늘면 `result1`, `result2`, `result3` 같은 이름 없는 변수가 쌓이고, 어느 단계가 실제로 '흐름'인지 '임시 저장'인지 눈으로 구분하기 어려워집니다. 단계 중 하나를 끼워 넣거나 빼려면 중간 변수 이름과 순서를 다시 손대야 합니다. 반대로 모든 처리를 한 함수에 몰아넣으면 재사용과 테스트가 어려워집니다. 단계를 의미 있는 단위로 쪼개면서도 그 순서를 깔끔하게 엮는 방법이 필요합니다.
함수 합성은 수학의 합성 함수 개념에서 왔습니다. `f(g(x))`처럼 한 함수의 결과를 다음 함수에 넘기는 것이 수학 표기였고, Haskell 같은 언어는 이 개념을 그대로 문법에 담았습니다. 주류 언어에서도 Ramda, Lodash/fp, RxJS 같은 라이브러리가 pipe, compose 함수를 제공하면서 자바스크립트 생태계에서도 일상적인 패턴이 됐습니다. Unix 파이프(`cat file | grep foo | sort | uniq`)와 같은 발상이기도 합니다. 각 단계가 한 가지 일만 하고, 그 결과가 다음 단계로 흘러가는 구조가 함수형 데이터 처리의 기본 문법이 된 것은 이 흐름을 읽는 방식이 직관적이기 때문입니다.
함수 합성의 기본은 출력과 입력을 맞추는 것입니다. 한 함수의 반환 타입이 다음 함수의 인자 타입과 일치해야 이어 붙일 수 있습니다. pipe 함수는 왼쪽에서 오른쪽으로 데이터가 흐르도록 함수를 연결하고, compose 함수는 오른쪽에서 왼쪽으로 연결합니다. `pipe(f, g, h)(x)`는 `h(g(f(x)))`와 같고, `compose(f, g, h)(x)`는 `f(g(h(x)))`와 같습니다. 어느 방향이든 핵심은 '각 단계가 입력 하나를 받아 출력 하나를 낸다'는 단항 함수 스타일입니다. 여러 인자를 받는 함수는 커링이나 부분 적용으로 단항으로 만든 뒤 합성에 넣는 것이 일반적입니다.
pipe로 만드는 데이터 파이프라인
// 작은 함수들
const trim = (s) => s.trim();
const toLower = (s) => s.toLowerCase();
const removeSpaces = (s) => s.replace(/\s/g, "");
// pipe로 합성
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const normalize = pipe(trim, toLower, removeSpaces);
normalize(" Hello World "); // "helloworld"
// 중간 변수 없이 의도가 드러남
// "공백 제거하고, 소문자로, 공백 전부 제거"라는 순서가 코드에 그대로 보임pipe는 함수 배열을 받아 첫 번째부터 차례로 적용합니다. normalize는 세 단계를 이어 붙인 하나의 함수이고, 호출 시점에 데이터가 왼쪽에서 오른쪽으로 흘러갑니다. 중간 변수 없이 처리 순서가 코드에 선언적으로 드러납니다.
함수 합성과 메서드 체이닝은 비슷해 보이지만 구조가 다릅니다. 메서드 체이닝은 `"hello".trim().toLowerCase()`처럼 객체에 붙어 있는 메서드를 점으로 연결하고, 각 메서드는 그 객체의 타입에 속박됩니다. 문자열 메서드를 숫자에 이어 붙일 수 없습니다. 함수 합성은 독립된 함수끼리 입출력 타입만 맞으면 자유롭게 조합할 수 있고, 함수를 재사용하거나 새 파이프라인에 끼워 넣기 쉽습니다. 객체 지향 코드에서는 체이닝이 자연스럽고, 함수형 데이터 변환에서는 합성이 더 유연합니다. 합성과 고차 함수도 구분하면 좋습니다. 고차 함수는 '함수를 다루는 함수'라는 넓은 범주이고, 합성은 그중에서도 '여러 함수를 순서대로 이어 하나로 만드는' 특정 패턴에 해당합니다.
Gain 처리 단계의 순서가 코드 표면에 그대로 드러나고, 각 단계를 작은 함수로 분리해 재사용하고 테스트하기 쉬워집니다. Cost 합성하려면 각 함수를 단항 스타일로 맞추고 입력·출력 타입을 꾸준히 정리해야 합니다. 에러 처리나 분기 로직이 많아지면 보조 래퍼가 늘어나 오히려 흐름이 복잡해질 수 있습니다. Decision Scale 변환 단계가 3개 이상이고 각 단계를 따로 재사용하거나 검증할 가치가 있을 때는 합성이 강합니다. 반대로 1~2단계짜리 단순 처리나 분기가 많은 흐름이라면 직접 작성한 코드가 더 읽기 쉽습니다.
함수 합성은 Redux의 compose, RxJS의 pipe, Ramda 기반 데이터 변환, 서버 미들웨어 체인처럼 여러 단계를 한 흐름으로 묶는 자리에서 핵심적으로 쓰입니다. React에서는 selector나 데이터 정제 함수를 단계별로 나눠 재사용할 때 유용합니다. 실무에서는 입력 정규화, 검증, 저장 직전 포맷 변환처럼 각 단계가 입력 하나와 출력 하나로 이어질 때 특히 잘 맞습니다.