📌 C# 이벤트·델리게이트·멀티캐스트 델리게이트 완전 이해하기

이 문서는 C#에서 자주 쓰이는 이벤트(event) 와 델리게이트(delegate)의 동작 원리, 그리고 내부적으로 어떻게 콜백이 가능해지는지 정리한 기술 노트입니다.


#️⃣ 1. 이벤트 구독하면 왜 콜백이 될까?

이벤트는 “특정 순간에 발생하는 신호”이고, 구독자는 그 신호를 듣고 있다가 반응합니다.

button.OnClick += HandleClick;

이 구조가 좋은 이유:

느슨한 결합(Decoupling) 이벤트를 만드는 쪽은 “신호를 발행”할 뿐, 누가 듣는지 알 필요 없음.

확장성 구독자 추가·제거가 자유로움.

동시 반응 가능 여러 구독자가 하나의 이벤트에 동시에 반응 가능.


#️⃣ 2. 그럼 콜백은 어떻게 가능한가?

핵심은:

델리게이트(delegate)는 “함수 주소(Function Pointer)”를 저장할 수 있는 타입이다.

구독하면 내부적으로 이런 일이 일어남:

  1. 함수의 주소가 델리게이트 객체에 저장됨

  2. 이벤트가 발생하면

OnClick?.Invoke();

를 통해 저장된 함수들을 모두 호출함

즉 “Invoke()”는 구독한 함수 포인터 목록을 순회하며 하나씩 호출하는 동작이다.


#️⃣ 3. 이벤트가 호출되는 시점은 누가 정하나?

발행자(publisher) 코드가 정한다.

버튼 예시:

if (isClicked)
    OnClick?.Invoke();

이 순간이 바로 “이벤트 발생 시점”.

이벤트는 외부에서 호출할 수 없고, 발행자만 호출할 수 있게 되어 있다.


#️⃣ 4. 델리게이트 vs 이벤트 차이

구분 델리게이트 이벤트

외부 호출 가능? 가능함 (위험) 불가능 외부 할당 가능? 통째로 변경 가능 +=, -=만 가능 목적 함수 포인터 안전한 콜백 시스템

이벤트는 델리게이트를 안전하게 감싼 문법적 보호막이다.


#️⃣ 5. 일반 함수 호출과 Invoke 호출의 차이

✔ 일반 함수 호출

→ 컴파일 타임에 호출할 함수가 결정됨 → “주소가 정해진 곳으로 바로 점프”

✔ 델리게이트 Invoke

→ 런타임에 저장된 함수 포인터 리스트를 순회하며 호출 → 즉, 동적 호출 방식

요약하면:

일반 함수 호출 = 단일 정적 호출 델리게이트 Invoke = 동적 함수 목록 순회 호출


#️⃣ 6. 🔥 멀티캐스트 델리게이트 내부 구조

멀티캐스트 델리게이트는 기본적으로 아래 정보를 담는 객체다:


📌 MulticastDelegate 내부 필드(개념)

Delegate
 ├─ target              // 메서드를 호출할 객체 (this)
 ├─ method              // 실제 메서드 주소(Method Pointer)
 ├─ _invocationList     // 멀티캐스트일 경우 저장되는 델리게이트 배열
 └─ _invocationCount    // invocationList 크기

📌 단일 델리게이트 구조

[Delegate]
  target -> 객체 or null
  method -> 함수 주소
  _invocationList -> null

📌 멀티캐스트 구조 (A += B += C)

[Delegate]
  target           -> 첫 메서드의 대상 객체
  method           -> 첫 메서드 주소
  _invocationList  -> [A, B, C]  // 델리게이트 배열
  _invocationCount -> 3

📌 Invoke() 동작 방식

Invoke() 는 내부적으로 다음 순서를 수행한다:

  1. _invocationList 를 가져온다

  2. 저장된 델리게이트들을 순서대로 반복

  3. 각 델리게이트의 method + target 조합을 실행

foreach(delegate d in _invocationList)
{
    call d.method on d.target
}

즉, Invoke가 “마법”이 아니라 함수 포인터 배열을 돌며 하나씩 호출하는 메서드일 뿐이다.


#️⃣ 7. ⭐ 연산은 얼마나 줄어드는가? (성능 관점)

멀티캐스트 델리게이트는 연산을 줄이는 기술이 아니다. 오히려 약간 더 비싸다.

왜 연산이 줄지 않을까?

둘 다 해야 하는 실제 작업은 동일하다:

A 호출
B 호출
C 호출

즉 필수 호출 개수 자체는 변하지 않음 → 호출은 3번 그대로.

직접 호출

A();
B();
C();

멀티캐스트 델리게이트

handlers?.Invoke(); // 내부에서 A, B, C 각각 호출

→ 둘 다 최종적으로 함수는 똑같이 3번 실행됨.

그런데 Invoke가 조금 더 비싸다

Invoke 내부에서 이런 작업을 추가로 수행하기 때문:

  1. null 체크

  2. _invocationList 접근

  3. 리스트 길이 확인

  4. 함수 주소 + 대상 객체 잡기

  5. 루프 돌면서 개별 호출

즉:

직접 호출이 더 빠르고, Invoke는 약간의 루프 + 구조체 조회 비용이 더 든다.


#️⃣ 8. 성능 상관 있냐? 언제 문제 되냐?

대부분의 이벤트:

OnClick

OnDie

SceneLoaded

OnLevelUp

→ 프레임당 자주 호출되지 않음 → 성능 걱정할 필요 없음

주의해야 할 상황은:

매 프레임 수만 번 이상 콜백 발생

델리게이트 목록을 계속 만들고 다시 붙였다가 떼는 경우

초핫 루프(Physics, AI, Pathfinding 핵심 루프)에 이벤트 사용

이런 극단적인 경우만 고려하면 됨.


#️⃣ 9. 결론 요약

Invoke는 마법이 아니라 “함수 포인터 리스트 순차 호출”

일반 함수 호출보다 약간 느리지만 구조적 이점이 압도적으로 크다

이벤트는 “안전한 델리게이트 시스템”

성능 문제는 극단적으로 자주 호출할 때만 고려