C# 대리자와 이벤트

김민구·2025년 5월 27일
0

C#

목록 보기
23/31

1. 대리자(Delegate)란 무엇인가?

대리자는 메서드를 대신 호출해주는 역할을 합니다. 쉽게 말해, 메서드를 참조하는(가리키는) 형식이라고 생각할 수 있습니다. 사람으로 비유하자면, 사장님이 비서에게 업무 지시를 하면 비서가 해당 업무를 담당하는 직원에게 전달하는 것과 유사합니다. 사장님은 비서만 알고 있으면 실제 업무를 누가 처리하는지는 몰라도 되는 것이죠.

C#에서 대리자를 선언할 때는 delegate 키워드를 사용하며, 대리자가 참조할 메서드의 반환 형식과 매개변수 목록을 지정해야 합니다.

delegate int MyDelegate(int a, int b); // 반환 형식 int, 매개변수 (int, int)인 메서드를 참조할 대리자

이렇게 선언된 MyDelegateint를 반환하고 int 타입 매개변수 두 개를 받는 모든 메서드를 참조할 수 있습니다.

대리자를 사용하려면 대리자 타입의 인스턴스를 생성해야 합니다. 인스턴스를 만들 때는 new 연산자가 필요합니다.

// MyDelegate 타입의 대리자 인스턴스 Callback 생성
MyDelegate Callback;
// Callback 인스턴스가 Plus 메서드를 참조하도록 할당
Callback = new MyDelegate(Plus); // 또는 Callback = Plus;
// Callback 인스턴스 호출 -> Plus 메서드가 실행됨
Console.WriteLine(Callback(3, 4)); // 7 출력

위 예시처럼, Callback 대리자 인스턴스는 Plus라는 메서드를 참조하고 있습니다. Callback을 호출하면 실제 Plus 메서드가 실행되는 것입니다. 대리자 인스턴스는 인스턴스 메서드뿐만 아니라 정적 메서드도 참조할 수 있습니다.

2. 대리자는 왜, 언제 사용하나요?

대리자를 사용하는 가장 큰 이유는 메서드를 매개변수로 전달하거나, 실행할 메서드를 나중에 결정할 수 있게 해주어 코드의 유연성을 높이기 위함입니다.

예를 들어, 배열을 정렬하는 BubbleSort 메서드를 만든다고 가정해 봅시다. 이 메서드는 어떤 기준으로 정렬할지 (오름차순인지 내림차순인지) 외부에서 지정받아야 합니다. 이때 대리자를 사용하면 정렬 기준을 결정하는 비교 로직을 담은 메서드를 대리자로 감싸 BubbleSort 메서드의 매개변수로 전달할 수 있습니다.

// 두 int를 비교하는 대리자 선언
delegate int Compare(int a, int b);

// 비교 로직을 매개변수로 받는 BubbleSort 메서드
static void BubbleSort(int[] DataSet, Compare Comparer)
{
    // ... 버블 정렬 로직 ...
    if (Comparer(DataSet[j], DataSet[j+1]) > 0) // Comparer 대리자 호출하여 비교
    {
        // ... 교환 로직 ...
    }
    // ...
}

// 오름차순 비교 메서드
static int AscendCompare(int a, int b) { /* 오름차순 비교 로직 */ return a > b ? 1 : (a == b ? 0 : -1); }
// 내림차순 비교 메서드
static int DescendCompare(int a, int b) { /* 내림차순 비교 로직 */ return a < b ? 1 : (a == b ? 0 : -1); }

// Main 메서드에서 사용
int[] array = { 3, 7, 4, 2, 10 };
// 오름차순 정렬: AscendCompare 메서드를 참조하는 대리자 전달
BubbleSort(array, new Compare(AscendCompare));

int[] array2 = { 7, 2, 8, 10, 11 };
// 내림차순 정렬: DescendCompare 메서드를 참조하는 대리자 전달
BubbleSort(array2, new Compare(DescendCompare));

이처럼 대리자를 이용하면 BubbleSort 메서드 자체는 변경하지 않고, 어떤 대리자를 전달하느냐에 따라 오름차순 또는 내림차순으로 정렬하는 유연한 코드를 작성할 수 있습니다.

3. 일반화 대리자 (Generic Delegate)

위의 Compare 대리자는 int 타입만 비교할 수 있습니다. 만약 double이나 string 등 다른 타입의 배열도 정렬하고 싶다면 어떻게 할까요? 이때 일반화 대리자를 사용합니다. 타입 매개변수 <T>를 사용하여 모든 타입을 다룰 수 있는 대리자를 선언할 수 있습니다.

// 일반화 대리자 선언: 어떤 타입 T의 두 값을 비교
delegate int Compare<T>(T a, T b);

// 일반화 메서드: 어떤 타입 T의 배열을 정렬
static void BubbleSort<T>(T[] DataSet, Compare<T> Comparer)
{
    // ... 정렬 로직 ...
    if (Comparer(DataSet[j], DataSet[j+1]) > 0) // T 타입 값 비교
    {
        // ... 교환 로직 ...
    }
    // ...
}

이제 BubbleSort 메서드는 int 배열, string 배열 등 다양한 타입의 배열을 정렬할 수 있으며, 비교 로직은 Compare<T> 일반화 대리자를 통해 전달받습니다. 일반적으로 일반화 대리자와 함께 사용되는 비교 메서드는 IComparable<T> 인터페이스를 구현하는 타입의 CompareTo 메서드를 활용합니다.

4. 대리자 체인 (Delegate Chain)

하나의 대리자 인스턴스는 여러 개의 메서드를 동시에 참조할 수 있으며, 이를 대리자 체인이라고 합니다. 대리자 체인에 메서드를 추가할 때는 += 연산자를 사용하고, 제거할 때는 -= 연산자를 사용합니다. 대리자 체인 인스턴스를 호출하면 체인에 연결된 모든 메서드가 순서대로 실행됩니다.

// 어떤 장소에 화재가 발생했음을 알리는 대리자
delegate void ThereIsAFire( string location );

// 화재 관련 처리 메서드
void Call119(string location) { Console.WriteLine("소방서죠? 불났어요! 주소는 {0}!", location); }
void ShotOut(string location) { Console.WriteLine("피하세요! {0}에 불이 났어요!", location); }
void Escape(string location) { Console.WriteLine("{0}에서 나갑시다!", location); }

// 대리자 체인 생성 및 메서드 연결
ThereIsAFire Fire = new ThereIsAFire(Call119); // 첫 번째 메서드 연결
Fire += new ThereIsAFire(ShotOut); // 두 번째 메서드 추가
Fire += new ThereIsAFire(Escape); // 세 번째 메서드 추가

// 대리자 체인 호출
Fire("우리 집"); // Call119, ShotOut, Escape 메서드가 순서대로 실행됨

+=-= 연산자 외에도 Delegate.Combine() 메서드로 대리자를 연결하고 Delegate.Remove() 메서드로 제거할 수도 있습니다. 대리자 체인은 특히 뒤에서 배울 이벤트를 처리할 때 많이 활용됩니다.

5. 익명 메서드 (Anonymous Method)

익명 메서드는 이름이 없는 메서드입니다. 대리자 인스턴스에 할당할 메서드가 간단하고 코드 내에서만 일시적으로 사용될 경우, 별도의 메서드를 정의하지 않고 delegate 키워드를 사용하여 코드 블록 형태로 직접 구현하여 할당할 수 있습니다.

delegate int Calculate(int a, int b);

// 익명 메서드를 사용하여 Calculate 대리자 인스턴스에 할당
Calculate Calc = delegate (int a, int b)
{
    return a + b; // 이름 없이 메서드 본체만 구현
};

// Calc 대리자 호출 (익명 메서드 실행)
Console.WriteLine($"3 + 4 = {0}", Calc(3, 4));

익명 메서드는 대리자를 매개변수로 받는 메서드를 호출할 때 코드를 간결하게 만드는 데 유용합니다. 예를 들어 BubbleSort 메서드 호출 시 비교 로직을 익명 메서드로 바로 구현하여 전달할 수 있습니다.

// BubbleSort 호출 시 익명 메서드로 비교 로직 전달
BubbleSort(array, delegate(int a, int b) // 익명 메서드 시작
{
    // 오름차순 비교 로직
    if (a > b) return 1;
    else if (a == b) return 0;
    else return -1;
}); // 익명 메서드 끝

6. 이벤트 (Event): 객체에 일어난 사건 알리기

이벤트는 객체에서 특정 상황이 발생했음을 외부(다른 객체)에 알리는 메커니즘입니다. 이벤트 기반 프로그래밍의 핵심 요소이며, 대리자를 기반으로 구현됩니다. 이벤트는 발행/구독(Publisher/Subscriber) 모델에서 발행자(Publisher) 역할을 합니다.

이벤트를 사용하려면 다음 단계를 따릅니다:
1. 이벤트 핸들러 대리자 선언: 이벤트 발생 시 호출될 메서드(이벤트 핸들러)의 시그니처를 정의하는 대리자를 선언합니다. 일반적으로 void 반환 형식에 이벤트 정보 전달을 위한 매개변수를 가집니다.

```csharp
delegate void EventHandler(string message);
```
  1. 클래스 내에 이벤트 선언: 이벤트 발행자 역할을 할 클래스 안에 event 키워드를 사용하여 이벤트 인스턴스를 선언합니다. 이때 앞서 선언한 대리자 타입을 사용합니다.
    class MyNotifier
    {
        public event EventHandler SomethingHappened; // event 키워드와 대리자 타입으로 이벤트 선언
        // ...
    }
  2. 이벤트 핸들러(메서드) 작성: 이벤트 발생 시 실제로 실행될 메서드를 작성합니다. 이 메서드는 1단계에서 선언한 대리자의 시그니처와 일치해야 합니다.
    class MainApp
    {
        static public void MyHandler(string message) // EventHandler 대리자 형식과 일치
        {
            Console.WriteLine(message);
        }
        // ...
    }
  3. 이벤트 핸들러 등록: 이벤트에 반응할 객체(구독자 Subscriber)는 이벤트 발행자 객체의 이벤트 인스턴스에 자신의 이벤트 핸들러 메서드를 += 연산자를 사용하여 등록합니다. 여러 개의 핸들러를 등록하면 대리자 체인처럼 관리됩니다.
    MyNotifier notifier = new MyNotifier();
    notifier.SomethingHappened += new EventHandler(MyHandler); // 이벤트 핸들러 등록
    // 또는 notifier.SomethingHappened += MyHandler; (간소화)
  4. 이벤트 발생 (Raising Event): 이벤트 발행자 클래스 내부에서 특정 조건이 충족되었을 때 이벤트 인스턴스를 호출하여 등록된 이벤트 핸들러들을 실행시킵니다.
    class MyNotifier
    {
        public event EventHandler SomethingHappened;
        public void DoSomething(int number)
        {
            // ...
            if (/* 특정 조건 만족 */)
            {
                 SomethingHappened("이벤트가 발생했습니다!"); // 이벤트 발생! 등록된 핸들러가 호출됨
            }
            // ...
        }
    }
    // Main에서 DoSomething 호출
    notifier.DoSomething(30); // 만약 30일 때 이벤트 발생 조건 만족하면 핸들러 실행

event 키워드를 사용하여 이벤트를 선언하면 몇 가지 장점이 있습니다. 가장 중요한 것은 클래스 외부에서는 이벤트 인스턴스를 직접 호출(실행)할 수 없게 되어 이벤트 발행자는 오직 자신만이 이벤트를 발생시킬 수 있게 됩니다. 또한, 이벤트에 등록된 핸들러가 하나도 없더라도 NullReferenceException이 발생하지 않도록 안전하게 관리됩니다.

profile
C#, Unity

0개의 댓글