본문 바로가기
TIL

[Unity] 델리게이트 (Delegate)

by vvin39 2025. 5. 20.

내일배움캠프 30일차 TIL

📌 델리게이트란?

델리게이트(delegate)는 C#에서 메서드를 변수처럼 다룰 수 있게 해주는 메서드 참조 타입 (특정 시그니처(매개변수, 반환값)를 가진 메서드를 참조할 수 있는 타입) 이다.

델리게이트는 특정 메서드들을 나중에 호출하기 위해 저장하거나, 외부에서 전달받아 실행할 수 있게 해주는 기능이다.

 

델리게이트의 목적

목적  설명
메서드를 변수처럼 전달 메서드를 파라미터로 전달해 콜백 구현 가능
동적으로 실행할 로직 결정 상황에 따라 실행할 메서드를 유동적으로 바꿀 수 있음
느슨한 결합 호출하는 쪽과 실행하는 쪽의 의존도를 낮춤 (OCP 등 원칙 만족)
이벤트 시스템 구현 버튼 클릭 등 Unity 이벤트 연결에 필수

델리게이트의 주요 사용처

사용처  예시
콜백 함수 비동기 처리 후 onComplete() 호출
전략 패턴 구현 공격 방식, 이동 방식 등을 런타임에 변경
이벤트 시스템 Unity의 onClick, onDie, onScoreChanged
LINQ/람다 함수 Where(x => x > 0)에서 Func<T, bool> 델리게이트 사용
상태 전환 로직 상태에 따라 다른 행동을 연결해서 실행
의존성 주입(Delegate Injection) 호출할 로직을 외부에서 주입받음

 

기본 구조

// 1. 델리게이트 선언
public delegate void MyDelegate(string message);

// 2. 델리게이트에 연결할 함수 정의
void SayHello(string msg)
{
    Debug.Log("Hello: " + msg);
}

// 3. 델리게이트 인스턴스 생성 및 함수 등록
MyDelegate del = SayHello;

// 4. 호출
del("Unity!");

 

SayHello 함수 자체를 변수처럼 넘기고 나중에 실행할 수 있다.

 

C#의 델리게이트는 내부적으로 MulticastDelegate 클래스를 상속받고 있다. 즉, 여러 메서드를 체인으로 묶어 실행할 수 있다.

Delegate[] list = myDelegate.GetInvocationList();

이렇게 등록된 메서드들을 배열로 가져와 개별 실행하거나 제거할 수 있다. delegate는 참조 타입이고, 불변 객체(immutable) 이다. +=나 -=는 내부적으로 새 객체를 반환한다.

왜 사용하나요?

상황  델리게이트로 해결 가능
버튼 클릭 시 특정 함수 실행 이벤트 연결
타이머가 끝났을 때 특정 함수 실행 콜백 함수 전달
코드 흐름을 유연하게 제어 외부에서 행동 지정
이벤트 기반 구조 구현 옵저버 패턴

 

간단 예제

 

버튼 클릭 이벤트 (델리게이트 활용)

public delegate void ClickAction();

public class ButtonHandler : MonoBehaviour
{
    public ClickAction onClick;

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            onClick?.Invoke(); // 등록된 함수 실행
        }
    }
}

 

public class GameLogic : MonoBehaviour
{
    public ButtonHandler button;

    void Start()
    {
        button.onClick = MyClickEvent;
    }

    void MyClickEvent()
    {
        Debug.Log("버튼이 눌렸습니다!");
    }
}

여러 함수 등록도 가능 (Multicast Delegate)

MyDelegate del = SayHello;
del += SayBye;

del("Player"); // 두 함수 모두 실행됨

델리게이트는 +=, -=로 여러 함수 연결과 해제가 가능.

유니티에서의 활용 예

1. 공격이 끝났을 때 콜백 실행

public class Enemy : MonoBehaviour
{
    public Action onDeath;

    public void Die()
    {
        Debug.Log("죽었습니다");
        onDeath?.Invoke();
    }
}
public class GameManager : MonoBehaviour
{
    public Enemy enemy;

    void Start()
    {
        enemy.onDeath = OnEnemyDeath;
    }

    void OnEnemyDeath()
    {
        Debug.Log("적이 죽었으니 점수 +100");
    }
}

 

2. 커스텀 이벤트 시스템 구현

public static class GameEvent
{
    public static Action onGameStart;
    public static Action onGameOver;
}
// GameManager.cs
void Start()
{
    GameEvent.onGameStart?.Invoke();
}
// UIManager.cs
void OnEnable()
{
    GameEvent.onGameStart += ShowStartUI;
}

void OnDisable()
{
    GameEvent.onGameStart -= ShowStartUI;
}

 

델리게이트의 종류

1. 일반 델리게이트

public delegate void MyDelegate(string msg);

MyDelegate del = ShowMessage;
del("Hello");

2. Action, Func, Predicate (기본 델리게이트)

  • Action : 반환값 없음
  • Func : 반환값 있음
  • Predicate<T> : Func<T, bool>과 동일
Action<string> print = Debug.Log;
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = x => x % 2 == 0;

→ 이들은 델리게이트 선언 없이도 편하게 쓸 수 있다.

 

델리게이트 체인이란?

델리게이트에 +=로 여러 개의 함수를 연결하면, 등록한 순서대로 차례대로 실행된다.

public delegate void MyDelegate();

void A() { Debug.Log("A"); }
void B() { Debug.Log("B"); }
void C() { Debug.Log("C"); }

void Start()
{
    MyDelegate del = A;
    del += B;
    del += C;

    del(); // 실행 순서: A → B → C
}

 

반환값이 있는 경우 (Func<T>)

Func<int> del = A;
del += B;
del += C;

int result = del(); // C()의 반환값만 남음

Func<T>의 경우에도 모든 함수는 실행되지만, 마지막 함수의 반환값만 유효

 

예)

int A() { Debug.Log("A"); return 1; }
int B() { Debug.Log("B"); return 2; }
int C() { Debug.Log("C"); return 3; }

// del 실행 → A, B, C 호출됨 → result = 3

주의: 중복 등록도 됨.

del += A;
del += A;
del += A;

이러면 A()가 3번 실행. 의도하지 않은 중복 호출이 생길 수 있으니 주의.

순서 변경하고 싶으면?

델리게이트 체인은 직접 순서 변경이 불가능하고, -=로 제거한 후 다시 +=로 등록해야 한다.

del -= A;
del += A; // 가장 마지막으로 이동

 

체인 호출 순서 정리

연산  효과
del += A 체인 뒤에 추가됨
del -= A 체인에서 제거됨 (여러 번 등록돼 있으면 가장 마지막 것 1개만 제거)
Invoke() 등록된 순서대로 차례로 실행됨

 

Unity에서의 실용 예

1. 버튼 클릭 이벤트 처리

public Action onClick;

void Start()
{
    onClick += PlaySound;
    onClick += ShowPopup;
    onClick += SaveProgress;
}

public void OnButtonClick()
{
    onClick?.Invoke();
}

void PlaySound() => Debug.Log("버튼 사운드 재생");
void ShowPopup() => Debug.Log("팝업 표시");
void SaveProgress() => Debug.Log("진행 상황 저장");

 

2. 플레이어가 죽었을 때 실행할 일들

public static Action onPlayerDead;

void Start()
{
    onPlayerDead += ShowGameOverScreen;
    onPlayerDead += StopEnemySpawning;
    onPlayerDead += SaveHighScore;
}

public void PlayerDie()
{
    Debug.Log("플레이어 사망 처리");
    onPlayerDead?.Invoke();
}
  • 게임 오버 화면 표시
  • 적 생성 중단
  • 점수 저장

모두 사망 시 동시에 실행되어야 하므로 델리게이트 체인으로 관리하기 좋다.

 

3. 게임 시작 처리

public static Action onGameStart;

void Awake()
{
    onGameStart += LoadMap;
    onGameStart += SpawnPlayer;
    onGameStart += StartMusic;
}

public void StartGame()
{
    Debug.Log("게임 시작!");
    onGameStart?.Invoke();
}

모듈화된 초기화 로직을 여러 곳에서 나눠서 관리하고 싶을 때 유용하다.

 

4. 충돌 이벤트에서 여러 시스템 반응 처리

public Action onPlayerHit;

void Start()
{
    onPlayerHit += ShowDamageEffect;
    onPlayerHit += PlayHitSound;
    onPlayerHit += ReduceHP;
}

void OnCollisionEnter2D(Collision2D collision)
{
    if (collision.collider.CompareTag("Enemy"))
    {
        onPlayerHit?.Invoke();
    }
}
 
  • 이펙트 재생
  • 사운드 재생
  • HP 감소

다양한 시스템이 동시에 반응해야 할 때 델리게이트 체인이 깔끔하게 처리해준다.

 

델리게이트 체인이 특히 유용한 상황 정리

상황  이유
여러 시스템이 동시에 반응해야 할 때 호출 순서대로 메서드를 연결할 수 있음
특정 이벤트에 쉽게 구독/해제하고 싶을 때 +=, -=으로 유연하게 관리 가능
UI, 사운드, 이펙트 등 분리된 처리 하나의 델리게이트로 중앙에서 실행 가능
시스템 간 의존성을 줄이고 싶을 때 이벤트 기반으로 모듈화 가능 (결합도 ↓)

 

실전 사용 패턴

1. 콜백(callback) 함수

void LoadData(Action onComplete)
{
    // 데이터 로딩 중...
    onComplete?.Invoke();
}

2. 전략 패턴

public delegate void AttackStrategy();

AttackStrategy attack;

void SetAggressive() => attack = () => Debug.Log("강공격!");
void SetDefensive() => attack = () => Debug.Log("방어 모드!");

void DoAttack() => attack?.Invoke();

3. LINQ / 람다 함수

델리게이트는 C#의 LINQ 문법에서도 광범위하게 사용

List<int> nums = new List<int> { 1, 2, 3, 4 };
var evenNums = nums.Where(x => x % 2 == 0);

여기서  x => x % 2 == 0 은 Func<int, bool>  델리게이트

 

실전에서 자주 겪는 실수 & 주의점

실수  해결 방법
등록만 하고 해제 안 함 OnDisable/OnDestroy에서 -= 해제
NullReferenceException ?.Invoke() 또는 if (del != null) 체크
static 이벤트 해제 누락 반드시 수동으로 -=
체인에 중복 메서드 등록 GetInvocationList()로 확인 가능

 

Unity에서 잘 쓰이는 패턴 요약

패턴  사용 예시
Action 이벤트 public static event Action onGameOver;
콜백 전달 LoadScene("Map", OnLoaded);
델리게이트 체인 onDie += PlaySound; onDie += ShowEffect;
전략 교체 상태 기계(State Machine)처럼 공격 방식 교체

 

델리게이트 사용 시 주의할 점

1. null 체크 꼭 하기

델리게이트는 구독된 메서드가 없으면 null이다. Invoke() 하기 전에 null 여부를 확인하지 않으면 NullReferenceException이 발생한다.

// ❌ 예외 발생 가능
onEvent.Invoke();  

// ✅ 안전한 호출
onEvent?.Invoke();

 

2. +=, -= 로 구독 관리 필수

델리게이트는 구독한 메서드를 누적하다. += 만 쓰고 -=로 해제하지 않으면, 같은 함수가 여러 번 실행되거나 메모리 누수가 생기는 문제가 발생한다.

void OnEnable()
{
    GameEvent.onStart += MyMethod;
}

void OnDisable()
{
    GameEvent.onStart -= MyMethod; // 꼭 해제!
}

유니티에서는 OnEnable/OnDisable 또는 Start/OnDestroy에 짝지어 등록 & 해제하는 게 일반적

3. 델리게이트 안에 익명 함수 쓰면 해제 불가

람다나 익명 메서드는 같은 코드라도 참조가 달라서 -=로 해제되지 않는다.

// ❌ 이건 해제 안 됨!
GameEvent.onStart += () => Debug.Log("시작!");
GameEvent.onStart -= () => Debug.Log("시작!"); // 다른 참조

// ✅ 이름 있는 함수는 해제 가능
GameEvent.onStart += MyMethod;
GameEvent.onStart -= MyMethod;

 

4. 이벤트 대상이 파괴되면 예외 위험

델리게이트는 객체가 파괴되어도 참조를 유지한다.

특히 MonoBehaviour 객체가 Destroy() 되었는데 연결된 델리게이트를 호출하면 문제가 발생할 수 있다.

if (target != null)
    onEvent?.Invoke();  // 또는 조건문으로 안전하게 처리

 

5. 너무 많은 함수 구독 = 추적 어려움

델리게이트는 디버깅이 어렵고, 누가 어디서 어떤 함수를 구독했는지 추적이 힘들 수 있다. 디버깅을 위해선 구독자 수 추적이나 로깅, 또는 UnityEvent 사용도 고려하는 것이 좋음!

 

6. static 델리게이트는 특히 조심

static 델리게이트는 전역 상태처럼 동작하므로, 한 번 구독하면 앱이 꺼질 때까지 남아 있을 수 있다.

  • 불필요한 참조 유지
  • 중복 호출
  • 참조 해제 안 되는 메모리 누수

  꼭 -=로 해제하거나 Clear() 로 초기화!

 

7. 멀티캐스트(delegate chain) 결과값은 마지막 하나만 반환

Func<int> del = A;
del += B;

int result = del(); // B의 결과만 반환됨

 

여러 함수가 연결돼 있어도 Func<T>는 마지막 함수의 리턴값만 유효. Action은 리턴값이 없어서 이런 걱정은 없다.

 

왜 델리게이트 등록 해제가 중요할까?

예를 들어,

구독만 하고 해제하지 않은 경우

public class Enemy : MonoBehaviour
{
    void OnEnable()
    {
        GameManager.OnPlayerDeath += Die;
    }

    void OnDisable()
    {
        GameManager.OnPlayerDeath -= Die;
    }

    void Die()
    {
        Debug.Log("Enemy가 죽었습니다.");
    }
}

이 구조에서는 OnEnable()에서 구독하고, OnDisable() 또는 OnDestroy()에서 해제해주는 게 매우 중요하다.

 

해제하지 않으면 어떤 문제가 생길까?

문제  설명
메모리 누수 GameManager가 Enemy를 계속 참조하고 있어 GC가 수거하지 못함
NullReferenceException 이미 Destroy된 객체의 메서드를 호출하려고 해서 에러 발생
중복 호출 재구독 시 같은 메서드가 여러 번 등록되면 이벤트가 중복 실행됨
의도치 않은 동작 죽은 오브젝트가 살아있는 척 이벤트에 반응하는 등 버그 발생

 

언제 해제해야 할까?

OnEnable() → OnDisable() 또는 Start() → OnDestroy() 구조를 많이 사용한다.

 

 

Unity에서 안전한 구조

void OnEnable()
{
    Player.OnJump += PlayJumpSound;
}

void OnDisable()
{
    Player.OnJump -= PlayJumpSound;
}

 

또는, 

void Start()
{
    Player.OnJump += PlayJumpSound;
}

void OnDestroy()
{
    Player.OnJump -= PlayJumpSound;
}

 

예외: static 이벤트는 더 조심!

public static event Action OnGameOver;
  • 이 이벤트는 앱이 꺼질 때까지 살아있음.
  • 구독 객체가 Destroy되어도 여전히 참조됨.
  • 필수적으로 해제하지 않으면 메모리 문제 심각해짐.

 

⚙️ 델리게이트 vs 이벤트

항목  델리게이트  이벤트 (event)
호출 위치 어디서든 호출 가능 선언한 클래스 내부에서만 호출 가능
보안성 낮음 (외부에서 호출 가능) 높음 (외부에선 구독만 가능)
주 사용처 전략 패턴, 콜백 UI 이벤트, 게임 상태 이벤트 등

2025.04.16 - [TIL] - 델리게이트(Delegate)와 이벤트(Event)

 

델리게이트(Delegate)와 이벤트(Event)

내일배움캠프 8일차 TIL📌 델리게이트 & 이벤트 왜 두 개념을 같이 보는걸까?델리게이트는 이벤트의 기반이기 때문! 델리게이트는 메서드를 참조할 수 있는 데이터 타입이고, 이벤트는 이 델리

vvin39.tistory.com

 

🎯 마무리 요약

델리게이트는 단순히 메서드를 넘겨주는 기능을 넘어서, 전략 패턴, 이벤트 시스템, 비동기 처리, 람다 활용, 코드 모듈화에 있어서 핵심적인 역할을 한다. Unity에서는 Action, Func, event 키워드를 자연스럽게 사용하면서 구독 / 해제를 적절히 관리하면 안정적인 코드 흐름을 만들 수 있다.

 

 

 

 

최근댓글

최근글

skin by © 2024 ttuttak