개발공부/C#

[Effective C#] 아이템 7 : 델리게이트를 이용하여 콜백을 표현하라

개발자 찐빵이 2021. 11. 22. 22:12
728x90

콜백이란?

나중에 실행할 함수를 아규먼트로 다른 함수에게 전달하는 것.
함수를 실행할 때 호출한다(call)는 표현을 사용하므로 나중에(back) + 호출한다(call)이 조합되어서 callback이라고 한다.

서버가 클라이언트에게 비동기적으로 피드백을 주기 위해 사용하는 방법이다.
콜백을 위해 멀티스레딩 기술도 사용되고, 동기적으로 상태를 갱신하는 기법도 활용된다.

C#에서 콜백은 델리게이트를 이용하여 표현한다.

델리게이트

메서드를 다른 메서드로 전달하기 위해 만들어진 것. 즉, 메서드를 전달한다!

델리게이트 장점

  • 타입 안정적인 콜백을 정의할 수 있다.
  • 클래스 간의 결합도를 낮춘다.
  • 콜백을 사용해야 하는 클라이언트를 단순하게 구성할 수 있다.
  • 런타임에 콜백 함수를 구성할 수 있다.
  • 하나의 델리게이트에 여러 콜백 함수를 추가할 수 있다.

델리게이트 사용방법.

  1. delegate 선언한다.
  2. delegate를 파라미터로 하는 method를 선언한다.
  3. 전달할 함수를 만든다.
  4. 선언한 delegate 객체를 new 연산자를 사용해서 만들어준다.
    이때 전달할 함수를 deletgate 생성자 아규먼트로 넘겨준다.
  5. 2번의 함수에 4번에서 만든 delegate를 아규먼트로 넣어서 호출하면 된다.

1번, 3번에서 만든 델리게이트와 함수의 return타입과 parameter 타입은 동일해야 한다.

코드 구현

델리게이트를 이용해서 sorting 함수를 구현해보자.
델리게이트를 사용하면 BubbleSort 내부에서 ascending인지, descending인지 상관 안 해도 된다.

// order type을 정해주는 델리게이트 정의
        delegate bool SelectOrderType(int a, int b);

        static bool Ascending(int a, int b)
        {
            return (a > b);
        }

        static bool Descending(int a, int b)
        {
            return (a < b);
        }

        static void BubbleSort(int[] numberArray, SelectOrderType selectOrderType)
        {
            for (int i = 0; i < numberArray.Length - 1; i++)
            {
                for (int j = 0; j < numberArray.Length - (i + 1); j++)
                {
                    if (selectOrderType(numberArray[j], numberArray[j + 1])) //콜백 함수 호출 
                    {
                        int temp = numberArray[j + 1];
                        numberArray[j + 1] = numberArray[j];
                        numberArray[j] = temp;
                    }
                }
            }
        }

        static void Main(string[] args)
        {
            int[] numberArray = { 22, 11, 55, 3, 17 };

            BubbleSort(numberArray, new SelectOrderType(Ascending)); // 오름차순 정렬

            foreach(var num in numberArray)
            {
                Console.Write($"{num} ");
            }

            Console.WriteLine();
            BubbleSort(numberArray, new SelectOrderType(Descending));// 내림차순 정렬
            foreach (var num in numberArray)
            {
                Console.Write($"{num} ");
            }
        }

제네릭 델리게이트

delegate T CalculationDelegate<T>(T x, T y);

static int Add(int x, int y)
{
    return x + y;
}

static double Sub(double x, double y)
{
    return x - y;
}

static void Calculation<T>(T x, T y, CalculationDelegate<T> calculationDelegate)
{
    Console.WriteLine(calculationDelegate(x, y));
}

static void Main(string[] args)
{
    CalculationDelegate<int> Plus = new CalculationDelegate<int>(Add);
    CalculationDelegate<double> Minus = new CalculationDelegate<double>(Sub);

    Calculation(11, 11, Plus);
    Calculation(5.11, 2.22, Minus);
}

자주 사용되는 델리게이트

Predicate : 조건을 검사하여 boolean 값을 반환하는 델리게이트 (Func <T, bool>과 동일하다.)
Action <> : 여러 개의 매개변수를 받지만 반환 타입이 void인 델리게이트
Func <> : 여러 개의 매개변수를 받아 단일의 결괏값을 반환하는 델리게이트

멀티캐스트

모든 델리게이트는 기본적으로 멀티캐스트가 가능하다.
일반적으로 동일한 타입의 매개변수를 취하더라도, 반환 타입이 다른 경우 서로 다른 델리게이트 타입으로 간주한다.
컴파일러는 이 둘 사이의 형 변환을 허용하지 않는다.
멀티캐스트 델리게이트는 한 번만 호출하면 델리게이트 객체에 추가된 모든 대상 함수가 호출된다.

멀티캐스트 델리게이트 주의해야 할 부분

1. 예외에 안전하지 않다.

멀티캐스트 델리게이트의 내부 동작 방식은 대상 함수들을 연속적으로 호출하는 형태이다.
델리게이트는 어떤 예외도 잡지 않으며, 예외가 발생하면 함수 호출 과정이 중단된다.

2. 마지막으로 호출된 대상 함수의 반환 값이 델리게이트의 반환 값으로 간주된다.

델리게이트를 선언할 때는 반환 타입을 void가 아닌 다른 타입으로 지정할 수 있다.

List<ComplicatedClass> container = new List<ComplicatedClass>();

public void LengthyOperation(Func<bool> pred)
{
    foreach(ComplicatedClass cl in container)
    {
        cl.DoLengthyOperation();
        // 사용자가 임의로 중단을 요청했는지 확인
        if (false == pred())
            return;
    }
}

위 메서드를 멀티캐스트 델리게이트 형태로 사용하면 문제가 발생한다.

Func<bool> cp = () => CheckWithUser();
cp += () => CheckWithSystem();
c.LengthyOperation(cp);

델리게이트의 반환 값은 체인 마지막으로 호출 된 함수의 반환값이 되고, 다른 반환값은 모두 무시된다.
따라서 위 예제의 경우 CheckWithUser()의 반환값은 무시된다.
이 문제를 해결하기 위해 델리게이트에 포함된 호출 대상 콜백 함수를 직접 다뤄야 한다.

public void LengtyOperation2(Func<bool> pred)
{
    bool bContinue = true;
    foreach(ComplicatedClass cl in bContinue)
    {
        cl.DoLengthyOperation();
        foreach (Func<bool> pr in pred.GetInvocationList())
            bContinue &= pr();
        if (!bContinue)
            return;
    }
}

델리게이트에 추가된 개별 메서드가 true를 반환한 경우에만 다음 메서드에 대한 호출을 이어간다.

멀티캐스트에 대해 조금 더 알아보기!

반응형