대리자(Delegate)

  • 35 minutes to read

영어 단어 delegate는 '위임하다' 또는 '대신하다'의 뜻을 가지고 있습니다. 대리자는 매개변수 목록 및 반환 형식이 있는 메서드에 대한 참조(포인터)를 나타내는 형식입니다. 여기서는 이미 있는 함수의 기능을 대신 호출해주는 개념인 대리자(delegate)에 대해 학습해보겠습니다.

> // 대리자: 매개 변수 목록 및 반환 형식이 있는 메서드에 대한 참조(포인터)를 나타내는 형식

대리자(위임/델리게이트)

대리자는 delegate 키워드를 사용하여 만듭니다. 대리자는 함수 자체를 하나의 데이터로 보고 대리자는 이름의 의미 그대로 다른 메서드를 대신 실행하는 기능입니다. 한 번에 하나 이상의 메서드를 대신해서 호출할 수 있는 개념입니다.

다음 리스트는 대리자에 대한 보충 내용이기에 모르는 내용이더라도 간단히 읽어보고 넘어갑니다.

  • 자동차 개체와 연관을 진다면 대리운전처럼 대리자(대리운전수)를 통해서 집에까지 좌회전(), 우회전(), 직진(), 주차() 등의 동작을 대신해서 할 수 있게 하는 개념과 비슷합니다.
  • 메서드의 매개 변수로 대리자 변수(개체)를 넘길 수 있습니다. 대리자를 사용하여 함수의 매개변수로 함수 자체를 전달할 수 있습니다.
  • 메서드의 매개 변수로 또 다른 메서드의 호출을 넘기는 기능입니다.
  • 대리자는 동일한 메서드 시그니처를 갖는 메서드의 참조를 담을 수 있는 그릇의 역할을 합니다.
  • 대리자는 람다(Lambda)와 같은 개념으로 봐도 됩니다.
  • 대리자를 사용하면 함수를 모아 놓았다가 나중에 실행하거나 실행 취소를 할 수 있습니다.
  • 대리자는 내부적으로 MulticastDelegate 클래스로부터 기능을 상속합니다.
  • 대리자는 앞으로 배울 이벤트(Event)를 만들어내기 위한 중간단계의 키워드로 존재합니다.
  • 참고로, C/C++의 함수포인터와 비슷한 개념으로 볼 수 있습니다.

대리자 만들고 사용하기

대리자를 사용하는 간단한 예제를 먼저 살펴보겠습니다. 다음 코드를 작성 후 실행해보세요.

코드: DelegateDemo.cs

using System;

class DelegateDemo
{
    //[1] 함수 생성 -> 매개 변수도 없고 반환값도 없는 함수
    static void Hi() => Console.WriteLine("안녕하세요.");

    //[2] 대리자 생성 -> 매개 변수도 없고 반환값도 없는 함수를 대신 실행할 대리자
    delegate void SayDelegate();

    static void Main()
    {
        //[A] Hi 함수를 say 이름으로 대신해서 호출
        SayDelegate say = Hi;
        say();

        //[B] Hi 함수를 hi 이름으로 대신해서 호출: 또 다른 모양
        var hi = new SayDelegate(Hi);
        hi();
    }
}
안녕하세요.
안녕하세요.

Hi() 함수는 매개 변수도 없고 반환 값도 없는 간단한 함수입니다. 이러한 동일한 스타일의 함수를 대신 호출해 줄 수 있는 대리자는 [2]번 코드와 같이 delegate void 함수명(); 형태로 구현이 가능합니다. 이렇게 만들어진 대리자는 [A] 코드 영역에서 처럼 이미 만들어 있는 Hi 함수를 대체할 목적으로도 사용이 가능합니다. Hi 함수를 say 이름으로 대힌 호출해줄 수 있다는 것만 이번 예제에서는 기억해두면 됩니다. 대리자는 이처럼 이미 있는 함수를 대신 호출(위임해서 호출)하는 개념이 첫 번째 목적입니다.

대리자를 사용하여 메서드 대신 호출하기

대리자를 생성했으면 해당 대리자를 사용하여 동일한 구조의 다른 메서드를 대신 호출할 수 있습니다. 대리자 형식 변수에 메서드를 등록하는 코드는 다음과 같습니다.

문법:

대리자 변수 = new 대리자(메서드이름);

대리자에는 += 연산자를 사용하여 하나 이상의 대신할 메서드를 등록할 수 있습니다.

**문법:**0

대리자 변수 += new 대리자(메서드이름);

대리자로 함수 대신 호출

델리게이트로 발음하는 대리자는 또 다른 표현으로는 "함수 포인터"라고 말할 수 있습니다. 다른 함수(메서드)를 대신 호출해주는 개념입니다. Hello() 이름으로 간단히 문자열만 출력하는 메서드를 대신 호출해주는 SayPointer 대리자를 만들고 사용하는 예제를 만들어 보겠습니다.

다음 내용를 소스 편집기에 입력 후 실행해보세요.

코드: DelegateNote.cs

using System;

class DelegateNote
{
    //[1] 대리자 생성: 매개 변수도 없고 반환 값이 없는 함수(메서드)를 담을 수 있는 포인터
    delegate void SayPointer();

    //[2] 샘플 함수 생성 
    static void Hello() => Console.WriteLine("Hello Delegate");

    static void Main()
    {
        //[A] 대리자의 인스턴스 생성 후 매개 변수로 대신 실행할 함수명 전달 
        SayPointer sayPointer = new SayPointer(Hello);

        //[B] 대리자 인스턴스로 함수 대신 호출하는 2가지 방법
        sayPointer(); // 대리자 변수에 괄호를 붙여 메서드 호출 
        sayPointer.Invoke(); // 명시적으로 Invoke() 메서드 호출
    }
}
Hello Delegate
Hello Delegate

[1]번 코드에서 delegate 키워드를 사용하여 void SayPointer() 코드로 반환 값이 없고 매개 변수가 없는 메서드를 대신 호출해주는 대리자를 만들었습니다. [2]번 코드에서는 SayPointer 대리자에 담아서 실행할 테스트 메서드인 Hello() 메서드를 만들었습니다. [A]와 같이 SayPointer의 인스턴스를 만들고 생성자에 대신 실행할 메서드 이름을 지정하는 식으로 대리자 개체를 생성할 수 있습니다. [B] 코드에서는 대리자 인스턴스 개체를 사용하여 메서드를 호출하는 2가지 방법을 보여줍니다. 하나는 직접 괄호를 붙여서 메서드처럼 호출하는 것이고 다른 하나는 Invoke() 메서드를 명시적으로 호출해서 실행하는 방법입니다.

대리자를 사용하여 원의 넓이를 구하는 함수를 대신 호출하기

대리자로 메서드를 대신 호출해주는 내용을 C# Interactive에서 아래 순서대로 한번 더 실행해 보세요. 프로젝트 기반 소스는 DelegateGetArea.cs 파일에 있습니다.

(1) 매개 변수가 int 형이고 반환값이 double 형인 GetArea() 메서드가 다음과 같이 있습니다.

> double GetArea(int r) => 3.14 * r * r;

(2) GetArea() 메서드를 대신 호출해 줄 수 있는 함수 포인터 역할을 하는 GetAreaPointer 대리자는 다음과 같이 만들 수 있습니다.

> delegate double GetAreaPointer(int r);

(3) 동일한 매개 변수와 반환값을 갖는 메서드를 대리자 개체에 담습니다.

> GetAreaPointer pointer = GetArea;

(4) 대리자 개체는 Invoke() 메서드 또는 대리자 개체 자체로 호출해서 사용됩니다.

> pointer.Invoke(10)
314
> pointer(10)
314

이름 없는 메서드를 대신 호출

delegate 키워드를 사용하면 이름이 없는 메서드를 생성 후 해당 이름없는 메서드를 대신 호출해 줄 수 있습니다.

코드: AnonymousDelegate.cs

using System;

class AnonymousDelegate
{
    delegate void SayDelegate();
    static void Main()
    {
        // `delegate` 키워드로 함수를 바로 정의해서 사용
        SayDelegate say = delegate ()
        {
            Console.WriteLine("반갑습니다.");
        };
        say();
    }
}
반갑습니다.

위 코드는 익명 메서드 또는 무명 메서드라 불리는 메서드를 delegate 키워드로 만들고 이를 SayDelegate 개체를 통해서 대신 호출해주는 코드 모양입니다. 잠시 후에 무명 메서드에 대해 다시 살펴보겠습니다.

함수 포인터

대리자 형식은 함수 포인터(Function Pointer)로도 불립니다. 대리자를 생성할 때 사용되는 delegate 키워드는 이름이 없는 메서드(무명 메서드)를 만들 때에도 함께 사용됩니다.

코드: FunctionPointer.cs

> //[1] 함수 포인터 형식
> public delegate void Whats();
> //[2] 함수 포인터 정의
> Whats whats = delegate { Console.WriteLine("함수 포인터 == 대리자"); };
> //[3] 함수 포인터 호출
> whats();
함수 포인터 == 대리자

강력한 형식의 대리자

Math.Pow() 메서드를 대신 호출하는 DelegateType 이름의 대리자를 만들고 사용해 보겠습니다.

코드: StronglyTypedDelegate.cs

> // 강력한 형식의 대리자 
> public delegate double DelegateType(double x, double y);
> DelegateType pow = Math.Pow;
> double actual = pow(2, 10);
> actual
1024

[실습] 대리자를 사용한 여러 개의 메서드 다중 호출

소개

이번 실습을 통해서 하나의 대리자 형식의 변수에 여러 개의 메서드를 등록 후 한 번에 여러 개의 메서드를 호출하는 메서드 다중 호출에 대한 내용을 살펴보겠습니다.

따라하기

(1) 새로운 C# 콘솔 프로젝트를 다음과 같이 생성합니다.

프로젝트 형식 템플릿 이름 위치
Visual C# 콘솔 응용 프로그램 DelegatePractice C:\C#

(2) 솔루션 탐색기에서 Program.cs 파일을 DelegatePractice.cs 파일로 이름을 변경한 후 이미 만들어져 있는 모든 코드를 삭제한 후 다음과 같이 프로그램을 작성합니다. 편의상 CarDrive 클래스의 멤버 메서드는 모두 정적(static) 멤버로 구성하였습니다.

코드: DelegatePractice.cs

using System;

namespace DelegatePractice
{
    public class CarDriver
    {
        public static void GoForward() => Console.WriteLine("직진");
        public static void GoLeft() => Console.WriteLine("좌회전");
        public static void GoRight() => Console.WriteLine("우회전");
    }

    public class Insa
    {
        public void Bye() => Console.WriteLine("잘가");
    }

    // [!] 대리자 생성: 의미상으로 대리운전, class와 같은 레벨로 생성해도 됨
    public delegate void GoHome();

    public class DelegatePractice
    {
        // [!] 대리자 형식(Type) 선언 : 메서드를 묶을 별칭, 클래스 내부에 생성도 가능
        public delegate void Say();

        private static void Hello() { Console.WriteLine("Hello"); }
        private static void Hi() { Console.WriteLine("Hi"); }

        static void Main(string[] args)
        {
            // [1] 메서드 따로 따로 호출
            CarDriver.GoLeft();
            CarDriver.GoForward();
            CarDriver.GoRight();

            // [2] 대리자를 통한 메서드 등록 및 호출
            GoHome go = new GoHome(CarDriver.GoLeft);
            go += new GoHome(CarDriver.GoForward);
            go += new GoHome(CarDriver.GoRight);
            go += new GoHome(CarDriver.GoLeft); // 등록
            go -= new GoHome(CarDriver.GoLeft); // 취소
            go(); // 집에 갑시다... 한번 호출

            Console.WriteLine();

            // [3] 대리자를 통해서 한번에 2개의 메서드 호출...
            Say say;                // [a] 대리자 형 변수 선언
            say = new Say(Hi);      // [b] Hi 메서드 지정
            say += new Say(Hello);  // [c] Hello 메서드 지정
            say();                  // [d] 대리자로 두 개의 메서드 호출
            
            // [4] 대리자를 통해서 호출
            Insa insa = new Insa();
            Say say2 = new Say(insa.Bye);
            say2 += new Say(insa.Bye);
            say2();
        }
    }
}

(3) 소스 코드를 다 입력한 후 Ctrl+F5를 눌러 프로그램을 실행하면 명령 프롬프트 창에 다음과 같이 출력됩니다.

좌회전
직진
우회전
좌회전
직진
우회전

Hi
Hello
잘가
잘가

마무리

현실 세계에서의 대리 운전을 한번 생각해보겠습니다. 내가 혼자 운전한다면, 좌회전(), 직진(), 우회전()을 내가 모두 수행하는 반면에, 대리운전 기사를 부르는 경우에는 "어디에 있는 집에 갑시다."라고 요청을 하면 대리운전 기사가 알아서 좌회전(), 직진(), 우회전()을 해서 집에 도착해 주는 경우를 프로그램 코드로 표현을 해 본 것입니다.

대리자는 여러 메서드를 묶어서 단계별로 실행하는 개념으로 파이프라인(Pipeline)으로 표현할 수도 있습니다. 나중에 ASP.NET Core를 학습하게되면 미들웨어라는 또 다른 개념으로 파이프라인의 의미를 더 잘 이해할 수 있습니다.

대리자를 사용한다는 것은 어찌보면 코드의 양을 증가시키고, 그 필요성이 많이 있지 않을 수도 있습니다. 하지만 대리자는 다음 장에서 살펴볼 이벤트(Event)를 만들어내기 위한 필수 키워드로 존재하기에 그 쓰임새는 메서드와 이벤트의 중간 정도에 위치해서 이벤트를 위한 헬퍼(Helper) 정도의 역할을 한다고 보면 됩니다.

무명 메서드(Anomynous Method)

무명 메서드익명 메서드라고도 하며 단어 그대로 이름이 없는 메서드입니다. 이번에는 무명 메서드를 사용해보겠습니다.

예제: 무명 메서드 사용

코드: AnonymousMethod.cs

// 무명 메서드(Anonymous Method): 대리자를 축약해서 표현하는 방법
using System;

namespace AnonymousMethod
{
    public class Print
    {
        public static void Show(string msg) => Console.WriteLine(msg);
    }

    public class AnonymousMethod
    {
        //[!] 대리자 선언
        public delegate void PrintDelegate(string msg);
        public delegate void SumDelegate(int a, int b);
        static void Main()
        {
            //[1] 메서드 직접 호출
            Print.Show("안녕하세요.");

            //[2] 대리자에 메서드 등록 후 호출
            PrintDelegate pd = new PrintDelegate(Print.Show);
            pd("반갑습니다.");

            //[3] 무명(익명) 메서드로 호출: `delegate` 키워드로 무명 메서드 생성 
            PrintDelegate am = delegate (string msg)
            {
                Console.WriteLine(msg);
            };
            am("또 만나요.");

            //[4] 무명메서드 생성 및 호출
            SumDelegate sd = delegate (int a, int b) { Console.WriteLine(a + b); };
            sd(3, 5); // 8
        }
    }
}
안녕하세요.
반갑습니다.
또 만나요.

Print 클래스에는 Show() 메서드가 하나 준비되어 있습니다. Show() 메서드를 사용하는 방법은 위 코드의 [1]번이 기본 방식이며, [2]번 코드처럼 새로운 PrintDelegate 대리자를 만들고 이곳에 담아서 대신 호출해 주는 방식이 있고, 좀 더 나가서 [3]번과 [4]번처럼 delegate 키워드로 이미 만들어 있는 함수가 아닌 해당 시점에 이름이 없는 무명 메서드를 만들고 이를 호출해서 사용할 수 있습니다.

사실, 박용준 강사도 무명 메서드람다 식의 등장으로 거의 사용을 안하게되는 기능 중 하나이니 위 예제 정도만 학습해 두기 바랍니다.

다음 샘플 코드를 보면 대리자 만들고 대리자에 특정 메서드를 무명 메서드가 아닌 람다 식으로 바로 만들고 사용할 수 있습니다. 이처럼 대리자에는 직접 메서드를 적용하던가 무명 메서드를 사용하던가 아니면 람다 식을 사용하던가를 결정할 수 있습니다.

코드: IntParameterAndIntReturnClass.cs

> delegate int IntParameterAndIntReturnDelegate(int x);
> IntParameterAndIntReturnDelegate pow = (x) => x * x;
> pow(3)
9

대리자 개체에 람다 식(Lambda Expression) 담기

대리자에 람다 식을 담아서 대신 호출해줄 수 있습니다. 간단한 형태의 메서드를 따로 만들지 않고 람다 식으로 만들면 됩니다.

코드: LambdaExpression.cs

> //[1] 대리자 선언
> delegate void Lambda();
> //[2] 대리자 개체에 람다식 정의: goes to 연산자
> Lambda hi = () => Console.WriteLine("안녕하세요.");
> //[3] 대리자 개체 호출
> hi();
안녕하세요.

위 코드와 같이 매개 변수가 없는 람다 식은 () => 본문 형태로 구현하면 됩니다.

파이썬: 매개변수가 없는 가장 기본적인 모양의 람다 식

코드: LambdaExpression.py

>>> # 람다 식(Lambda Expression)
>>> ## 매개변수가 없는 람다 식 만들기
>>> hi = lambda : print("Hello.")
>>> hi()
Hello.
>>> bye = lambda : print("Bye.")
>>> bye()
Bye.

매개 변수도 있고 반환값도 있는 경우의 람다 식

이번에는 매개 변수도 있고 반환값도 있는 람다 식을 사용해보겠습니다.

코드: LambdaExpressionArgs.cs

> //[1] 매개 변수도 있고 반환값도 있는 대리자 선언
> delegate int Lambda(int i);
> //[2] 람다 식으로 대리자 개체 생성
> Lambda square = x => x * x;
> square(3)
9
> square(4)
16

위 코드는 람다 식의 x => x * x; 형식으로 넘어온 정수 형식을 2번 곱해서 반환해주는 함수를 square 이름으로 대신 사용하는 내용입니다.

파이썬: 매개변수도 있고 반환값도 있는 경우의 람다 식

코드: LambdaExpressionArgs.py

>>> #[?] 람다식: 매개 변수도 있고 반환값도 있는 경우
>>> #[1] 매개 변수도 있고 반환값도 있는 람다 식 선언
>>> square = lambda x: x * x
>>> square(3)
9
>>> square(4)
16

람다 식에서 형식 선언

이번에는 람다 식에서 형식을 선언하는 방법을 알아보겠습니다. 다음 내용을 Visual Studio의 C# Interactive에 입력한 뒤 실행해보세요.

코드: LambdaExpressionTypeDeclare.cs

> delegate bool Lambda(string msg, int len);
> // 람다 식에 형식 선언
> Lambda isLong = (string msg, int len) => msg.Length > len;
> isLong("안녕하세요.", 5)
true
> isLong("반갑습니다.", 10)
false

람다 식에서 여러 줄 코드 처리

이번에는 람다 식에서 여러 줄 코드 처리하는 방법을 알아보겠습니다.

코드: LambdaExpressionMultiLine.cs

> delegate void Hi();
> Hi hi = () =>
. {
.     Console.WriteLine("안녕하세요.");
.     Console.WriteLine("반갑습니다.");
. };
> 
> hi();
안녕하세요.
반갑습니다.

람다 식에 줄괄호를 사용하여 여러 줄을 처리할 수 있습니다.

메서드의 매개 변수에 대리자 형식을 사용하기

대리자는 메서드의 매개 변수에 전달될 수 있습니다.

예제: 메서드의 매개변수에 대리자 사용하기

코드: DelegateParameter.cs

using System;

class DelegateParameter
{
    //[?] 대리자(델리게이트)는 동일한 메서드 시그니처를 갖는 메서드의 참조를 담을 수 있는 그릇
    delegate void Runner();

    static void Main()
    {
        RunnerCall(new Runner(Go));     // "직진"
        RunnerCall(new Runner(Back));   // "후진"
    }

    static void RunnerCall(Runner runner) => runner(); // 넘어온 메서드(함수) 실행        
    static void Go() => Console.WriteLine("직진");
    static void Back() => Console.WriteLine("후진");
}
직진
후진

특정 메서드의 매개 변수에 대리자 형식을 지정하면 해당 대리자를 받고 해당 메서드내에서 매개 변수로 넘어온 대신 호출해주는 메서드를 실행할 수 있습니다.

Action 대리자와 Func 대리자 그리고 Predicate 대리자

.NET API에 내장된 유용한 제네릭 대리자는 Action 제네릭 대리자와 Func 제네릭 대리자 그리고 Predicate 제네릭 대리자가 있습니다.

  • Action 대리자: 반환 값이 없는 메서드를 대신 호출합니다.
  • Func 대리자: 매개 변수와 반환 값이 있는 메서드를 대신 호출합니다.
  • Predicate 대리자: T 매개 변수에 대한 bool 값을 반환하는 메서드를 대신 호출합니다.

Action<T> 대리자 사용하기

Action 제네릭 대리자를 사용하면 Console.WriteLine과 같은 메서드를 대신 호출할 수 있습니다.

> // `Action` 제네릭 대리자 
> Action<string> printf = Console.WriteLine;
> printf("메서드 대신 호출");
메서드 대신 호출

Func<T> 대리자 사용하기

Func<매개 변수 형식, 반환 값 형식> 형태로 특정 메서드 또는 익명 메서드를 대신 호출해줄 수 있는 대리자 개체를 만들 수 있습니다.

Func<T>의 가장 오른쪽 T는 반환 값을 나타냅니다. 다음 코드는 Math.Abs() 메서드를 대신 호출해주는 abs()를 만들고 호출하는 내용을 보여줍니다.

> Func<int, int> abs = Math.Abs;
> abs(-10)
10

다음 코드는 2개의 입력 매개 변수를 받는 Math.Pow() 메서드를 대신 호출해주는 pow() 함수를 만드는 방법을 보여줍니다.

> Func<double, double, double> pow = Math.Pow;
> pow(2, 20)
1048576

다음 샘플 코드는 문자열을 입력받으면 해당 문자열을 소문자로 변환 후 반환해주는 toLower 대리자를 만들어 줍니다.

> Func<string, string> toLower = str => str.ToLower();
. Console.WriteLine(toLower("LambdaExpression"));
lambdaexpression

Func 대리자와 람다 식을 사용하면 LINQ와 같이 C#에서 함수형 프로그래밍 스타일로 개발할 수 있습니다.

Func 대리자는 이미 있는 메서드 이외도 익명 메서드 및 람다 식을 담아 사용이 가능합니다.

코드: FuncLambda.cs

> //[1] 익명 메서드 담기
> Func<int, int> anonymous = delegate (int x) { return x * x; };
> anonymous(2)
4
> //[2] 람다 식 담기
> Func<int, double> lambda = x => x / (double)2;
> lambda(3)
1.5

Func 대리자를 사용하여 람다 식 만들기

이번에는 Func<T, T> 대리자를 사용하여 람다 식을 만드는 방법을 살펴보겠습니다.

코드: LambdaExpressionWithFunc.cs

> //[1] 매개 변수가 int이고 반환값이 int인 람다 식
> Func<int, int> square = x => x * x;
> square(3)
9

다음 내용은 동일한 내용을 무명 메서드와 람다 식을 사용하여 표현한 내용입니다. 결론적으로 말해서 무명 메서드는 사용할 일이 없고 대신에 람다 식을 사용하면 됩니다.

코드: LambdaExpressionWithFunc.cs

> //[2] 무명 메서드와 람다 식 비교
> Func<int> getNumber1 = delegate () { return 1234; };
> getNumber1()
1234
> Func<int> getNumber2 = () => 1234;
> getNumber2()
1234
> Func<int, int> addOne1 = delegate (int x) { return x + 1; };
> addOne1(10)
11
> Func<int, int> addOne2 = x => x + 1;
> addOne2(10)
11
> Func<string, string, string> plus1 = delegate (string a, string b) { return $"{a} {b}"; };
> plus1("Hello", "World")
"Hello World"
> Func<string, string, string> plus2 = (a, b) => $"{a} {b}";
> plus2("Hello", "World")
"Hello World"

Func 대리자로 메서드 대신 호출

Func 제네릭 대리자는 람다 식을 포함한 무명 메서드 또는 일반 메서드를 대신 호출해 주는 기능을 제공합니다.

코드: FuncDemo.cs

// `Func` 대리자
using System;

class FuncDemo
{
    static void Main()
    {
        // [1] int를 입력 받아 0이면 true을 반환
        Func<int, bool> zero = number => number == 0;
        Console.WriteLine(zero(1234 - 1234)); // True

        // [2] int를 입력 받아 1을 더한 값을 반환
        Func<int, int> one = n => n + 1;
        Console.WriteLine(one(1)); // 2

        // [3] int 2개를 입력 받아 더한 값을 반환
        Func<int, int, int> two = (x, y) => x + y;
        Console.WriteLine(two(3, 5)); // 8
    }
}
True
2
8

Func 대리자를 사용하면 따로 delegate 키워드를 사용하지 않고도 대신 람다 식 또는 함수 등을 만들고 호출할 수 있습니다.

10.4.1. FuncDemo.java

코드: FuncDemo.java

// Func Delegate(대리자) 사용하기 - Function, BiFunction 인터페이스를 사용하여 초간단 람다식 표현  
import java.util.function.Function;
import java.util.function.BiFunction; 

public class FuncDemo {
    public static void main(String[] args) {
        //[1] Function<매개변수, 반환값> isZero = ... 
        // zero.apply() 메서드는 0인지 아닌지를 판단 
        Function<Integer, Boolean> zero = number -> number == 0;
        System.out.println("Result: " + zero.apply(1234 - 1234)); // true
        System.out.println("Result: " + zero.apply(1234)); // false

        //[2] one.apply() 메서드는 정수에 1을 더해서 반환해주는 기능 
        Function<Integer, Integer> one = n -> n + 1;
        System.out.println("Result: " + one.apply(1)); // 2

        //[3] two.apply() 메서드는 두 수의 합을 반환
        BiFunction<Integer, Integer, Integer> two = (x, y) -> x + y;
        System.out.println("Result: " + two.apply(3, 5)); // 8
    }
}
Result: true
Result: false
Result: 2
Result: 8

Func 대리자로 메서드 또는 람다 식 대신 호출하기

이번에는 프로젝트 기반 소스로 Func 대리자를 사용해보겠습니다.

코드: FuncDelegate.cs

using System;

class FuncDelegate
{
    static void Main()
    {
        // [1] Add 함수 직접 호출
        Console.WriteLine(Add(3, 5));

        // [2] `Func` 대리자로 Add 함수 대신 호출: 반환값이 있는 메서드를 대신 호출
        Func<int, int, string> AddDelegate = Add; // Add 메서드를 대신 호출
        Console.WriteLine(AddDelegate(3, 5));

        // [3] 람다식(Lambda): 메서드 -> 무명메서드 -> 람다식으로 줄여서 표현
        Func<int, int, string> AddLambda = (a, b) => (a + b).ToString(); 
        Console.WriteLine(AddLambda(3, 5));
    }

    /// <summary>
    /// 두 수의 합을 문자열로 반환
    /// </summary>
    static string Add(int a, int b) => (a + b).ToString();
}
8
8
8

Func 대리자를 사용하면 동일한 매개 변수와 반환 값이 있는 메서드를 대신해서 호출할 수 있습니다. 특정 메서드가 구현되어 있지 않으면 무명 메서드 또는 람다 식을 사용하여 바로 함수를 만들고 호출할 수 있습니다.

Predicate 대리자 사용하기

Predicate<T> 대리자는 T를 매개 변수로 받은 후 어떤 로직을 수행한 후 그 결과를 bool 형식으로 반환하는 메서드를 대신 호출해 줍니다.

코드: PredicateDemo.cs

> Predicate<string> isNullOrEmpty = String.IsNullOrEmpty;
> isNullOrEmpty("Not Null")
False
> Predicate<Type> isPrimitive = t => t.IsPrimitive;
> isPrimitive(typeof(int))
true

Predicate 제네릭 대리자를 메서드의 매개 변수로 사용하기

매개 변수에 Func<T>, Action<T>, Predicate<T> 형식을 지정한 메서드는 람다 식을 매개 변수로 받을 수 있습니다. 다음 코드를 작성 후 실행해 보세요. FindNumbers()는 1부터 100까지의 정수 중에서 33의 배수를 구하는 함수입니다.

코드: LambdaExpressionWithPredicate.cs

// 
> // 매개 변수로 람다 식을 받는 메서드
> static IEnumerable<int> FindNumbers(Predicate<int> predicate)
. {
.     for (int i = 1; i <= 100; i++)
.     {
.         if (predicate(i))
.         {
.             yield return i; 
.         }
.     }
. }
> var numbers = FindNumbers(f => f % 33 == 0);
> numbers
FindNumbers { 33, 66, 99 }

메서드의 매개 변수로 메서드 전달하기

닷넷에 내장된 제네릭 대리자인 Func 대리자를 사용하면 메서드의 매개 변수로 int, string과 같이 메서드 이름 자체를 지정해서 메서드를 넘겨줄 수 있습니다.

코드: PassMethodAsParameter.cs

using System;

class PassMethodAsParameter
{
    // [1] 입력한 문자열의 길이를 반환하는 메서드
    static int StringLength(string data) => data.Length;

    // [2] 매개 변수가 string이고 반환값이 int인 메서드를 매개 변수로 받아 사용
    static void StringLengthPrint(Func<string, int> stringLength, string message)
        => Console.WriteLine($"메시지의 크기는 {stringLength(message)}입니다.");

    // [A] 메서드의 매개 변수로 특정 메서드(StringLength) 전달하기 
    static void Main() => StringLengthPrint(StringLength, "안녕하세요.");
}
메시지의 크기는 6입니다.

[1]번 코드의 StringLength() 메서드를 [2]번 코드의 StringLengthPrint() 메서드의 첫 번째 매개 변수로 전달하는 예입니다. [A] 코드의 첫 번째 매개 변수에 메서드 이름을 지정하여 전달하는 것을 볼 수 있습니다.

이러한 유형의 프로그래밍 형태는 학습 시기에는 많이 사용되지 않으나 .NET API에 굉장히 많이 적용되어 있기에 프로그래밍을 계속 학습하다보면 많이 접할 예정입니다.

메서드의 매개 변수로 람다 식 전달

Calc() 메서드의 마지막 매개 변수에 Func<T> 형태로 람다 식을 전달하여 덧셈 또는 곱셈을 구현한 예제입니다.

코드: FuncParameter.cs

> // 메서드의 매개 변수로 람다 식 전달 
> void Calc(int x, int y, Func<int, int, int> calc) => Console.WriteLine(calc(x, y));
> Calc(3, 5, (x, y) => x + y);
8
> Calc(3, 5, (x, y) => x * y);
15

참고: Expression<T>

.NET에는 Func<T> 이외에 Expression<T> 클래스도 제공합니다. Func<T>는 바로 실행 가능한 대리자 개체를 생성하는 대신에 Expression<T>는 대리지 구문 자체를 담고 있는 개체를 만들고 이를 사용하려면 Compile()과 같은 추가적인 메서드를 호출하여 대리자 개체를 만들고 사용할 수 있습니다. Expression<T> 클래스에 대한 내용은 Expression Tree라는 개념으로 이 강의의 범위를 벗어나기에 C# Interactive에서 다음 코드를 한 번 정도 실행해 보고 넘어가세요.

> Func<int, bool> isBig = i => i > 5;
> isBig(10)
true
> Expression<Func<int, bool>> expression = i => i > 5;
> var isBigOther = expression.Compile();
> isBigOther(10)
true

장 요약

대리자와 Func 제네릭 대리자를 사용하면 메서드에 대한 참조를 가지는 새로운 개체를 만들고, 메서드의 매개 변수에 메서드 자체를 전달할 수도 있습니다. 앞서 우리가 사용해 온 LINQ와 확장 메서드는 대리자의 개념을 사용하여 함수형 프로그래밍 스타일로 코드를 작성할 수 있도록 도와줍니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com