
만약, C#에서 '메서드(기능)' 자체를 변수에 담아서 전달할 수 있다면 어떨까요?
마치 '심부름꾼'에게 오늘은 "A 가게에 가서 우유 사 와!"라고 시키고,
내일은 "B 세탁소 가서 옷 찾아와!"라고 시키는 것처럼,
수행할 임무(메서드) 자체를 동적으로 바꿔서 전달하는 개념이 대리자(Delegate)입니다.
대리자(Delegate)는 '메서드에 대한 참조(Reference)'를 저장하는 특별한 타입입니다.
C언어와 C++의 함수 포인터와 비슷하지만, 훨씬 안전하고 객체 지향적인 방식이죠.
핵심은 '임무 명세서(대리자 타입)'와 '실제 임무(메서드)'의 '서명(Signature)'이 완벽하게
일치해야 한다는 점입니다. 서명이란 메서드의 반환 타입과 매개변수 목록을 의미합니다.
이것이 바로 대리자가 '타입에 안전하다(Type-safe)'고 불리는 이유입니다.
자, 그럼 직접 심부름꾼을 코드로 고용해 봅시다.
delegate키워드를 사용하여 대리자 타입을 선언합니다.
정수 두 개를 받아 정수 하나를 반환하는 Calculate라는 명세서를 만들어 보겠습니다.
[코드]
// 1단계: 대리자 선언 (임무 명세서 정의)
// 반환 타입: int, 매개변수: (int, int)
// 이 서명과 일치하는 모든 메서드를 참조할 수 있는 'Calculate' 타입을 정의
public delegate int Calculate(int a, int b);
// 2단계: 대리자에 할당할 메서드 작성 (실제 임무 정의)
// Calculate 명세서와 서명이 똑같은 Add와 Subtract 메서드를 만듭니다.
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Subtract(int a, int b)
{
return a - b;
}
}
// 3단계: 대리자 인스턴스 생성 및 호출 (심부름꾼 고용 및 실행)
// 이제 Calculate 타입의 '심부름꾼'을 고용하고, 실제 임무를 맡긴 뒤 실행해 보겠습니다.
class Program
{
static void Main()
{
Calculator calculator = new Calculator();
// 1. 'calc'라는 심부름꾼(대리자 인스턴스)에게 'Add' 임무를 할당
Calculate calc = new Calculate(calculator.Add); // 정식 문법
// Calculate calc = calculator.Add; // 더 간결한 문법도 가능
// 2. 대리자 호출: calc를 호출하면 실제로는 Add 메서드가 실행됨
int result = calc(10, 5);
Console.WriteLine($"결과: {result}"); // 출력: 결과: 15
// 3. 이제 'calc' 심부름꾼에게 다른 임무를 맡겨보자!
calc = calculator.Subtract;
// 4. 다시 대리자 호출: 이제는 Subtract 메서드가 실행됨
result = calc(10, 5);
Console.WriteLine($"결과: {result}"); // 출력: 결과: 5
}
}
[실행 결과]
결과: 15
결과: 5
동일한 코드가, 대리자에 어떤 메서드가 할당되어 있느냐에 따라
전혀 다른 결과를 만들어냈습니다. 이것이 대리자의 핵심입니다.
대리자의 진정한 힘은 메서드의 인자로 다른 메서드를 전달할 때 드러납니다.
이를 통해 특정 조건이 만족했을 때 나중에 호출될 메서드,
즉 콜백(Callback) 메서드를 구현할 수 있습니다.
콜백(Callback)은 비동기 프로그래밍에서 핵심적인 역할을 합니다.
이제 계산 결과를 다양한 방식으로 '보고'하는 기능을 만들어 봅시다.
[코드]
// 보고 방식을 정의할 대리자
public delegate int Calculate(int a, int b);
public delegate void Report(string message);
public class Calculator
{
// 보고 방식을 메서드로 정의
public int Add(int a, int b)
{
return a + b;
}
public int Subtract(int a, int b)
{
return a - b;
}
public void ReportToConsole(string message)
{
Console.WriteLine($"[콘솔 보고] {message}");
}
public void ReportToFile(string message)
{
File.WriteAllText("보고서.txt", message);
Console.WriteLine($"[파일 보고] {message}");
}
// 계산을 수행하고, 그 결과를 '보고'하는 역할까지 하는 메서드
// 두 번째 인자로 '어떻게 보고할지(Report)'에 대한 '기능'을 전달받음!
public void Perform(int a, int b, Calculate calcMethod, Report reportMethod)
{
int result = calcMethod(a, b);
reportMethod($"계산 결과는 {result} 입니다.");
}
}
class Program
{
static void Main()
{
Calculator calculator = new Calculator();
// 1. 덧셈을 하고, 그 결과를 '콘솔'에 보고해줘!
calculator.Perform(20, 10, calculator.Add, calculator.ReportToConsole);
// 2. 뺄셈을 하고, 그 결과를 '파일'에 보고해줘!
calculator.Perform(20, 10, calculator.Subtract, calculator.ReportToFile);
}
}
[실행 결과]
[콘솔 보고] 계산 결과는 30 입니다.
[파일 보고] 계산 결과는 10 입니다.
[실행 결과: 보고서.txt]
계산 결과는 10 입니다.
Perform메서드는 자신이 어떤 계산을 하는지,
그 결과를 어떻게 보고해야 하는지 전혀 알지 못합니다.
그저 외부에서 주입된 '기능(대리자)'을 실행할 뿐입니다.
대리자를 사용하면 코드는 유연해지고 각 기능의 책임이 명확하게 분리됩니다.
대리자(Delegate)는 메서드를 값처럼 다룰 수 있게 해주는 C#의 강력한 기능입니다.
| 개념 | 설명 |
|---|---|
| 대리자(Delegate) | 메서드에 대한 참조를 담는 타입 |
| 타입 안전성 | 대리자에 정의된 서명과 일치하는 메서드만 할당 가능 |
| 핵심 용도 | 메서드를 다른 메서드의 인자로 전달 (콜백, 이벤트 핸들러 등) |