C# Delegate 정리(Unity 예제)

식혜드식혜·2025년 4월 17일

C# 문법 정리

목록 보기
2/8

람다식을 제대로 이해하려면, 먼저 delegate(델리게이트) 개념을 알아야 합니다.
이 글은 C#의 delegate개념부터 Action, Func, Predicate, event 키워드에 대해 설명하는 글입니다.

Delegate란?

  • Delegate는 메서드를 변수처럼 담을 수 있게 해주는 타입이다.
    쉽게 말해, 메서드를 데이터처럼 다루게 해주는 기능이다.

정의하는 법

public delegate void MyDelegate(int number);
  • delegate는 마치 메서드 정의처럼 생겼고, 그걸 통해 이 delegate에는 어떤 형태(시그니처)의 메서드만 들어올 수 있는지를 정의해주는 것이다.
  • 이 MyDelegate 타입에는 매개변수가 int 하나고, 반환값이 void인 메서드만 담을 수 있다. 즉, MyDelegate는 일종의 메서드 틀(형식) 이라고 생각하면 된다.

<사용하는 법>

public delegate void MyDelegate();

void PrintA() => Debug.Log("A"); 
void PrintB() => Debug.Log("B");

void Start()
{
    MyDelegate myFunc;

    myFunc = PrintA; // 마치 값을 대입하듯 메서드를 담는다
    myFunc();        // A 출력

    myFunc = PrintB; // 다른 메서드로 교체 가능
    myFunc();        // B 출력
}

delegate 변수는 해당 delegate와 시그니처가 같은 메서드들을 값처럼 대입하고 바꿀 수 있다.
비유하자면 delegate는 메서드를 넣을 수 있는 틀 같은 것이다. 틀에 맞는 모양의 메서드만 들어갈 수 있다.

Delegate를 왜 사용할까?

  • 한 코드가 다른 코드에 너무 의존하지 않게 만든다.
    Deleate를 사용하지 않은 예시
public class GameManager : MonoBehaviour
{
    public UIManager uiManager;
    public SoundManager soundManager;

    public void PlayerDied()
    {
        uiManager.ShowGameOver();       // 직접 호출
        soundManager.PlayDeathSound();  // 직접 호출
    }
}
  • 해당 코드는 GameManager가 UIManager와 SoundManager를 직접 알고 있어야 하기 때문에, 결합도가 높은 상태이다. UIManager나 SoundManager의 내부 구현이 바뀌면 GameManager도 고쳐야 하는 문제가 발생한다.
  • 또한 테스트나 재사용이 어렵고, 수정이 많을 수록 코드가 복잡해진다.

Delegate를 사용한 예시

public class GameManager : MonoBehaviour
{
    public delegate void PlayerDeathHandler();
    public event PlayerDeathHandler OnPlayerDied;

    public void PlayerDied()
    {
        OnPlayerDied?.Invoke();  // 외부에 알림만 함
    }
}
public class UIManager : MonoBehaviour
{
    public GameManager gameManager;

    void Start()
    {
        gameManager.OnPlayerDied += ShowGameOver; // 이벤트에 콜백 등록
    }

    void ShowGameOver()
    {
        Debug.Log("Game Over UI 표시");
        // 실제 UI 처리
    }
}

public class SoundManager : MonoBehaviour
{
    public GameManager gameManager;

    void Start()
    {
        gameManager.OnPlayerDied += PlayDeathSound;
    }

    void PlayDeathSound()
    {
        Debug.Log("죽음 사운드 재생");
        // 실제 사운드 재생
    }
}
  • 여러 시스템을 하나의 이벤트로 연결 가능하다 (Delegate는 여러 메서드를 한 번에 연결하고 한 번에 실행할 수 있는 구조다.)
  • 콜백 패턴 구현 가능 (비동기 작업 완료 후 처리 등)
  • GameManager가 모든 것을 직접 알고 처리하지 않고,
    "플레이어가 죽었을 때" 일어날 일을 외부에서 등록해서 분리시켜주는 거다.

Delegate 선언 및 사용법

public delegate void OnClickAction;

public class ButtonExample : MonoBehaviour
{
    public OnClickAction onClick;

    void Start()
    {
        onClick = PrintMessage; // 메서드 대입
        onClick();              // 실행
    }

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

익명 메서드 (Anonymous Method)

익명 메서드는 이름 없이 정의된 일회성 함수이다.
주로 한 번만 사용할 간단한 작업을 정의할 때 유용하다.

using UnityEngine;

public class Test : MonoBehaviour
{
    delegate void MyDelegate();

    void Start()
    {
        MyDelegate onClick = delegate { Debug.Log("익명 메서드 실행!"); };
        onClick();
    }
}
  • 너무 복잡한 로직을 익명 메서드 안에 넣으면 가독성 저하가 생긴다.
  • 람다식을 사용하면 더 간결하게 같은 로직을 작성할 수 있다.
    예를 들어, 위의 예제를 람다식으로 바꾸면 다음과 같이 작성할 수 있다.
using UnityEngine;

public class Test : MonoBehaviour
{
    // 델리게이트 타입 선언
    delegate void MyDelegate();

    void Start()
    {
        // 람다식으로 델리게이트 할당
        MyDelegate onClick = () => { Debug.Log("람다식 실행!"); };

        // 호출
        onClick();
    }
}

이렇게 하면 코드가 더 짧고 읽기 쉬워진다. 람다는 간단한 함수 표현식을 더욱 직관적으로 작성할 수 있도록 돕는 문법적 요소이다.

Action, Func, Predicate

얘네들은 델리게이트의 특정 형태(제네릭 버전)이다.

  • Action, Func, Predicate는 자주 쓰이는 형태를 미리 제네릭으로 만들어둔 대표 delegate 타입이라고 보면 된다.

Action

Action은 반환값이 없는(void) 메서드를 담을 수 있는 제네릭 delegate 타입이다.
즉, 어떤 동작만 수행하고, 결과를 돌려주지 않아도 되는 경우에 주로 사용된다.
예시

Action sayHello = () => Debug.Log("Hello!");
sayHello();

Action<int> printScore = score => Debug.Log($"Score: {score}");
printScore(100); // 출력: Score: 100

Func

Func는 반환값이 있는 delegate 타입이다.
가장 마지막 제네릭 타입이 반환형이고, 그 앞에 오는 타입들은 매개변수 타입이다.
예시

Func<int, int> square = x => x * x;
Debug.Log(square(4)); // 출력: 16

Func<string, int> getLength = str => str.Length;
Debug.Log(getLength("Unity")); // 출력: 5

Predicate

Predicate는 반환형이 bool이고, 매개변수가 1개인 delegate 타입이다.
즉, 조건 판별용 함수를 담을 때 사용한다.
예시

Predicate<int> isEven = x => x % 2 == 0;
Debug.Log(isEven(10)); // 출력: True
구분ActionFuncPredicate일반 delegate
리턴 타입void (아무것도 반환 안 함)반환값 있음 (제네릭으로 지정)bool만 반환직접 지정 가능
매개변수0개 ~ 여러 개0개 ~ 여러 개정확히 1개원하는 대로 정의 가능
제네릭 기반OOOX 직접 선언해야 함
예시Action<int>Func<int, string>Predicate<string>public delegate void MyDel(int x);
사용 편의성O 자주 씀O 자주 씀O 조건 검사할 때 유용처음부터 직접 설계 가능

event 한정자

event는 delegate 앞에 붙여 외부에서 직접 이벤트를 호출하지 못하게 막고,
해당 이벤트가 발생하는 시점을 해당 클래스로 제한하는 역할을 한다.

  • 클래스 외부에서 직접 이벤트를 변경하거나 호출할 수 없게 만든다.
    이벤트를 외부에서 +=(구독) 또는 -=(구독 해지) 방식으로만 조작할 수 있으며, 직접 호출할 수는 없다.
public class HealthSystem
{
    // 이벤트 선언
    public event Action OnDeath;

    public void Die()
    {
        Debug.Log("죽었습니다.");
        OnDeath?.Invoke(); // 이벤트를 내부에서만 발생시킴. 외부에서는 이벤트를 실행할 수 없음.
    }
}

event의 핵심은 외부에서 이벤트를 구독하고 해지할 수 있지만, 이벤트를 발동시키는 책임은 해당 클래스로 제한된다는 점이다. 이로 인해 외부에서 OnDeath = null;처럼 이벤트를 null로 변경하거나 직접 Invoke를 호출하는 일이 불가능하다.

Multicase Delegate(멀티캐스트)

여러 메서드를 덧붙여 하나의 델리게이트로 묶는 방법을 제공한다.

  • += 연산자를 통해 여러 개의 메서드를 체인처럼 등록할 수 있으며,
    델리게이트가 호출되면 등록된 메서드들이 차례대로 실행된다.
public delegate void Notify();

Notify notify = () => Debug.Log("A");
notify += () => Debug.Log("B");

notify(); // 출력: A\nB
  • notify()를 호출하면 A와 B가 차례대로 출력된다.
    즉, 델리게이트에 등록된 여러 메서드들이 순차적으로 실행된다.
  • notify -= () => Debug.Log("A");는 동작하지 않는다.
    이유는 notify에 등록된 람다 표현식은 새로운 참조로 생성되기 때문에, 동일한 코드라고 하더라도 메모리 상에서 다른 객체로 취급된다.

이처럼 델리게이트는 여러 메서드를 등록하고 호출할 수 있기 때문에 멀티캐스트 델리게이트라고 불린다.

-> 메서드를 따로 정의해서 추가/제거하는 것이 안전하다.

void PrintA() => Debug.Log("A");

Notify notify = PrintA;  // 메서드를 델리게이트에 직접 할당
notify += PrintA;        // 메서드를 덧붙임
notify -= PrintA;        // 메서드를 안전하게 제거

Unity에서 Delegate를 자주 쓰는 곳

  1. UI 버튼 클릭 처리
  • UI 버튼의 클릭 이벤트를 Delegate로 처리하여 다양한 행동을 유연하게 연결할 수 있다.
  1. 애니메이션/효과 완료 후 동작 처리
  • 애니메이션이나 효과가 끝났을 때 특정 작업을 수행하는 데 Delegate를 활용한다.
  1. 게임 상태 변화 알림 (HP 0, 클리어 등)
  • 게임 상태(예: HP 0, 클리어 등)가 변화할 때 필요한 알림을 Delegate로 처리하여 게임의 흐름을 관리한다.
  1. 씬 전환/로드 완료 콜백
  • 씬 전환이나 로드 완료 후 실행할 작업을 Delegate를 이용해 설정할 수 있다.

🔚결론

  • Delegate는 메서드를 변수처럼 다루는 방법이다.
    이를 통해 메서드를 동적으로 연결하고 실행할 수 있다.

  • Delegate는 다양한 상황에서 효율적이고 유연한 이벤트 처리를 가능하게 합니다. 특히 Action, Func, Predicate는 자주 사용되는 Delegate 유형이다.

  • Event는 Delegate에 대한 안전장치로, 외부에서 Delegate를 잘못 수정하거나 호출하는 것을 방지해준다.

  • Unity에서는 콜백 구조나 비동기 처리에 필수적인 개념으로, Delegate를 적극적으로 활용하여 유연한 코드 구조를 만들 수 있다.

  • del()로 직접 호출할 수도 있지만, del.Invoke()로 실행하는 것을 권장한다. del?.Invoke()로 호출하면 null일 때, 안전하게 null 체크를 할 수 있다.
  • 또한 del()로 호출하면 이것이 그냥 함수인지, delegate인지 헷갈릴 수 있다. 반면, Invoke로 호출하면 알기가 쉽다.
profile
안녕하세요! 유니티 공부 중인 고3 학생입니다

0개의 댓글