순수 함수와 불변성
함수형 프로그래밍(Functional Programming, FP)은 명령형(Imperative) 프로그래밍이나 객체지향(Object-Oriented) 프로그래밍과는 다른 방식으로 소프트웨어를 구축하는 패러다임입니다. FP는 계산을 함수 평가(evaluation)로 보고, 상태를 변경하는 명령보다는 함수 적용(application)에 중점을 둡니다.
함수형 프로그래밍의 핵심 개념 중 두 가지가 순수 함수(Pure Functions)와 불변성(Immutability)입니다.
이 두 원칙은 코드의 예측 가능성을 높이고, 테스트를 쉽게 만들며, 병렬/동시성 환경에서도 안정성을 확보하는 데 큰 역할을 합니다.
타입스크립트가 FP를 강제하지는 않지만, 강력한 타입 시스템을 통해 순수 함수와 불변성 원칙을 지키는 코드를 더 안전하게 작성할 수 있습니다.
순수 함수
정의: 순수 함수는 다음 두 가지 조건을 만족하는 함수입니다.
동일한 입력에는 항상 동일한 출력 보장: 함수의 실행 결과는 오직 입력된 인자에만 의존하며, 외부 상태에 영향을 받지 않습니다.
부수 효과(Side Effects) 없음: 함수는 외부 세계의 어떤 것도 변경하지 않습니다. 즉, 함수 외부의 변수를 수정하거나, 콘솔에 출력하거나, 네트워크 요청을 보내거나, 파일에 쓰는 등의 작업을 하지 않습니다.
설명:
순수 함수는 수학에서의 함수 개념과 매우 유사합니다. f(x) = x^2 라는 함수는 x가 2일 때 항상 4를 반환하고, 함수를 호출한다고 해서 x의 값이 바뀌거나 다른 어떤 외부 영향도 미치지 않습니다.
// 배열의 각 숫자를 두 배로 만드는 함수
function doubleNumbers(numbers: number[]): number[] {
// 입력 배열을 변경하지 않고 새로운 배열을 반환
return numbers.map(num => num * 2);
}
const originalNumbers = [1, 2, 3];
const doubledNumbers = doubleNumbers(originalNumbers);
console.log("Original Numbers:", originalNumbers); // [1, 2, 3] - 원본 변경 없음
console.log("Doubled Numbers:", doubledNumbers); // [2, 4, 6]
// 항상 동일한 입력 -> 동일한 출력 보장
console.log(doubleNumbers([5, 6])); // [10, 12]
console.log(doubleNumbers([5, 6])); // [10, 12]doubleNumbers 함수는 numbers 배열을 직접 수정하지 않고, map 메서드를 통해 새로운 배열을 반환합니다. 따라서 외부 상태를 변경하지 않고, 항상 동일한 입력에 대해 동일한 출력을 보장합니다.
let total = 0; // 외부 상태
// 1. 외부 상태를 변경하는 함수 (부수 효과 발생)
function addToTotal(amount: number): number {
total += amount; // 외부 변수 total을 변경
return total;
}
console.log(addToTotal(10)); // 10
console.log(addToTotal(5)); // 15 (total 값이 이전에 변경되었으므로 출력값이 달라짐)
console.log("Final total:", total); // 15
// 2. 입력에 따라 출력이 달라지는 함수 (외부 상태에 의존)
let discountRate = 0.1; // 외부 상태
function calculatePrice(price: number): number {
// 외부 변수 discountRate에 의존
return price * (1 - discountRate);
}
console.log(calculatePrice(100)); // 90
discountRate = 0.2; // 외부 상태 변경
console.log(calculatePrice(100)); // 80 (동일 입력이지만 출력값 달라짐)
// 3. 콘솔 출력 (부수 효과)
function logAndReturn(value: string): string {
console.log(value); // 콘솔 출력은 부수 효과
return value;
}addToTotal은 외부 변수 total을 변경하므로 부수 효과가 있고, total 값에 따라 동일한 입력에서도 다른 출력을 반환할 수 있습니다.
calculatePrice는 discountRate라는 외부 변수에 의존하므로, 이 값이 바뀌면 동일한 입력 price에 대해서도 결과가 달라집니다.
logAndReturn은 콘솔에 값을 출력하는 부수 효과를 가집니다.
- 예측 가능성: 동일한 입력에는 항상 동일한 출력이 보장되므로, 함수 호출의 결과를 쉽게 예측할 수 있습니다.
- 테스트 용이성: 외부 상태나 순서에 의존하지 않으므로, 함수를 격리하여 독립적으로 테스트하기 매우 쉽습니다. 목(Mock) 객체나 복잡한 테스트 환경 설정이 필요 없습니다.
- 디버깅 용이성: 버그가 발생했을 때, 순수 함수는 문제의 원인을 추적하기 쉽습니다. 입력만 확인하면 됩니다.
- 병렬 처리 용이성: 순수 함수는 외부 상태를 변경하지 않으므로, 여러 스레드나 코어가 동시에 실행해도 경쟁 조건(Race Condition)이나 교착 상태(Deadlock) 같은 동시성 문제가 발생하지 않습니다.
- 캐싱(Memoization) 가능: 입력에 대한 출력이 항상 같으므로, 동일한 입력이 주어질 때 이전에 계산된 결과를 재사용(캐싱)할 수 있어 성능 최적화에 유리합니다.
불변성
정의: 불변성이란 데이터가 한 번 생성되면 그 상태를 변경할 수 없음을 의미합니다. 대신, 데이터를 변경해야 할 때는 원본 데이터를 수정하는 대신, 변경된 부분을 반영한 새로운 데이터 사본을 만듭니다.
설명: 객체지향 프로그래밍에서는 객체의 상태를 직접 변경(Mutable)하는 것이 일반적입니다. 하지만 이러한 가변성은 예측하기 어려운 버그를 유발하고, 코드의 복잡성을 증가시킵니다. 특히 여러 함수나 스레드가 동일한 데이터를 공유하고 변경할 때 문제가 커집니다.
불변성은 이러한 문제들을 해결하는 핵심적인 방법입니다. 모든 데이터가 불변하면, 데이터가 어떤 함수에 의해 언제 변경되었는지 걱정할 필요가 없어집니다.
타입스크립트 적용 예시 가변성 (나쁜 예)interface User {
name: string;
age: number;
hobbies: string[];
}
const userProfile: User = {
name: "Alice",
age: 30,
hobbies: ["reading", "hiking"]
};
// 1. 객체 속성 직접 변경
function celebrateBirthday(user: User): void {
user.age += 1; // user 객체의 age 속성을 직접 변경
}
// 2. 배열 직접 변경
function addHobby(user: User, newHobby: string): void {
user.hobbies.push(newHobby); // user.hobbies 배열을 직접 변경
}
console.log("Before:", userProfile); // { name: 'Alice', age: 30, hobbies: [ 'reading', 'hiking' ] }
celebrateBirthday(userProfile);
addHobby(userProfile, "cooking");
console.log("After:", userProfile); // { name: 'Alice', age: 31, hobbies: [ 'reading', 'hiking', 'cooking' ] }
// userProfile 객체 자체가 변경되었음을 알 수 있습니다.
// 만약 이 userProfile 객체가 여러 곳에서 참조되고 있었다면,
// 의도치 않은 side effect를 유발할 수 있습니다.celebrateBirthday와 addHobby 함수는 userProfile 객체의 내부 상태를 직접 변경합니다. 이는 원본 객체를 사용하는 다른 부분에 예기치 않은 영향을 미칠 수 있습니다.
interface User {
readonly name: string; // readonly 키워드로 속성 불변성 강조
readonly age: number;
readonly hobbies: readonly string[]; // readonly 배열 타입
}
const userProfile: User = {
name: "Alice",
age: 30,
hobbies: ["reading", "hiking"]
};
// 1. 객체 속성 변경 시 새로운 객체 반환
function getNextBirthdayUser(user: User): User {
// 스프레드 연산자 (...)를 사용하여 새로운 객체 생성 및 속성 변경
return { ...user, age: user.age + 1 };
}
// 2. 배열 변경 시 새로운 배열 반환
function getUserWithNewHobby(user: User, newHobby: string): User {
// 스프레드 연산자와 slice/concat 등을 사용하여 새로운 배열 생성
return {
...user,
hobbies: [...user.hobbies, newHobby] // 새로운 배열 생성
};
}
console.log("Original:", userProfile);
const userAfterBirthday = getNextBirthdayUser(userProfile);
const userAfterAddingHobby = getUserWithNewHobby(userAfterBirthday, "cooking");
console.log("Original after operations:", userProfile); // 원본은 변경되지 않음
// { name: 'Alice', age: 30, hobbies: [ 'reading', 'hiking' ] }
console.log("After Birthday:", userAfterBirthday); // 새로운 객체 생성
// { name: 'Alice', age: 31, hobbies: [ 'reading', 'hiking' ] }
console.log("After Adding Hobby:", userAfterAddingHobby); // 또 다른 새로운 객체 생성
// { name: 'Alice', age: 31, hobbies: [ 'reading', 'hiking', 'cooking' ] }
// TypeScript의 'readonly' 키워드는 컴파일 타임에 불변성을 강제하여 실수를 방지합니다.
// userProfile.age = 31; // Error: 읽기 전용 속성이므로 'age'에 할당할 수 없습니다.
// userProfile.hobbies.push("painting"); // Error: 'readonly string[]' 형식에 'push' 속성이 없습니다.getNextBirthdayUser와 getUserWithNewHobby 함수는 원본 user 객체를 수정하지 않고, 스프레드 문법(...)을 사용하여 변경된 속성을 가진 새로운 객체를 반환합니다. readonly 키워드를 사용하면 타입스크립트 컴파일러가 불변성 위반을 감지하여 오류를 발생시킵니다.
- 예측 가능성: 데이터가 변경되지 않으므로, 어떤 함수가 데이터를 변경할지 추적할 필요가 없어 코드의 흐름을 이해하기 쉽습니다.
- 병렬 처리 및 동시성: 데이터가 변경되지 않으므로, 여러 스레드나 함수가 동시에 데이터를 읽어도 안전하며, 경쟁 조건이 발생하지 않습니다.
- 버그 감소: 의도치 않은 상태 변경으로 인한 버그를 원천적으로 방지합니다.
- 테스트 용이성: 함수가 불변 데이터를 받아서 불변 데이터를 반환한다면, 테스트 케이스를 작성하기가 훨씬 쉬워집니다.
- 시간 여행 디버깅: 이전 상태의 데이터를 쉽게 유지하고 접근할 수 있으므로, 애플리케이션의 상태 변화를 추적하고 디버깅하는 데 유용합니다 (예: Redux DevTools).
- 성능 최적화: 특정 조건에서 객체 참조의 동일성 검사만으로 변경 여부를 빠르게 판단할 수 있어, 렌더링 최적화(React의 PureComponent/memo)나 캐싱에 활용될 수 있습니다.
순수 함수와 불변성의 관계
순수 함수는 부수 효과가 없어야 하므로, 함수 내에서 외부 상태를 변경하지 않아야 합니다. 이 외부 상태에는 함수 인자로 전달된 객체나 배열도 포함됩니다. 만약 함수 인자로 가변(Mutable) 객체가 들어왔는데, 함수가 그 객체를 직접 수정한다면 이는 부수 효과가 되어 순수 함수가 아닙니다.
따라서 순수 함수를 작성하기 위해서는 불변성 원칙을 철저히 지켜야 합니다. 함수가 인자로 받은 데이터를 변경해야 할 때는 항상 새로운 사본을 만들어서 반환해야 합니다.
요약- 순수 함수: 동일 입력 -> 동일 출력, 부수 효과 없음.
- 불변성: 데이터 생성 후 변경 불가, 변경 필요 시 새 사본 생성.
이 두 가지 원칙은 함수형 프로그래밍의 가장 기본적인 기둥이며, 이들을 적용함으로써 더 예측 가능하고, 테스트하기 쉬우며, 안정적인 코드를 작성할 수 있습니다. 타입스크립트는 readonly 키워드와 엄격한 타입 검사를 통해 이러한 원칙을 강제하고 장려하는 데 훌륭한 도구가 됩니다.