6. 대리자(2)
1. 참고 자료
더보기
A. 참고 링크
B. 학습순서
- Callback 용어 개념
- Delegate 용어 개념
- 대리자를 선언하고 사용하는 방법
- 대리자를 사용하는 이유
- 일반화 대리자를 사용하는 방법
- Multicast Delegete (대리차 체인)
- 대리자에서 이벤트로 개념 확장
C. 대리자 사용 목적
- 메서드를 변수처럼 사용하기 위해
- 이벤트 처리
- 코드의 결합도 낮춤 (느슨한 결합, Loosely Coupled)
- 여러 개의 메서드를 하나의 대리자에 연결, 한 번에 실행 가능
- LINQ, 람다 표현식에서의 활용
- 메서드 호출을 런타임에 동적으로 변경할 때
- 비동기 호출 (BeginInvoke/EndInvoke)
2. 대리자 사용법
3. 느슨한 결합 구조
더보기
A. 느슨한 결합 (Loosely Coupled) 구조란?
- 정의
- 한 객체가 다른 객체의 구체적인 타입(구현체)을 모르고도 기능을 실행할 수 있는 구조.
즉,"무엇이 호출되는지는 모르지만 호출만 하면 된다"라는 방식입니다.
- 한 객체가 다른 객체의 구체적인 타입(구현체)을 모르고도 기능을 실행할 수 있는 구조.
- 장점
- 코드 변경 시 다른 클래스에 영향을 덜 줌 → 유지보수 용이
- 교체/확장 가능 → 새로운 기능을 추가할 때 기존 코드를 수정할 필요가 줄어듦
- SOLID 원칙 중 DIP (Dependency Inversion Principle) 과 관련 있음
B. 시나리오
- Notifier : 알림을 발생시키는 발신자 역할
- Logger : 메시지를 로그로 출력
- EmailSender : 메시지를 이메일로 출력
- 특징 : Notifier는 Logger, EmailSender의 존재를 모름 → 결합도 ↓
C. 소스코드 (대리자 버전 → 이벤트 버전 리팩토링)
// 1. 대리자 정의
delegate void AlarmHandler(string message);
// 2. 알림을 보내는 클래스 (발신자)
class Alarm
{
public AlarmHandler? Delegate_Alarm;
public void run_01()
{
Console.WriteLine("무언가 작업이 실행되었습니다!(카톡 수신)");
Delegate_Alarm?.Invoke("Alarm에서 이벤트가 발생했습니다.");
}
}
// 3. 로그 기록 클래스
class Logger
{
public void Log(string msg)
{
Console.WriteLine($"[로그 기록기] {msg}");
}
}
// 4. 클라우드 백업 클래스
class CloudBackup
{
public void BackupData(string msg)
{
Console.WriteLine($"[클라우드 백업기] {msg}");
}
}
Alarm.run_01( )함수 실행 → 대리자 → 대리자에 연결된 함수(logger.Log( ), backup.BackupData( ))실행
class Program
{
static void Main()
{
var notifier = new Alarm(); // 2. 알림을 보내는 클래스 (발신자)
var logger = new Logger(); // 3. 로그 기록 클래스
var backup = new CloudBackup();// 4. 클라우드 백업 클래스
notifier.Delegate_Alarm += logger.Log;
notifier.Delegate_Alarm += backup.BackupData;
notifier.run_01();
}
}
D. 참고
- 대리자를 통해 호출자는 피호출자(구현체)에 의존하지 않음
- 코드 수정 없이 기능을 교체하거나 확장할 수 있음
- SOLID 원칙을 기반으로 이해할 것
- 이런 구조는 전략 패턴(Strategy Pattern), 옵저버 패턴(Observer Pattern)의 기초가 됨
E. 이후 이벤트와 SOLID 학습 후 다시 참고할 것
using System;
namespace LooselyCoupledExample
{
// 1. 대리자 정의 (이벤트 기반 사용 시 필요 없음, Action<string> 사용 가능)
// delegate void NotifyHandler(string message);
// 2. 알림을 보내는 클래스 (발신자)
class Notifier
{
public event Action<string>? OnNotify;
public void DoSomething()
{
Console.WriteLine("Something happened!");
OnNotify?.Invoke("Event triggered in Notifier.");
}
}
// 3. 인터페이스 기반 수신자 정의
interface IReceiver
{
void Handle(string msg);
}
// 4. 로그를 기록하는 클래스 (수신자 1)
class Logger : IReceiver
{
public void Handle(string msg) => Console.WriteLine($"[Logger] {msg}");
}
// 5. 이메일을 보내는 클래스 (수신자 2)
class EmailSender : IReceiver
{
public void Handle(string msg) => Console.WriteLine($"[Email] {msg}");
}
class Program
{
static void Main()
{
var notifier = new Notifier();
// 어떤 메서드가 호출될지는 Notifier는 모름 (느슨한 결합)
IReceiver logger = new Logger();
IReceiver email = new EmailSender();
// 이벤트에 메서드 연결
notifier.OnNotify += logger.Handle;
notifier.OnNotify += email.Handle;
notifier.DoSomething();
}
}
}
소스코드를 대리자에서 이벤트 기반 구조로 수정했습니다.
인터페이스(IReceiver)를 통해 느슨한 결합을 유지하는 형태로 리팩토링했습니다.
Notifier는 Logger나 EmailSender에 직접 의존하지 않고,
IReceiver 인터페이스를 통해 동작을 호출합니다.
4. Action, Func, Predicate
더보기
1. Action<T> 예제
- 반환값이 없는 메서드 호출에 사용 (void)
using System;
class ActionExample
{
static void PrintMessage(string msg) => Console.WriteLine($"[Action] {msg}");
static void Main()
{
Action<string> show = PrintMessage;
show("Hello from Action!");
}
}
2. Func<T, TResult> 예제
- 반환값이 있는 메서드 호출에 사용
using System;
class FuncExample
{
static int Add(int a, int b) => a + b;
static void Main()
{
Func<int, int, int> sum = Add;
int result = sum(3, 4);
Console.WriteLine($"[Func] Result = {result}");
}
}
3. Predicate<T> 예제
- bool을 반환하는 조건식 메서드에 사용
using System;
using System.Collections.Generic;
class PredicateExample
{
static bool IsEven(int num) => num % 2 == 0;
static void Main()
{
Predicate<int> checkEven = IsEven;
List<int> nums = new List<int> { 1, 2, 3, 4, 5 };
List<int> evens = nums.FindAll(checkEven);
Console.WriteLine("[Predicate] Even numbers: " + string.Join(", ", evens));
}
}
5. 대리자에서 이벤트로
더보기
1. 소스코드 /w 주석
// 'Callback'이라는 이름의 대리자를 선언합니다.
// 이 대리자는 string 형식의 매개변수를 받아 반환값이 없는 메서드를 참조할 수 있습니다.
delegate void Callback(string message);
// Button 클래스: Click_Signal이라는 대리자 타입의 이벤트 역할 필드를 가집니다.
class Button
{
// 외부에서 메서드를 연결할 수 있는 대리자 필드
public Callback? Click_Signal; // null 허용으로 초기에는 연결된 메서드가 없을 수 있음
}
// Label 클래스: 화면에 메시지를 출력하는 역할
class Label
{
string name; // 레이블의 이름
// 생성자: Label 객체 생성 시 이름을 초기화
public Label(string name) => this.name = name;
// 슬롯 함수: 버튼의 대리자를 통해 호출될 메서드
public void Slot_Function(string message) =>
Console.WriteLine($"{name}.SomethingHappened : {message}"); // 메시지 출력
}
// 버튼을 클릭하면, 레이블에 출력되는 동작을 가정합니다.
class MainApp
{
static void Main(string[] args)
{
// Click_Signal 대리자를 가진 Button 객체를 생성합니다.
// conn.Click_Signal은 나중에 특정 메서드(콜백 함수)를 참조합니다.
var conn = new Button();
// 레이블을 만들고 이름을 "Label"로 설정합니다.
var lbl = new Label("Label");
// Click_Signal이 발생하면, 동작할 레이블의 slot 함수와 연결합니다.
conn.Click_Signal += lbl.Slot_Function;
// conn 객체의 Click_Signal 대리자가 실행되면(버튼이 눌리거나, 값이 입력되면)
// label의 Slot_Function 함수가 "You've got mail."을 전달받아 실행됩니다.
conn.Click_Signal("You've got mail.");
}
}
2. 소스코드
using System;
namespace DelegateTest10
{
delegate void Callback(string message);
class Button
{
public Callback? Click_Signal;
}
class Label
{
string name;
public Label(string name) => this.name = name;
public void Slot_Function(string message) =>
Console.WriteLine($"{name}.SomethingHappened : {message}");
}
class MainApp
{
static void Main(string[] args)
{
var conn = new Button();
var lbl = new Label("Label");
conn.Click_Signal += lbl.Slot_Function;
conn.Click_Signal("You've got mail.");
}
}
}