멀티 스레딩 환경 또는 다른 백그라운드 작업에서 어떤 일을 시키고 끝났는지 확인해야 하는 경우가 있습니다.

일이 끝났니?

첫 번째 방법으로는 주기적으로 일을 다 끝냈는지 체크하는 방식이 있습니다.

주기가 짧을수록 체크하는 횟수가 많아지고, 그만큼 일이 종료됐는지 빨리 체크할 수 있지만 언제 종료될지 모르는 일을 체크하기 위해 반복문으로 끊임없이 체크해야 합니다. 이는 자원 낭비로도 이어질 수 있습니다.

반대로 주기를 길게 두고 체크한다면 끝나는것은 감지하겠지만 정확히 어느 시점에 끝났는지 감지하는 것은 어려울 것입니다.

소스 예제는 아래와 비슷한 상황일 것 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Program
{
    static bool isDone = false;

    private static void Main(string[] args)
    {
        DoSomething();
        while (!isDone) // 100ms 주기로 체크
		    {
			      Console.WriteLine("끝났니?");
			      Thread.Sleep(100);
		    }

        Console.WriteLine("작업 끝남!");
    }

    private static async Task DoSomething() // 예시, 1초 후에 끝나는지는 모름
    {
        await Task.Delay(1000);
        isDone = true;
    }
}

polling

일이 끝나면..

일이 끝나면 해야할 일들을 미리 알려주는 겁니다. 따라서 위 방식처럼 일이 끝났는지 체크하기 위한 로직 자체가 필요 없다는 뜻입니다.

일을 하는 주체는 끝나자 마자 미리 전달 받은 일을 진행함으로서 체크 주기마다 달라졌던 딜레이도 없을 것입니다!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Program
{
    // static bool isDone = false; 플래그 불필요!
    static Action onSomething;

    private static void Main(string[] args)
    {
        DoSomething();

        onSomething += () => // 1. 일이 끝난 후 해야할 일을 정의
        {
            Console.WriteLine("작업 끝남!");
        };

        // 단순 프로그램 종료 방지
        Thread.Sleep(2000);
    }

    private static async Task DoSomething()
    {
        await Task.Delay(1000);
        onSomething(); // 2. 일이 끝나면 호출됨
    }
}

일이 끝난 후 해야할 일을 함수로 정의했고, 이 함수를 백그라운드에서 돌아가는 DoSomething() 메서드 끝에 실행해주고 있습니다.

여기서 onSomething 이 바로 콜백함수 되겠습니다. 😄

💡 OnClick() OnKeyDown() OnClose() 등 콜백함수는 보통 On으로 시작합니다.

🚀 Delegate

C# 에서는 C/C++ 처럼 어떤 함수를 변수처럼 보관해 주는 함수 포인터 개념이 바로 Delegate 입니다.

C 에서는 구조체에 함수 포인터를 넣음으로서 오늘날의 Class 개념(멤버 변수 및 메서드)을 구현했고, 위 예시처럼 콜백에서도 사용 되었었습니다.

Delegate 는 이처럼 함수 포인터처럼 함수의 내용을 변수처럼 저장하고 있다가 그 변수를 참조하여 함수를 호출도 하며, 컨테이너처럼 작동하여 여러 함수를 가리킬 수도 있습니다.

💡 ActionFunc 와 같은 자료형은 모두 Delegate를 이용하여 만든 템플릿입니다! ActionReturn 타입이 없는 함수형, FuncReturn 타입이 있는 함수형으로 구분 됩니다. Delegate를 직접 만들어서 사용해도 되지만 위 두 자료형을 사용하시는 것이 편리합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Program
{
    static bool isDone = false;
    static Action onSomething; // 사실은 delegate void Action() 과 같은 타입!

    private static void Main(string[] args)
    {
        DoSomething();

        onSomething += () =>
        {
            Console.WriteLine("작업이 끝나면 해야할 일 1");
        };

        onSomething += () =>
        {
            Console.WriteLine("작업이 끝나면 해야할 일 2");
        };

        onSomething += () =>
        {
            Console.WriteLine("작업이 끝나면 해야할 일 3");
        };

        Thread.Sleep(2000);
    }

    private static async Task DoSomething()
    {
        await Task.Delay(1000);
        onSomething?.Invoke();
    }
}

💡 함수 포인터에 아무것도 할당하지 않고(is null) 그 포인터를 참조하여 함수 호출을 할 경우 널 포인트 오류가 나는 것은 Delegate 도 똑같습니다! 따라서 onSomething?.Invoke(); 와 같은 형태로 사용하시는 것을 권장 드립니다.

callback

예시 상황

  1. 캐릭터가 어느 오브젝트에 닿았을 때를 감지하여 어떤 행동을 해야한다.
    • 유니티에서는 OnCollisionEnter() OnTriggerEnter() 등이 있습니다.
  2. 사용자가 어떤 설정을 변경했을 때 어떤 행동을 해야한다.
    • OnChangeVolue() 과 같이 만들면 되겠죠?
  3. 어떤 작업을 백그라운드(스레드풀과 같은) 처리를 했는데, 이 때 나온 결과로 무엇을 해야한다.
    • Action 이 아닌 Func 로 만들어서 호출해주면 되겠죠?

위와 같이 콜백을 등록하여 호출하면 매우 편리하며 종속 관계도 많이 줄일 수 있어서 좋습니다! 😎