1. 도입
순수 함수 — 동일한 입력에 대해 항상 같은 결과를 반환하고 어떤 부수 효과도 일으키지 않는 함수입니다. 쉽게 말하면: 순수 함수는 주변을 "망가뜨리지"도 않고 외부로부터 "감염"되지도 않습니다.
함수가 순수하다고 판단되는 조건:
- 입력 인수만 기반으로 값을 계산한다.
- 외부 변수나 프로그램 상태를 변경하지 않는다.
- 변할 수 있는 외부 변수나 상태에 의존하지 않는다.
순수성의 두 가지 황금 규칙
- 결정론성: 같은 입력은 같은 출력.
- 부수 효과 없음: 함수는 파일도, 전역 변수도, 사용자 인터페이스도 변경하지 않는다 (안녕, Console.WriteLine).
종교가 아니라 합리적 접근
이게 학문적 집착처럼 보일 수 있지만, 실제로는 그렇지 않습니다:
- 순수 함수는 예측 가능하다. 테스트하기 쉽다 — 특정 인수로 호출하면 특정 결과가 나온다.
- 순수 함수는 코드 어디에나 안전하게 "섞어" 넣을 수 있다. 어딘가가 깨질 걱정이 없다.
- 멀티코어 환경에서 순수 함수는 안전성 보장이다: 데이터 경합 걱정 없이 병렬로 실행할 수 있다.
2. 순수 함수와 비순수 함수 예제
순수 함수: 모든 것이 예측 가능
// 순수 함수: 외부를 건드리지 않음
int Add(int a, int b)
{
return a + b;
}
int Square(int x)
{
return x * x;
}
Add(2, 3)를 백 번 호출해도 결과는 항상 5다. 지루하지만 안전하다.
비순수 함수: 규칙을 어김
// 순수성을 위반: 외부 상태(정적 변수)에 의존
int counter = 0;
int Increase()
{
counter++;
return counter;
}
여기서 Increase()를 호출하면 매번 다른 값을 반환하므로 더 이상 결정론적이지 않다.
// 순수성을 위반: 외부 효과(화면 출력)를 발생
int AddAndPrint(int a, int b)
{
int sum = a + b;
Console.WriteLine(sum); // 부수 효과!
return sum;
}
무작위성과 시간은 어떻게?
DateTime.Now나 Random을 사용하는 모든 함수는 이미 순수하지 않다:
// 순수하지 않음!
int GetRandomNumber()
{
return new Random().Next();
}
차이점 표
| 특성 | 순수 함수 | 비순수 함수 |
|---|---|---|
| 동일 인수에 대해 항상 같은 결과 | 예 | 아니오 |
| 부수 효과 | 없음 | 있음 |
| 외부 상태에 대한 의존성 | 없음 | 있음 |
3. 데이터 불변성: 이론과 실전
불변성(immutability) — 객체는 생성 이후 변경할 수 없는 접근법이다. 새로운 값이 필요하면 새 객체를 만든다.
왜 중요한가?
- 애플리케이션은 데이터 변경으로 인한 우발적 오류에 강해진다.
- 누군가가 특정 객체의 필드를 몰래 바꾸는 일(숨은 변경)이 사라진다.
- 불변성은 여러 자동 최적화와 병렬 계산의 기반이 된다.
C#에서의 간단한 예
.NET의 불변 타입
C#의 문자열(string)은 불변이다! string.Concat(s, "world")를 할 때마다 새 문자열이 생성된다.
string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"
배열과 컬렉션: 기본적으로 mutable
int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // 배열이 변경됨!
불변성을 "체감"하기: 코드로 컴파일
기존 객체/값을 변경하는 대신 새 객체를 반환:
// 이렇게 하지 말자:
void AddToList(List<int> list, int value)
{
list.Add(value); // 뮤테이트!
}
// 이렇게 하는 게 낫다:
List<int> AddToList(List<int> list, int value)
{
var newList = new List<int>(list) { value }; // 새 리스트
return newList;
}
일러스트: 변경 vs 불변
flowchart LR
A[원본 객체] --"뮤테이션"--> B[같은 객체, 내부가 바뀜]
A --"불변성"--> C[새로운 객체]
4. 실제 C# 코드에서 왜 필요한가?
- 현대 C# 라이브러리들(LINQ, Entity Framework, ASP.NET Core 등)은 순수 함수와 불변성에 기반한 설계를 선호한다.
- 불변성은 누군가가 중요한 값을 엉뚱하게 덮어쓰는 "마법 같은" 버그를 줄여준다.
- 순수 함수는 단위 테스트(unit tests)를 쉽게 만든다: 테스트에서는 입력과 출력만 검증하면 된다, 외부 세계 신경 쓸 필요 없음.
예: 문자열 작업
string s = "Hello";
string newS = s.Replace("H", "J"); // s는 여전히 "Hello"; newS는 "Jello"
예: LINQ와 컬렉션
Where, Select 같은 메서드는 기존 컬렉션을 바꾸지 않고 새 컬렉션을 반환한다.
var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// numbers는 그대로다!
실무 예: 불변 객체로 구성 설정
많은 현대 .NET API는 구성(configuration)을 위해 불변 객체를 사용한다. 예: JsonSerializerOptions:
var options = new JsonSerializerOptions
{
WriteIndented = true
};
// 이 객체는 "플레이 중"에 변경되지 않아 안정성이 높아진다.
5. 전형적인 실수와 함정
문제는 겉보기에는 "순수한" 코드에서 데이터를 "실수로" 뮤테이트할 때 시작된다.
컬렉션을 다루다 보면 종종 필터링하려다 원본을 변경하는 실수를 한다.
또는 List<T>.Add 같은 메서드가 제자리에서 객체를 변경한다는 사실을 잊어버린다.
교활한 예제:
List<int> DoubleTheNumbers(List<int> xs)
{
// 실수! 원본 리스트를 뮤테이트하고 같은 객체를 반환한다.
foreach (var i in xs)
xs.Add(i * 2);
return xs;
}
이 코드는 컬렉션을 순회하는 동안 컬렉션을 변경하기 때문에 실행 오류(InvalidOperationException)가 날 수도 있다 — 뮤테이션의 고전적 문제.
올바른 방식:
List<int> DoubleTheNumbers(List<int> xs)
{
var newList = new List<int>(xs.Select(x => x * 2));
return newList;
}
GO TO FULL VERSION