6. 대리자(2)
728x90

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, CloudBackup의 존재를 모름 → 결합도 ↓
C. 대리자 버전
delegate void AlarmHandler(string message);
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}");
}
}
class Program
{
static void Main()
{
var alram = new Alarm(); // 2. 알림을 보내는 클래스 (발신자)
var logger = new Logger(); // 3. 로그 기록 클래스
var backup = new CloudBackup();// 4. 클라우드 백업 클래스
alram.Delegate_Alarm += logger.Log;
alram.Delegate_Alarm += backup.BackupData;
//Alarm.run_01( )함수 실행 → 대리자 → 대리자에 연결된 함수(logger.Log( ), backup.BackupData( ))실행
alram.run_01();
}
}
D. 대리자 버전 상세
: 콜백구조 이해- Alarm.run_01( )함수 실행 → 대리자 → 대리자에 연결된 함수(logger.Log( ), backup.BackupData( ))실행
/// <summary>
/// 1. 대리자(Delegate) 타입 정의
/// - AlarmHandler는 "string 하나를 받고, 반환값이 없는 메서드" 서명을 갖는 <콜백>을 나타냅니다.
/// - 대리자가 실행되면, 대리자에 등록된 메서드가 실행됩니다.
/// - [1]시작: class Alarm의 run_01()메서드
/// - [2]대리자 실행
/// - [3]도착: 대리자에 등록된 제3의 클래스의 메서드() 동작
/// </summary>
delegate void AlarmHandler(string message);
/// <summary>
/// 2. 알림(이벤트)을 발생시키는 발신자 역할의 클래스
/// - 발신자는 "무엇이 호출되는지"를 몰라도 대리자만 호출하면 됨 → 느슨한 결합(Loosely Coupled)
/// </summary>
class Alarm
{
/// <summary>
/// 2-1. 대리자 인스턴스(콜백 목록) 보관
/// - null 허용: 아무도 구독하지 않았을 수 있으므로 Nullable 처리습니다.
/// (학습 목적상 delegate 필드 사용 예시를 유지)
/// → 실무 권장: `public event AlarmHandler? Delegate_Alarm;` 처럼 event 키워드로 캡슐화
/// </summary>
public AlarmHandler? Delegate_Alarm;
/// <summary>
/// 2-2. 알림(이벤트) 클래스의 실제 구현 동작 로직 발생 지점
/// - 이 메서드는 "발신 트리거" 역할을 합니다. (예: 카톡 수신, 파일 변경, 센서 감지 등)
/// </summary>
public void run_01()
{
Console.WriteLine("무언가 작업이 실행되었습니다!(카톡 수신)");
// 2-3. 구독자(수신자)들에게 알림 브로드캐스트
// - ?.Invoke : null-conditional 호출. 구독자가 없으면(null) 아무 일도 하지 않고 안전하게 반환
// - Invoke는 등록된 순서대로 각 수신자의 메서드를 호출합니다.
// - message 파라미터는 수신자들에게 전달할 컨텍스트/설명 문자열입니다.
Delegate_Alarm?.Invoke("Alarm에서 이벤트가 발생했습니다.");
}
}
- 대리자를 통해 호출자는 피호출자(구현체)에 의존하지 않음
- 코드 수정 없이 기능을 교체하거나 확장할 수 있음
- SOLID 원칙을 기반으로 이해할 것
- 이런 구조는 전략 패턴(Strategy Pattern), 옵저버 패턴(Observer Pattern)의 기초가 됨
E. 이벤트 + 인터페이스 기반
using System;
namespace LooselyCoupledExample
{
// 1. 대리자 정의 (이벤트 기반 사용 시 필요 없음, Action<string> 사용 가능)
// delegate void NotifyHandler(string message);
// 2. 알림을 보내는 클래스 (발신자)
class Alarm
{
public event Action<string>? OnNotify;
public void run_02() // [1] 시작
{
Console.WriteLine("무언가 작업이 실행되었습니다!(카톡 수신)");
OnNotify?.Invoke("Event triggered in Notifier."); // [2] 중계: 이벤트(대리자)
}
}
// 3. 인터페이스 기반 수신자 정의
interface IReceiver
{
void Handle(string msg);
}
// 4. 로그를 기록하는 클래스 // [3] 도착1
class Logger : IReceiver
{
public void Handle(string msg) => Console.WriteLine($"[로그 기록기] {msg}");
}
// 5. 클라우드 백업 클래스 // [3] 도착2
class CloudBackup : IReceiver
{
public void Handle(string msg) => Console.WriteLine($"[클라우드 백업기] {msg}");
}
class Program
{
static void Main()
{
var alarm = new Alarm();
// 어떤 메서드가 호출될지는 Notifier는 모름 (느슨한 결합)
IReceiver logger = new Logger();
IReceiver backup = new CloudBackup();
// 이벤트에 메서드 연결
alarm.OnNotify += logger.Handle;
alarm.OnNotify += backup.Handle;
// run_02() 실행 시 이벤트 발생 → 대리자 → 대리자에 연결된 logger.Handle;, backup.Handle; 실행
alarm.run_02();
}
}
}
소스코드를 대리자에서 이벤트 기반 구조로 수정했습니다.
인터페이스(IReceiver)를 통해 느슨한 결합을 유지하는 형태로 리팩토링했습니다.
콜백 구조는 시작 > 중계 > 도착
발신자 > 대리자 > 수신자
[시작] 알람 클래스의 run_02() 실행 (이벤트 발생)
[중계] → 이벤트(대리자)
[도착] → 대리자에 연결된 logger.Handle;, backup.Handle; 실행
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. invoke
: 대리자(delegate)나 이벤트(event)에 연결된 메서드(구독자)를 실행시키는 역할
더보기
using System;
class Program
{
// 1. 대리자 정의 (string 매개변수, 반환 없음)
delegate void MyDelegate(string msg);
// 2. 실행할 메서드
static void ShowMessage(string text)
{
Console.WriteLine(text);
}
static void Main()
{
// 3. 대리자 변수에 메서드 연결
MyDelegate del = ShowMessage;
// 4. Invoke를 사용해서 메서드 실행
del.Invoke("안녕하세요, 대리자 Invoke 예제입니다!");
}
}
- MyDelegate → 어떤 메서드를 대신 실행할 수 있는 "참조 타입"
- del = ShowMessage; → 대리자가 ShowMessage를 가리킴
- del.Invoke("...") → 사실상 ShowMessage("...") 호출과 같음
즉, Invoke는 “대리자가 가리키는 메서드를 실행한다” 라는 뜻입니다.
6. 대리자에서 이벤트로
더보기
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.");
}
}
}
3. 이벤트 버전
using System;
namespace DelegateTest10.Events
{
// 버튼 동작되면 호출할 연결역할
delegate void Callback(string message);
class Button
{
public event Callback? Click_Signal; // 대리자를 이벤트로 변경 (외부에서 Invoke 금지)
public void RaiseClick(string message) // [1] 시작, 버튼이 클릭됨
{
Click_Signal?.Invoke(message); // [2] 이벤트(대리자)가 동작
//lbl.Slot_Function(message); // 이벤트가 동작하면, 39번 라인에서 동작
}
}
// 수신자
class Label
{
private readonly 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 btn = new Button();
var lbl = new Label("Label");
btn.Click_Signal += lbl.Slot_Function; // [3] 등록된 lbl.Slot_Function 동작
btn.RaiseClick("You've got mail."); // [1] 시작, 버튼이 클릭됨 >> [2] 이벤트(대리자)가 동작
// 필요 시 해지 가능
btn.Click_Signal -= lbl.Slot_Function;
}
}
}