CodeGym /행동 /C# SELF /고차 함수 (Higher-Order Functions, HOF)

고차 함수 (Higher-Order Functions, HOF)

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

1. 소개

학교에서 한 번쯤 들어봤을 거야: "이건 함수의 함수다". 고차 함수(Higher-Order Functions, HOF)는 다른 함수를 인수로 받거나, 함수를 결과로 반환하거나, 둘 다 하는 함수야.

쉽게 말하면: 네 메서드가 다른 함수를 파라미터로 받을 수 있다면(델리게이트나 람다 같은), 또는 함수를 반환할 수 있다면 — 축하해, 그건 고차 함수야!

고차 함수는 마스터 키와 비슷해. 스스로 문을 열 수 있을 뿐만 아니라, 나중에 네가 열 수 있게 다른 키를 전달할 수도 있어.

실무에서의 활용

괜히 복잡해 보일 수 있지만, 실무에서 왜 이런 꼼수들이 필요한지 한 문장으로 정리하면:
고차 함수는 코드를 유연하고 재사용 가능하며 간결하게 만든다.
이건 LINQ, 컬렉션 필터링/정렬, 데이터 처리 파이프라인 구축, 콜백/이벤트 설정, 심지어 DI(의존성 주입) 같은 것들의 기반이야.

몇 가지 실제 시나리오:

  • 메서드 동작을 제한하거나 바꾸고 싶을 때 로직(예: 필터, 정렬, 변환)을 전달한다.
  • 공통 처리기를 만들어 필요한 동작을 델리게이트로 "끼워넣기" 한다.
  • 각 함수가 자기 방식대로 데이터를 수정하는 체인(파이프라인)을 만든다.
  • 플랫폼별 추상화: Windows에서 할 일, Linux에서 할 일을 함수로 전달해서 분리한다.

2. 간단한 고차 함수 예제

다른 함수를 받는 함수

가장 고전적인 예 — 델리게이트나 람다를 받는 메서드.


// 고차 함수: process 함수를 매개변수로 받음
void ForEach<T>(IEnumerable<T> collection, Action<T> process)
{
    foreach (var item in collection)
    {
        process(item); // 인수로 받은 함수를 호출
    }
}

// 사용 예:
var numbers = new List<int> { 1, 2, 3 };
ForEach(numbers, n => Console.WriteLine($"요소: {n}"));

무슨 일이 일어나는지? 메서드 ForEach는 각 요소에 대해 뭘 해야 할지 모른다. 할 일은 전달된 프로세서(process)를 호출하는 것뿐이다. 이 프로세서는 화면에 출력하거나, DB에 저장하거나, UI에 그리는 등 어떤 것도 될 수 있다.

참고: 컬렉션의 ForEach나 거의 모든 LINQ 메서드는 사실상 고차 함수야!

함수를 반환하는 함수

이번에는 반환도 하는 예제를 보자.


// 고차 함수: 다른 함수를 반환
Func<int, int> CreateMultiplier(int factor)
{
    // factor 변수를 사용하는 람다를 반환
    return x => x * factor;
}

// 사용 예:
var multiplyBy10 = CreateMultiplier(10);
Console.WriteLine(multiplyBy10(7)); // 70

여기서 CreateMultiplier는 인자를 미리 곱해주는 함수를 반환한다. 이건 함수 팩토리 예시지.

함수를 받고 함수를 반환하는 함수


Func<int, int> Compose(Func<int, int> f, Func<int, int> g)
{
    // g를 적용한 뒤 f를 적용: f(g(x))를 반환
    return x => f(g(x));
}

// 사용 예:
Func<int, int> increment = x => x + 1;
Func<int, int> doubleIt = x => x * 2;

var incrementThenDouble = Compose(doubleIt, increment);

Console.WriteLine(incrementThenDouble(5)); // (5 + 1) * 2 = 12

이런 식의 조합(composition)은 Select, Where, OrderBy 같은 메서드로 데이터 스트림을 처리할 때 핵심이야.

3. C#에서 고차 함수를 어떻게 지원하는가

함수형 언어(Haskell, F# 등)는 기본적으로 모든 함수가 고차 함수야. 하지만 C#도(2.0부터) 델리게이트와 람다 덕에 이 패러다임을 잘 지원해.

  • 델리게이트(Func, Action, Predicate) — 함수 타입.
  • 람다 표현식 — 즉석에서 함수를 만드는 문법.
  • 메서드는 델리게이트를 인자로 받거나 반환할 수 있으므로 고차 함수가 기본 제공된다.

시각적 다이어그램

flowchart LR
    A[데이터] --> B[함수 1]
    B --> C[함수 2]
    C --> D[결과]
    subgraph "처리 파이프라인 (고차 함수)"
        B
        C
    end

4. 애플리케이션 확장하기

단계별 데모 앱을 계속 발전시켜보자 — 이번엔 "미니 문자열 처리기"를 만들어 보자.

간단한 고차 메서드 추가

사용자 이름 목록을 임의로 변형하고 싶다고 해보자. 함수로 변형 로직을 전달한다.


// 문자열 리스트와 변환 함수(transformer)를 받는 메서드
List<string> TransformNames(List<string> names, Func<string, string> transformer)
{
    var result = new List<string>();
    foreach (var name in names)
    {
        result.Add(transformer(name));
    }
    return result;
}

이 메서드를 어떻게 쓰지?


var names = new List<string> { "안나", "보리스", "세르게이" };

// 대문자로 변환
var upperNames = TransformNames(names, n => n.ToUpper());

// 각 이름에 "존경하는(님) " 붙이기
var politeNames = TransformNames(names, n => "존경하는(님) " + n);

foreach (var n in upperNames)
    Console.WriteLine(n); // 안나, 보리스, 세르게이

foreach (var n in politeNames)
    Console.WriteLine(n); // 존경하는(님) 안나, ...

TransformNames는 범용적이다: 변환 로직은 인자로 받은 transformer에게 위임한다. 예를 들어 ToUpper를 호출하거나 다른 레시피를 넣을 수 있다.

타입에 대한 적응

지금 예제는 어떤 타입에도 쉽게 확장할 수 있다.


// 제네릭한 범용 메서드 - 아무 T에 대해 동작하는 고차 함수
List<TResult> Map<T, TResult>(List<T> items, Func<T, TResult> transformer)
{
    var result = new List<TResult>();
    foreach (var item in items)
    {
        result.Add(transformer(item));
    }
    return result;
}

적용 예:


var numbers = new List<int> { 1, 2, 3 };
var doubled = Map(numbers, x => x * 2); // [2, 4, 6]
var strings = Map(numbers, x => $"숫자: {x}"); // ["숫자: 1", ...]

5. 고차 함수를 통한 필터링과 집계

필터링과 검색 로직은 오래전부터 고차 함수로 구현되어 왔다.


// 필터링: 고차 함수
List<T> Filter<T>(List<T> items, Predicate<T> criteria)
{
    var result = new List<T>();
    foreach (var item in items)
    {
        if (criteria(item)) // 기준 함수 호출
        {
            result.Add(item);
        }
    }
    return result;
}

사용 예:


var names = new List<string> { "안나", "보리스", "안드레이" };
var aNames = Filter(names, n => n.StartsWith("안"));
// 결과: "안나", "안드레이"

6. 함수 합성(function composition) 개념

고차 함수는 개별 함수를 조합해서 체인을 만들 수 있게 해준다. C#에서는 두 함수를 받아서 그 조합을 반환하는 식으로 구현할 수 있다.


// 함수 합성기: 먼저 g를 적용하고 그 결과에 f를 적용하는 함수 반환
Func<T, TResult> Compose<T, TIntermediate, TResult>(
    Func<TIntermediate, TResult> f,
    Func<T, TIntermediate> g)
{
    return x => f(g(x));
}

// 예제:
Func<int, int> plusOne = n => n + 1;
Func<int, int> timesTwo = n => n * 2;

var plusOneThenDouble = Compose(timesTwo, plusOne);
Console.WriteLine(plusOneThenDouble(3)); // (3 + 1) * 2 = 8

7. 유용한 팁들

문제의 배경: 왜 예전엔 더 복잡했나?

델리게이트와 람다가 없던 시절엔 개발자들이 비슷한 루프를 잔뜩 복사해서 "필터", "변환", "그룹화" 같은 걸 구현했어. 고차 함수 덕분에 가변적인 동작 부분을 함수 파라미터로 빼낼 수 있게 되었고, 코드 중복을 크게 줄이고 표현력을 끌어올렸지.

조금의 문법적 설탕: 식-본문 메서드(expression-bodied methods)

고차 함수는 종종 한 줄짜리 식-본문 메서드로 쓰면 간결하다:


List<string> FilterNames(Predicate<string> pred) =>
    Names.Where(name => pred(name)).ToList();

List<TResult> MapNames<TResult>(Func<string, TResult> transformer) =>
    Names.Select(transformer).ToList();

LINQ와의 비교

LINQ가 고차 함수를 어떻게 활용하는지 보자:

LINQ 메서드 어떤 델리게이트를 받는가 목적
Where
Func<T, bool>
요소를 필터링
Select
Func<T, TResult>
요소를 변환
OrderBy
Func<T, TKey>
키로 정렬
Aggregate
Func<TAcc, T, TAcc>
컬렉션을 집계(축약)
Any
Func<T, bool>
조건을 만족하는 요소가 있는지 확인

이 모든 메서드는 고차 함수라는 아이디어 위에서 만들어졌어: 네가 동작 규칙을 쓰면 표준 라이브러리가 인프라를 제공하는 방식이지.

8. 가능한 실수와 함정

델리게이트 타입 혼동.
처음엔 Action, Func, Predicate가 헷갈릴 수 있다.
팁: 반환 타입이 bool이면 보통 Predicate다. 값을 반환하면 Func, 아무것도 반환하지 않으면 Action.

변수 캡처(클로저).
반환된 함수가 외부 스코프의 변수를 사용할 때는, 호출 시점에 값이 실제로 무엇인지 신경 써야 해. 값이 복사되는 게 아니라 캡처되는 거야.

복잡한 체인의 디버깅.
함수들이 긴 파이프라인으로 조합될 때 어느 단계에서 데이터가 꼬였는지 파악하기 어려울 수 있다. 중간에 로그를 찍거나 주석을 달자:


n => {
  Console.WriteLine("연산 전:" + n);
  var res = n * 2;
  Console.WriteLine("연산 후:" + res);
  return res;
}
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION