CodeGym /행동 /C# SELF /순수 함수와 불변성

순수 함수와 불변성

C# SELF
레벨 51, 레슨 1
사용 가능

1. 도입

순수 함수 — 동일한 입력에 대해 항상 같은 결과를 반환하고 어떤 부수 효과도 일으키지 않는 함수입니다. 쉽게 말하면: 순수 함수는 주변을 "망가뜨리지"도 않고 외부로부터 "감염"되지도 않습니다.

함수가 순수하다고 판단되는 조건:

  • 입력 인수만 기반으로 값을 계산한다.
  • 외부 변수나 프로그램 상태를 변경하지 않는다.
  • 변할 수 있는 외부 변수나 상태에 의존하지 않는다.

순수성의 두 가지 황금 규칙

  1. 결정론성: 같은 입력은 같은 출력.
  2. 부수 효과 없음: 함수는 파일도, 전역 변수도, 사용자 인터페이스도 변경하지 않는다 (안녕, 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;
}
2
과제
C# SELF, 레벨 51, 레슨 1
잠금
불변 리스트에 숫자 추가하기
불변 리스트에 숫자 추가하기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION