728x90

1. 참고 자료

더보기

A. 참고 링크

MSDN

 

 

B. 학습순서

  1. Callback 용어 개념
  2. Delegate 용어 개념
  3. 대리자를 선언하고 사용하는 방법
  4. 대리자를 사용하는 이유
  5. 일반화 대리자를 사용하는 방법
  6. Multicast Delegete (대리차 체인)
  7. 대리자에서 이벤트로 개념 확장

 

 

C. 대리자 사용 목적

  1. 메서드를 변수처럼 사용하기 위해
  2. 이벤트 처리
  3. 코드의 결합도 낮춤 (느슨한 결합, Loosely Coupled)
  4. 여러 개의 메서드를 하나의 대리자에 연결, 한 번에 실행 가능
  5. LINQ, 람다 표현식에서의 활용
  6. 메서드 호출을 런타임에 동적으로 변경할 때
  7. 비동기 호출 (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;
        }
    }
}