1. 대리자(Delegate)의 본질
대리자는 사용자 정의 참조 형태로, delegate 키워드로 표기하며 반환값과 매개변수를 갖습니다. 실무에서는 동일한 시그니처를 가진 메서드들의 참조를 보관하고, 호출 시점에 일괄 실행하는 메커니즘으로 활용됩니다.
1.1 선언 규칙
- 클래스와 동일한 수준에서 선언하며, 메서드 내부에서는 선언 불가
- 접근 제한자는 클래스 선언과 동일하게 적용
- 선언문은 반드시 세미콜론(
;)으로 종료 - 네이밍 컨벤션:
Handler접미사 사용 권장 (예:ClickHandler,DataReceivedHandler)
// 기본 선언 문법
[접근_수식어] delegate 반환_형식 대리자_이름(매개변수_목록);
// 예시
public delegate void LogHandler(string message);
internal delegate void ConnectionCallback(); // 매개변수 없을 때도 () 필수
public delegate TResult TransformHandler<TInput, TResult>(TInput source);
1.2 인스턴스화와 호출
모든 대리자는 암시적으로 System.Delegate를 상속받습니다. 인스턴스화는 메서드 참조를 연결하는 과정이며, 동기/비동기 실행 모두 지원합니다.
public delegate void NotifyHandler(string user, string content);
class NotificationService
{
static void Main()
{
NotifyHandler notifier = SendEmail;
notifier += SendPush; // 멀티캐스트 등록
// 동기 실행
notifier("admin", "서버 점검 예정");
// 명시적 동기 호출
notifier.Invoke("admin", "서버 점검 예정");
// 비동기 실행 (BeginInvoke/EndInvoke 패턴)
notifier.BeginInvoke("admin", "긴급 알림", ar => {
Console.WriteLine("비동기 처리 완료");
}, null);
}
static void SendEmail(string recipient, string body) =>
Console.WriteLine($"[Email] {recipient}: {body}");
static void SendPush(string target, string message) =>
Console.WriteLine($"[Push] {target}: {message}");
}
2. 범용 대리자: Func와 Action
.NET 3.5부터 표준화된 제네릭 대리자를 제공하여 직접 선언 없이 즉시 사용 가능합니다.
| 형식 | 반환값 | 용도 |
|---|---|---|
Action<T1, T2, ...> | void | 부수효과 중심의 작업 |
Func<T1, T2, ..., TResult> | TResult | 값을 계산하여 반환 |
// Action 활용
Action<string, int> repeatPrint = (text, count) => {
for (int i = 0; i < count; i++) Console.WriteLine(text);
};
// Func 활용
Func<int, int, int> maxFinder = (left, right) => left > right ? left : right;
// 복합 예시
Func<string, bool> validator = input => !string.IsNullOrWhiteSpace(input);
3. 가변성(Variance) in 대리자
3.1 핵심 개념
C# 4.0부터 제네릭 인터페이스와 대리자에 공변성(Covariance)과 반공변성(Contravariance)을 명시적으로 지정할 수 있습니다. 이는 리스코프 치환 원칙(LSP)을 기반으로 한 타입 안전한 변환을 보장합니다.
- out 키워드: 반환 타입에 적용 → 공변성 허용
- in 키워드: 매개변수 타입에 적용 → 반공변성 허용
3.2 대리자에서의 적용
class Animal { }
class Dog : Animal { }
class Cat : Animal { }
class VarianceDemo
{
// 공변성: 반환 타입의 범위를 넓힐 수 있음
delegate Animal Factory<out T>(); // T는 반환 타입에만 사용
// 반공변성: 매개변수 타입의 범위를 좁힐 수 있음
delegate void Consumer<in T>(T item); // T는 입력 매개변수에만 사용
static void Main()
{
// 공변성: Dog 팩토리를 Animal 팩토리로 참조
Factory<Dog> dogMaker = () => new Dog();
Factory<Animal> animalMaker = dogMaker; // OK: Dog → Animal
// 반공변성: Animal 소비자를 Dog 소비자로 참조
Consumer<Animal> animalFeeder = (a) => Console.WriteLine(a.GetType().Name);
Consumer<Dog> dogFeeder = animalFeeder; // OK: Animal → Dog
dogFeeder(new Dog()); // 실제로는 animalFeeder 실행
}
}
4. 무명 메서드와 람다 식의 진화
4.1 단계별 발전 과정
// 1단계: 명명된 메서드 (C# 1.0)
bool IsPositive(int value) => value > 0;
Predicate<int> predicate1 = IsPositive;
// 2단계: 무명 메서드 (C# 2.0)
Predicate<int> predicate2 = delegate(int value) {
return value > 0;
};
// 3단계: 람다 식 (C# 3.0+)
Predicate<int> predicate3 = (int value) => { return value > 0; };
// 4단계: 식 본문 람다 (가장 간결)
Predicate<int> predicate4 = value => value > 0;
4.2 람다 식의 캡처 메커니즘
class ClosureExample
{
static void DemonstrateCapture()
{
int threshold = 50;
var random = new Random();
// 외부 변수 threshold를 캡처
Func<int, bool> aboveThreshold = score => score > threshold;
// 캡처된 변수 수정 시 주의: 람다 내에서도 영향받음
var scores = Enumerable.Range(1, 10).Select(_ => random.Next(100));
var filtered = scores.Where(aboveThreshold);
threshold = 80; // 람다 실행 전 값 변경
Console.WriteLine(string.Join(", ", filtered)); // 80보다 큰 값만 출력
}
}
5. 실무 활용 패턴
5.1 이벤트 기반 아키텍처
class EventBroker<TEvent> where TEvent : EventArgs
{
private readonly Dictionary<string, List<Func<object, TEvent, Task>>> _handlers = new();
public void Subscribe(string channel, Func<object, TEvent, Task> handler)
{
if (!_handlers.ContainsKey(channel))
_handlers[channel] = new List<Func<object, TEvent, Task>>();
_handlers[channel].Add(handler);
}
public async Task PublishAsync(string channel, object sender, TEvent eventData)
{
if (!_handlers.TryGetValue(channel, out var channelHandlers))
return;
var tasks = channelHandlers.Select(h => h(sender, eventData));
await Task.WhenAll(tasks);
}
}
// 사용
var broker = new EventBroker<FileSystemEventArgs>();
broker.Subscribe("file:changed", async (sender, e) => {
await File.AppendAllTextAsync("log.txt", $"{e.FullPath} 변경됨\n");
});
5.2 전략 패턴의 함수형 구현
record Order(decimal Amount, string Category, bool IsVIP);
class DiscountEngine
{
private readonly List<Func<Order, decimal?>> _strategies = new();
public void AddRule(Func<Order, decimal?> calculator) =>
_strategies.Add(calculator);
public decimal CalculateFinalPrice(Order order)
{
var applicableDiscount = _strategies
.Select(s => s(order))
.Where(d => d.HasValue)
.DefaultIfEmpty(0)
.Max();
return order.Amount * (1 - applicableDiscount.Value);
}
}
// 구성
var engine = new DiscountEngine();
engine.AddRule(o => o.Amount > 10000 ? 0.1m : null); // 금액 기반
engine.AddRule(o => o.IsVIP ? 0.05m : null); // 등급 기반
engine.AddRule(o => o.Category == "BOOK" ? 0.03m : null); // 카테고리 기반