CodeGym /행동 /C# SELF /함수형 프로그래밍 입문

함수형 프로그래밍 입문

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

1. 도입

함수형 프로그래밍(ФП)은 기본 구성 단위가 객체나 절차/메소드가 아니라 수학적 의미의 함수인 프로그래밍 패러다임입니다. 함수형 프로그래밍에서는 "무엇을 계산할지"를 설명하는 데 초점을 두고, "어떻게 계산할지"에는 덜 신경 씁니다.

람다 표현식과 LINQ를 다루면서 함수형 아이디어를 이미 접해봤을 거예요. 차이는 뭘까요? 실제로는: OOP는 객체와 그 상호작용을 묘사하고, 절차적 프로그래밍은 단계들의 집합을 다루며, 함수형 프로그래밍은 함수의 합성, 행동을 값으로 전달하기, 상태 변경을 피하는 것(immutability), 부작용의 배제를 중심으로 합니다.

왜 새로운 패러다임이 필요할까?

  • 더 깔끔하고 예측 가능하며 테스트하기 쉬운 코드.
  • 멀티스레딩 지원이 단순해짐 ("상태가 없으면 문제도 없다").
  • 간결하고 표현력 높은 코드 (코드가 적을수록 버그도 적음).
  • 높은 수준의 재사용 가능한 추상화.

비유

레스토랑에 "오믈렛을 만들어 주세요"라는 주문이 들어왔다고 가정해봅시다. 명령형 요리사는 명령 목록을 따릅니다: 달걀을 가져오고, 깨고, 휘젓고, 굽고. 함수형 요리사는 이렇게 말하죠: res = omlet(jajca) — 함수들로 작업하고 주방의 내부 상태에서 추상화합니다(거의요).

C#에서는 두 접근법을 모두 사용할 수 있습니다. 이 덕분에 언어가 실제 프로젝트에서 매우 유연하고 강력해집니다.

함수형의 핵심 개념

1. 고차 함수

함수는 매개변수로 전달되거나 다른 함수에서 반환되거나 변수에 저장될 수 있습니다. 람다와 델리게이트로 이미 이런 걸 해봤죠. 함수 위주의 함수들(함수를 다루는 함수들)이 함수형의 기초입니다.

2. 순수 함수

함수의 결과가 오직 파라미터에만 의존하고 외부를 변경하지 않을 때 그 함수를 "순수"하다고 합니다. 동일한 인자로 두 번 호출하면 항상 같은 결과를 반환합니다.

3. 불변성 (Immutability)

데이터를 "제자리에서" 변경하지 않습니다: 새로운 상태는 새로운 객체입니다. 프로그램을 추론하기 훨씬 쉬워지고 멀티스레딩에서 도움이 됩니다.

4. 부작용 없음

함수는 파일에 쓰지 않고, 전역 변수를 변경하지 않으며, 화면에 그리지 않고 단지 결과만 반환합니다. 현실에서는 부작용이 불가피하지만 시스템의 가장자리에 격리하는 게 일반적입니다.

5. 함수 합성

함수들을 레고 블록처럼 조합해 새로운 함수를 만들 수 있습니다. 예: 양수만 필터링하고 제곱한 뒤 합하기. 각 연산은 별도의 함수이고 쉽게 조합됩니다 (WhereSelectSum).

2. C#에서의 FP: 이론에서 실전으로

C#은 멀티패러다임 언어입니다: OOP, 절차적 접근뿐 아니라 람다, 델리게이트, 확장 메서드와 LINQ로 강력한 함수형 스타일을 잘 지원합니다.

학습용 앱 예제로 풀어보기

숫자와 문자열 리스트를 다루는 프로그램을 개발한다고 해봅시다. 우리의 목표는 이 데이터에 함수형 스타일로 다양한 연산을 적용하는 것입니다.

예제 1: 고차 함수 사용


// 모든 리스트 요소에 동작을 적용한다
public static void ForEach<T>(List<T> items, Action<T> action)
{
    foreach (var item in items)
    {
        action(item);
    }
}

사용 예:


var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // 함수-파라미터

보이시죠? 함수를 변수에 담거나 일반 값처럼 전달할 수 있습니다 — 마치 주방에서 사과를 전달하듯이!

예제 2: 순수 함수

프로그램 상태를 변경하지 않고 입력에만 의존하는 함수:


int MultiplyByTwo(int x)
{
    return x * 2;
}
  • 외부 어떤 것에도 의존하지 않습니다.
  • 외부를 변경하지 않습니다.
  • x = 5이면 항상 10을 반환합니다.

다음은 전역 변수를 사용하고 변경하는 함수와 비교해보세요:


int total = 0;
int AddToTotal(int x)
{
    total += x;
    return total;
}

이건 더 이상 순수 함수가 아닙니다 — 결과가 외부 상태에 의존하고 그것을 변경합니다.

예제 3: 데이터 불변성

입력 데이터를 변경하는 대신 새로 만듭니다:


List<int> AddOneToEach(List<int> numbers)
{
    return numbers.Select(n => n + 1).ToList();
}

입력 리스트는 전혀 변경되지 않습니다. 멀티스레드 환경에서는 잠금과 데이터 레이스가 줄어들어 특히 편합니다.

예제 4: 함수 합성

모든 짝수의 제곱 합을 구하기:


int SumOfEvenSquares(List<int> numbers)
{
    return numbers
        .Where(n => n % 2 == 0)     // 짝수만 남긴다
        .Select(n => n * n)         // 제곱한다
        .Sum();                     // 합친다
}

읽기 쉽고 선언적입니다: 각 연산이 별도의 함수입니다.

3. 유용한 팁

FP, LINQ 그리고 C#

LINQ는 컬렉션에 대한 거의 "실무 함수형"입니다: 고차 함수(Where, Select 등)를 사용해서 원본을 변경하지 않고 새 시퀀스를 얻고, 각 변환은 별도의 표현입니다. 결과는 IEnumerable<T>로, 무엇을 얻을지 설명하고 어떻게 순회할지는 지연시킵니다.

유추 테이블

명령형 (절차적/OOП) 함수형 (LINQ/FP 스타일)
foreach (var x in xs) ...
xs.Select(...)
컬렉션을 "변경"한다 새 컬렉션을 얻는다
상태 (total += x) 순수 함수 (xs.Sum())
"이걸 해라"로 기술 "우리가 얻고 싶은 것"으로 기술

FP vs OOP: 두 세계 — 같은 C#

서로 대립하는 진영이 아닙니다. 현실의 C# 프로젝트에서는 둘을 섞어서 씁니다: 도메인 모델은 클래스(OOP)로 구성하는 게 편하고, 컬렉션 처리나 데이터 집계, 변환은 LINQ, 람다, 확장 메서드를 통한 함수형 스타일로 처리합니다.

델리게이트에 대한 지식은 직접적으로 유용합니다: Func<T, TResult>, Predicate<T>, Action<T>는 FP 스타일의 전형적인 빌딩 블록입니다.

범용 필터 함수:


List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
    var result = new List<T>();
    foreach (var item in items)
    {
        if (predicate(item))
            result.Add(item);
    }
    return result;
}

호출 예:


var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);

다양한 조건의 여러 메서드를 만드는 대신 하나의 범용 함수를 사용합니다.

왜 고용주와 인터뷰어는 FP 개발자를 선호할까?

  • FP는 전체 시스템을 띄우지 않고 작은 코드 블록을 테스트하는 걸 돕습니다.
  • 로직을 유지보수하기 쉽습니다: 상태가 적으면 버그 원인도 적습니다.
  • 병렬 및 비동기 코드를 작성하기 쉬워집니다 — 전역 상태가 없으니 데이터 레이스가 줄어듭니다.

그리고 "광신"이 되지 않는 법

네, FP는 강력합니다. 하지만 C#은 완전한 함수형 언어가 아니고, 모든 문제에 완벽한 순수성이 필요한 건 아닙니다. 지역 변수와 적절한 뮤테이션을 필요할 때 쓰는 걸 두려워하지 마세요. 중요한 건 가독성, 예측 가능성, 테스트 용이성입니다. FP 요소는 도구일 뿐, 신념체계가 아닙니다.

4. 초보자가 흔히 하는 실수

겉으로는 함수형처럼 보이지만 실제로는 그렇지 않은 코드를 만들기 쉽습니다.

예를 들어, 함수가 새 컬렉션을 반환하지만 그 과정에서 원본 리스트를 변형하면 불변성 원칙을 깨고 호출자의 기대를 망가뜨립니다.

또 다른 예: 람다가 외부 변수를 참조하고 변경한다면, 이는 부작용으로 간주되어 코드 행위의 예측 가능성을 떨어뜨립니다.

C# 컴파일러가 이를 막아주지 않습니다: 언어 자체가 둘 다 허용하니까요. 그래서 FP 실천에서는 함수가 "자기 자신으로서" 살도록, 외부를 변경하거나 외부에서 읽지 않고 오직 인자만 사용하는지 주의하는 것이 중요합니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION