C# - 델리게이트 (Delegate)

2025. 9. 22. 21:02·C#

델리게이트 (Delegate)

특정 시그니처의 메서드를 가리키는 형식(type)

특정 시그니처(반환/매개변수)를 만족하는 메서드를 값처럼 담아 전달 / 저장/ 호출하게 해준다.

 

 

선언과 대입

using System;

namespace Delegate
{
    // 반환: void, 매개변수: 없음
    delegate void MyDel();

    // 반환: int, 매개변수: 없음
    delegate int MyDel2();

    class TempClass
    {
        public int Property { get; set; }
        public void PrintTemp()
        {
            Console.WriteLine("매개변수도 없고, 반환도 없는 메서드");
        }
    }

    class Program
    {
        static void Main()
        {
            MyDel dele = null;   // 참조형이므로 null 가능

            var obj = new TempClass();
            dele = obj.PrintTemp;                   // 메서드 참조 대입(괄호 X)
            dele = new MyDel(obj.PrintTemp);        // 동일 의미

            dele();                                 // 호출
        }
    }
}

 

메서드 주소를 기억하는 참조형이며 변수에 null 대입이 가능하다.

괄호를 붙이면 호출이 되어버리므로 대입시에는 괄호를 쓰지 않고 함수명만 기입한다.

 

 

인스턴스 / 정적 메서드 모두 가능

using System;

namespace Delegate
{
    delegate int MyDel(int a, int b); // int 반환, int 2개 인자

    class TestClass
    {
        public static int AddTwoNumbers(int f, int s) => f + s; // 정적
    }

    class Program
    {
        static void Main()
        {
            MyDel myMethod = TestClass.AddTwoNumbers; // 정적 메서드 참조
            Console.WriteLine(myMethod(5, 22));       // 27
        }
    }
}

 

using System;

namespace Delegate
{
    delegate void MyDel1();

    class TestClass
    {
        int health = 100;
        static int maxHealth = 500;

        public void PrintHealth() => Console.WriteLine(health);
        public static void PrintDefaultHealth() => Console.WriteLine(maxHealth);
    }

    class Program
    {
        static void Main()
        {
            var player = new TestClass();

            MyDel1 d1 = player.PrintHealth;      // 인스턴스 메서드
            MyDel1 d2 = TestClass.PrintDefaultHealth; // 정적 메서드

            d1();
            d2();
        }
    }
}

 

멀티캐스트 (Delegate Chain)

여러 메서드를 하나의 델리게이트 변수에 체인하여 순차 호출할 수 있다.

using System;

namespace Delegate
{
    delegate void StatChange(int level);

    class Player
    {
        int hp = 100, def = 10, atk = 30;

        public void ChangePlayerHP(int lv)  { hp  += lv * 10; Console.WriteLine($"변동된 플레이어 체력: {hp}"); }
        public void ChangePlayerAtk(int lv) { atk += lv * 2;  Console.WriteLine($"변동된 플레이어 공격력: {atk}"); }
        public void ChangePlayerDef(int lv) { def += lv * 2;  Console.WriteLine($"변동된 플레이어 방어력: {def}"); }
    }

    class UI
    {
        public static void PlayerLvlUp(int val) => Console.WriteLine($"레벨이 {val} 만큼 올랐다");
    }

    class Program
    {
        static void Main()
        {
            var plr = new Player();

            StatChange chain = UI.PlayerLvlUp;   // = 대입
            chain += plr.ChangePlayerHP;         // += 추가
            chain += plr.ChangePlayerAtk;
            chain += plr.ChangePlayerAtk;        // 중복 추가 가능
            chain += plr.ChangePlayerDef;

            chain(2);                            // 모두 호출

            chain -= plr.ChangePlayerAtk;        // 제거
            chain -= plr.ChangePlayerDef;
            chain(3);

            chain -= plr.ChangePlayerDef;        // 없는 항목 제거 시도: 예외 없음(안전)
            chain(1);
        }
    }
}

 

내부적으로 Invocation List에 (대상 인스턴스, 메서드) 쌍이 순서대로 저장된다.

-=는 존재하지 않는 항목을 빼도 예외가 발생하지 않도록 안전 처리되어 있다.

 

using System;

namespace Delegate
{
    delegate void StatChange(int level);

    class Player
    {
        public void ChangePlayerHP(int lv) { /* ... */ }
    }

    class Program
    {
        static void Main()
        {
            Player plr2 = null;

            StatChange chain = null;
            chain += plr2.ChangePlayerHP; // 여기서 NullReferenceException
            chain(2);
        }
    }
}

 

plr2를 null로 바꿨는데도 체인을 호출한 것만으로는 안 터지는 이유??

 

체인에 추가할 때 이미 그 시점의 인스턴스 참조를 델리게이트가 붙잡는다.

이후 지역변수 plr2를 null로 바꿔도 체인이 들고 있는 참조는 그대로 호출된다.

반대로 등록 시점에 null이라면 바로 예외가 난다.

 

이것은 클로져(캡쳐) 때문이 아니라, 델리게이트 자체가 인스턴스+메서드 쌍을 보관하기 때문이라고 한다.

 

 

Delegate 위험성

  • 강참조 보유 : 멀티캐스트 델리게이트 / 이벤트는 대상 인스턴스를 강참조 한다. 구독 해제를 잊으면 GC가 수거하지 못해 누수가 된다. "구독한 곳에서 반드시 해제" 원칙을 지켜야 한다.
  • 예외 전파 : 체인 중 한 핸들러가 예외를 던지면 그 지점 이후 호출은 중단된다. 필요시 GetInvocationList()로 분해해 개별 try/catch 처리하기도 한다.
  • 스레드 안전 : 멀티스레드에서 += / =- / Invoke의 경쟁 상태에 유의. 로컬 카피 후 호출 패턴이 안전하다.

 

 

익명함수(람다식)

람다는 익명 메서드를 간결하게 표현하는 문법이다. 델리게이트에 자연스럽게 대입된다.

Javascript의 화살표 함수랑 비슷하게 생겼는데 실제로 arrow function(화살표 함수)라고도 부른다.

using System;

namespace Delegate
{
    delegate int Calc(int a, int b);

    class Program
    {
        static int AddNumbers(int f, int s) => f + s;
        static int MultiplyNums(int f, int s) => f * s;

        static void Main()
        {
            Calc calculator;

            calculator = (f, s) => f + s;       // 단문
            calculator = (f, s) => { return f + s; }; // 복문
            calculator = AddNumbers;            // 기존 메서드 참조 대입

            // 여러 동작으로 교체
            calculator = (f, s) => f - s;
            calculator = (f, s) => f * s;
        }
    }
}

 

프로퍼티 get을 람다처럼 간결하게 표현할 수도 있다.

public int Size => width * height;

 

 

 

콜백

특정 작업이 끝난 뒤 실행할 메서드를 넘기는 것.

함수 안에서 함수를 넘겨줘 실행하는 개념이다.

using System;

namespace Delegate
{
    delegate void CallbackDel(string message);

    class Program
    {
        static void DoWork(CallbackDel callback)
        {
            Console.WriteLine("오래 걸리는 게임 기능(예: 로그인)...");
            System.Threading.Thread.Sleep(3000);
            callback("완료");
        }

        static void AfterWork(string msg)      => Console.WriteLine($"콜백 + {msg}");
        static void AnotherWork(string msg)    => Console.WriteLine($"콜백(다른기능) + {msg}");

        static void Main()
        {
            Console.WriteLine("시작");
            DoWork(AfterWork);
            Console.WriteLine("끝");
        }
    }
}

 

 

타이머 콜백

using System;
using System.Threading;

namespace Delegate
{
    class Program
    {
        static void CallbackMethod(object? state)
        {
            Console.WriteLine("3초 후 호출될 메서드");
        }

        static void Main()
        {
            Console.WriteLine("타이머 시작");
            using var timer = new Timer(CallbackMethod, null, 3000, Timeout.Infinite);
            Console.WriteLine("이 로그가 먼저 찍힙니다");
            Console.ReadLine();
        }
    }
}

 

 

제네릭 대리자 ( Action / Func )

Action : 반환, 파라미터 없음

using System;

namespace Delegate
{
    delegate void 옛날식(); // 오늘날엔 Action이 대체

    class Program
    {
        static void Main()
        {
            옛날식 d1 = () => Console.WriteLine("옛날 델리게이트");
            Action a1 = () => Console.WriteLine("Action 사용 권장");
            a1();
        }
    }
}

 

 

Func : 반환, 파라미터 있음.

using System;

namespace Delegate
{
    public delegate int CaclDel(int x, int y); // 옛날식(비교용)

    class Program
    {
        static void Main()
        {
            Action myAction = () => Console.WriteLine("이거 가능ㅋ");
            myAction();

            // 마지막 제네릭 인자는 '반환형'
            Func<int, int, int> func = (a, b) => a + b;
            Console.WriteLine(func(3, 4));               // 7

            Func<int, int, int, int, int, int> func2 = (a, b, c, d, e) => a + c + d + e;
            Console.WriteLine(func2(3, 4, 5, 6, 7));     // 3 + 5 + 6 + 7 = 21
        }
    }
}

 

 

Invoke

델리게이트나 이벤트에 등록된 메서드를 명시적으로 실행시키는 것.

단, 아무 등록도 하지 않으면 Null 예외가 발생하기 때문에 ?.Invoke() 형식으로 안전하게 사용하는 게 좋다.

using System;

namespace Delegate
{
    class Button
    {
        public Action onClick; // 외부에서 Invoke 가능 → 위험
        public void Click()
        {
            Console.WriteLine("클릭 됐음");
            onClick?.Invoke();
        }
    }

    class Player { public void PrintMyStat() => Console.WriteLine("플레이어 스탯"); }
    class Enemy  { public void PrintEnemyStat() => Console.WriteLine("적 스탯"); }

    class Program
    {
        static void Main()
        {
            var button = new Button();
            var player = new Player();
            var enemy  = new Enemy();

            button.onClick += player.PrintMyStat;
            button.Click();

            Console.WriteLine("--------------------");
            button.onClick += enemy.PrintEnemyStat;
            button.Click();

            // 외부에서 임의로 Invoke 가능 → 설계상 취약
            button.onClick.Invoke();
        }
    }
}

 

그렇다고 private로 막으면 구독 자체가 불가능한데...

class Button
{
    private Action onClick; // 외부가 += 자체를 못 함
    public void Click()
    {
        Console.WriteLine("클릭 됐음");
        onClick?.Invoke();
    }
}
// => 구독을 막아버려서 목적 달성 실패

 

여기서 나오는 게 바로 이벤트!

 

 

이벤트

델리게이트의 안전한 캡슐화

델리게이트 필드를 public으로 두면 외부가 Invoke를 임의로 호출할 수 있다. (의도치 않은 트리거)

외부가 =로 구독 목록을 통째로 덮어쓰기 할 수도 있다... 이걸 방지해야 한다.

 

event로 이것을 해결할 수 있다.

using System;

namespace Delegate
{
    class Button
    {
        public event Action onClick; // 외부는 += / -=만 가능, Invoke/대입 불가
        public void Click()
        {
            Console.WriteLine("클릭 됐음");
            onClick?.Invoke();       // 발생은 내부에서만
        }
    }

    class Player { public void PrintMyStat() => Console.WriteLine("플레이어 스탯"); }
    class Enemy  { public void PrintEnemyStat() => Console.WriteLine("적 스탯"); }

    class Program
    {
        static void Main()
        {
            var button = new Button();
            var player = new Player();
            var enemy  = new Enemy();

            button.onClick += player.PrintMyStat;
            button.Click();

            Console.WriteLine("--------------------");
            button.onClick += enemy.PrintEnemyStat;
            button.Click();

            // button.onClick.Invoke(); // 컴파일 에러: 외부에서 Invoke 금지
        }
    }
}

 

event는 대입(=)과 Invoke()를 막고 += / -= (추가, 제거) 만 허용한다.

내부에서 발생(raise)할 수 있으므로 UI위젯 등에서 의도하지 않은 외부 트리거를 차단한다.

 

 

Null 조건부(?.)

Action chain = null;
chain?.Invoke();     // 안전: chain이 null이면 아무 일도 하지 않음

 

이벤트 / 델리게이트 호출부는 Null 가드가 필수적이다.

안 그러면 프로그램이 터져버릴 위험이 있기 때문...

체인 내부의 각 항목이 예외를 던지면 그 시점에서 이후 항목 호출이 중단된다(전파).

필요하다면 각 구독자 호출을 try-catch로 감싸고 개별 로깅하기도 한다.

 

 

 

 

정리

델리게이트 (Delegate)

메서드에 대한 참조를 담는 형식. 정수형 변수에 숫자를 담듯이, 델리게이트 변수에는 메서드를 담을 수 있다.

콜백 구현, 알고리즘 교체, 이벤트 처리, 메서드 체이닝 등에 사용된다.

메서드 체인 기능을 통해 메서드를 등록, 삭제 할 수 있다.

 

이벤트 (Event)

델리게이트를 기반으로 만들어지며, 특정 상황이 발생했을 때 외부에 알림을 주는 안전 장치다.

외부에서 = 대입이나 Invoke 호출을 막아주는 것으로 캡슐화를 보장한다.

UI 버튼 클릭 이벤트, 네트워크 응답 완료, 다운로드 완료 등 상태 변화 → 알림 구조에서 사용된다.

'C#' 카테고리의 다른 글

C# - StringBuilder  (0) 2025.09.23
C# - 캡쳐(Capture), 클로져(Closure) - feat React.useState  (0) 2025.09.23
C# - 인터페이스 문제 풀이  (0) 2025.09.22
C# - 인터페이스 (Interface)  (0) 2025.09.21
C# - 제네릭 (Generic)  (0) 2025.09.21
'C#' 카테고리의 다른 글
  • C# - StringBuilder
  • C# - 캡쳐(Capture), 클로져(Closure) - feat React.useState
  • C# - 인터페이스 문제 풀이
  • C# - 인터페이스 (Interface)
ellug
ellug
Web / App / Game Dev
  • ellug
    dev.ellug
    ellug
  • 전체
    오늘
    어제
    • 분류 전체보기 (95)
      • Javascript (React, Next) (1)
      • C# (66)
      • Graphics (1)
      • Unity (25)
      • 프로그래머스 (0)
        • Lv.0 (0)
        • Lv.1 (0)
        • Lv.2 (0)
        • Lv.3 (0)
      • AI Agent (0)
      • StableDiffusion (0)
      • ETC (1)
      • 잡담 (0)
      • 유머 (0)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    자료구조
    C#
    stack
    Design Pattern
    class
    Queue
    console
    struct
    delegate
    네트워크
    firebase
    클래스
    최적화
    Network
    Abstract
    interface
    알고리즘
    게임
    Photon
    콘솔
    터미널
    list
    function
    inputsystem
    스택
    unity
    큐
    추상 클래스
    유니티
    useState
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
ellug
C# - 델리게이트 (Delegate)
상단으로

티스토리툴바