델리게이트 (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 |