아이템 7: 델리게이트를 이용하여 콜백을 표현하라

콜백이란..?

아빠: “찬우야, 아빠가 책 읽는 동안 마당의 잔디를 깎아주렴”
아들: “아빠, 우선 마당 청소를 했어요”
아들: “아빠, 잔디깎기에 기름을 넣었어요”
아들: “아빠, 잔디깎기가 동작하지 않아요”
아빠: “그래 내가 동작 시켜볼께”
아들: “아빠, 다 했어요”

이 대화는 콜백을 설명하기 위한 것이다.

아빠는 아들에게 일을 시켰고, 아들은 수차례에 걸쳐 아빠에게 상태를 보고했다.

상태를 확인하기 위해 잠깐 책을 읽는 것을 멈추긴 했지만 완전히 중단한 채로 아들의 일을 마치기를 기다리지 않았다.

아들은 중요하다고 생각하는 상태를 아빠에게 알리면서 도움을 청하기도 했다.

콜백은 서버가 클라이언트에게 비동기적으로 피드백을 주기 위해서 사용하는 기법이다.

이를 위해 멀티스레딩기술도 사용되고, 동기적으로 상태를 갱신하는 기법도 활용된다.

C#에서 콜백을 표현하는 방법

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

델리게이트를 이용하면 타입 안정적인 콜백을 정의할 수 있다.

대부분의 경우에 델리게이트는 event와 함께 사용되지만 반드시 그래야 하는 것은 아니다.

여러 클래스가 상호 통신을 수행해야 할 때 클래스 간의 결합도를 낮추고 싶다면 인터페이스보다 델리게이트를 사용하는 것이 좋다.

델리게이트는 런타임에 통지 대상을 설정할 수 있고, 다수의 클라이언트에게 통지를 보낼수도 있다.

하나의 델리게이트는 여러 메서드에 대한 참조를 포함할 수 있기 때문이다.

각 메서드는 정적 메서드일 수도 있고, 인스턴스 메서드일 수도 있다.

즉, 델리게이트를 사용하면 통지를 전달하려는 대상이 한 개일 수도 있고, 여러 개일 수도 있다.

런타임에 구성하기 나름이다..!

예제

콜백과 델리게이트는 C#이 제공하는 관용구의 하나이며 람다 표현식을 사용하는 경우애도 널리 활용된다.

.NET Framework 라이브러리는 Predicate , Action <>, Func <>와 같은 형태로, 자주 사용되는 델리게이트를 정의해두고 있다.

Predicate는 조건을 검사하여 부울값을 반환하는 델리게이트다.

Func<>는 여러 개의 입력 매개변수를 받아서 하나의 결과를 반환하는 델리게이트다.

Action<>은 입력 매개변수를 받지 않고, 반환값도 없는 델리게이트다.

*Func<T, bool>과 Predicate는 동일하다고 볼 수 있다.*

일반적으로 동일한 타입의 매개변수를 취하더라도 반환 타입이 다른 경우 서로 다른 델리게이트 타입으로 간주하며, 컴파일러는 이 둘 사이의 형변환을 허용하지 않는다.

LINQ는 이러한 개념을 기반으로 만들어졌다.

class Program
{
    delegate bool delegateCheck(int n);

    static void Main(string[] args)
    {
        List<int> numbers = Enumerable.Range(1, 200).ToList();

        var oddNumbers = numbers.Find(n => n % 2 == 1);

        var test = numbers.TrueForAll(n => n < 50);

        numbers.RemoveAll(n => n % 2 == 0);

        numbers.ForEach(item => Console.WriteLine(item));
    }
}

Find()메서드는 Predicate 형식의 델리게이트를 사용하여 리스트 내에 포함된 요소에 대하여 테스트를 수행한다.

매우 단순하다 즉, bool을 반환하는 사전에 정의된 델리게이트이기 때문에 해당 로직에 대한 참 거짓을 통과한 케이스만 반환한다.

TrueForAll()메서드는 각 요소를 개별적으로 테스트하되 모든 항목이 테스트를 통과한 경우에만 true를 반환한다.

RemoveAll()메서드는 각 요소를 개별적으로 테스트하되 테스트를 통과한 요소만 리스트에서 제거한다.

ForEach()메서드는 각 요소에 대하여 델리게이트로 지정한 동작을 수행하고 컴파일러는 람다 표현식을 메서드로 변환 후, 이 메서드를 참초하는 델리게이트를 생성한다.

멀티캐스트

모든 델리게이트는 기본적으로 멀티캐스트가 가능하다.

멀티캐스트 델리게이트는 한 번만 호출하면 델리게이트 객체에 추가된 모든 대상 함수가 호출된다.

하지만 이러한 구조는 두 가지 주의해야 할 부분이 있다.

먼저 예외에 안전하지 않다.

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

멀티캐스트 델리게이트의 내부 동작 방식은 대상 함수들을 연속적으로 호출하는 형태로 구현된다.

델리게이트는 어떤 예외도 잡지 않으며, 따라서 예외가 발생하면 델리게이트 호출은 중단된다.

반환값에서도 유의해야 한다.

선언시 void가 아닌 다른 타입으로 지정한 경우 반환값은 멀티캐스트 체인에서 마지막으로 호출된 함수의 반환값이 되며, 다른 반환값은 모두 무시된다.

이러한 문제를 해결하기 위해선 델리게이트에 포함된 호출 대상 콜백 함수를 직접 다뤄야 한다.

델리게이트는 추가된 메서드의 리스트를 가지므로, 이 리스트를 살펴서 직접 호출하는 방식으로 코드를 작성하는 것

정리

델리게이트는 런타임에 콜백을 구성하는 최고의 방법이다.

델리게이트를 사용하면 콜백을 사용해야 하는 클라이언트를 더욱 단순하게 구성할 수 있을 뿐만 아니라 런타임에 콜백 함수를 구성할 수 있다.

게다가 하나의 델리게이트에 여러 개의 콜백 함수를 추가할 수 있다.

따라서 콜백이 필요한 경우엔 델리게이트를 반드시 이용하자..!

유니티에선 좀 더 쉽게 사용하기 위해 UnityEvent를 사용할 수 있는데.. 인스펙터 단위에서 다루기 편하다 보니 사용이 편할 수 있지만, 인스펙터 단위이기 때문에 쉽게 깨질 수 있다.

가장 많이 사용하는 것은 아무래도 아직까진 Action인 것 같은데 캡슐화를 이뤄야 하기 때문에 event를 사용하는게 좋다고는 알고 있다.

또한,, 한가지 작업이 끝날 때 다른 클래스와 의존성을 줄이면서 해당 작업의 종료 시점까지 대기하는 콜백을 위해 asnyc/await를 사용할 수 있다.

이런 구조도 최근에 필요에 의해 만들게 되었는데 좋은 구조인지 아직 잘 모르겠다.

비동기의 경우 테스트가 힘들다고 알고 있어서.. 테스트 코드를 작성하기 위해선 또 다른 방법을 찾아봐야 할 것 같다.

댓글남기기