순수 함수, 부수 효과, 참조 투명성
함수형 프로그래밍의 출발점은 순수 함수입니다. 부수 효과와 참조 투명성은 순수 함수를 설명하는 두 가지 측면입니다. 셋을 함께 이해하면 FP가 왜 이 방향을 선택했는지 보입니다.
순수 함수란
두 가지 조건을 만족하는 함수입니다.
- 같은 입력에 항상 같은 출력
- 부수 효과 없음
// 순수 함수
function add(a: number, b: number): number {
return a + b;
}
add(2, 3); // 항상 5
add(2, 3); // 항상 5반면 이런 함수는 순수하지 않습니다.
let count = 0;
// 순수하지 않음 - 호출할 때마다 결과가 다름
function increment(): number {
return ++count;
}
increment(); // 1
increment(); // 2 (같은 입력인데 다른 출력)부수 효과
함수가 반환값 외에 외부 세계에 영향을 미치는 모든 것이 부수 효과입니다.
// 부수 효과의 예시들
// 1. 외부 변수 변경
let total = 0;
function addToTotal(n: number) {
total += n; // 부수 효과: 외부 상태 변경
}
// 2. 콘솔 출력
function greet(name: string) {
console.log(`Hello, ${name}`); // 부수 효과: I/O
}
// 3. API 호출
async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`); // 부수 효과: 네트워크
return res.json();
}
// 4. 인자로 받은 객체 변경
function addItem(cart: string[], item: string) {
cart.push(item); // 부수 효과: 입력 변이(mutation)
}부수 효과 자체가 나쁜 것은 아닙니다. 프로그램은 결국 무언가를 출력하고, 데이터를 저장하고, 화면에 표시해야 합니다. 문제는 부수 효과가 예측하기 어렵게 흩어져 있을 때입니다.
FP는 부수 효과를 없애는 게 아니라 명시적으로 격리하는 방향을 선택합니다.
// 순수하지 않은 버전 - 부수 효과가 숨어있음
function processOrder(orderId: string) {
const order = db.find(orderId); // 숨겨진 I/O
order.status = 'processed'; // 숨겨진 변이
db.save(order); // 숨겨진 I/O
sendEmail(order.userId, '처리완료'); // 숨겨진 I/O
return order;
}
// 순수 버전 - 로직과 효과 분리
function calculateOrderStatus(order: Order): Order {
return { ...order, status: 'processed' }; // 순수: 새 객체 반환
}
// 효과는 바깥에서 명시적으로 처리
async function handleOrder(orderId: string) {
const order = await db.find(orderId);
const processed = calculateOrderStatus(order); // 순수 로직 호출
await db.save(processed);
await sendEmail(processed.userId, '처리완료');
}참조 투명성
표현식을 그 결과값으로 교체해도 프로그램의 동작이 바뀌지 않는 성질입니다.
const x = add(2, 3); // add(2, 3)을 5로 교체해도 동작이 같음
const y = x * 2; // 5 * 2 = 10
// 위와 동일
const y2 = add(2, 3) * 2; // 10참조 투명성이 없으면 코드를 부분적으로 이해하기 어렵습니다.
let i = 0;
function next(): number {
return i++;
}
const a = next() + next();
// a는 0 + 1 = 1
// next()를 그 결과로 교체하려고 해도 불가능
// 첫 번째 next()는 0, 두 번째는 1이기 때문반면 순수 함수는 어디서든 교체 가능합니다.
const double = (x: number) => x * 2;
// 이 두 코드는 완전히 동일
const result1 = double(5) + double(3);
const result2 = 10 + 6;왜 중요한가
테스트가 쉬워집니다
순수 함수는 입력과 출력만 검증하면 됩니다. 외부 상태를 설정하거나 모킹할 필요가 없습니다.
// 테스트하기 어려운 버전
function getDiscountedPrice(productId: string): number {
const product = db.getProduct(productId); // DB 필요
const discount = discountService.getRate(); // 외부 서비스 필요
return product.price * (1 - discount);
}
// 테스트하기 쉬운 버전
function applyDiscount(price: number, discountRate: number): number {
return price * (1 - discountRate);
}
// 테스트
expect(applyDiscount(10000, 0.1)).toBe(9000);
expect(applyDiscount(10000, 0.2)).toBe(8000);추론이 쉬워집니다
함수를 보면 무슨 일을 하는지 바로 알 수 있습니다. 외부 상태를 확인하러 다닐 필요가 없습니다.
병렬 실행이 안전합니다
순수 함수는 공유 상태를 변경하지 않으므로 race condition이 없습니다.
정리
- 순수 함수: 같은 입력 → 같은 출력, 부수 효과 없음
- 부수 효과: 반환값 외의 외부 영향 (I/O, 상태 변경 등) — 없애는 게 아니라 격리하는 것이 목표
- 참조 투명성: 표현식을 결과값으로 교체 가능 — 코드를 국소적으로 이해할 수 있게 해줌
세 개념은 하나의 원칙을 다른 각도에서 바라본 것입니다: 함수가 하는 일을 함수 시그니처만으로 알 수 있어야 한다.