본문 바로가기

Develop/.NET 가이드

[C#] Delegates 대리자

반응형

delegates 대리자
Delegates

Delegate (대리자)를 왜 사용해야 하는지, 그리고 어떤 상황에 사용되는지 살펴보자.

설계 목적

마이크로소프트 공식 문서에 따르면 Delegate (대리자) 기능은 아래와 같은 목적으로 설계되었습니다.

  1. Late Binding 동적 바인딩을 지원하는 언어 구문을 만들어, 개발자가 여러 다양한 소프트웨어 설계 문제에 동적 바인딩을 적용하여 해결할 수 있도록 설계
  2. C 언어의 메서드 포인터와 같이, 메서드를 호출할 수 있는 방법이 필요해서 지정된 메서드를 호출할 수 있는 대리자여러 메서드 호출을 연결할 수 있는 대리자를 프레임워크에 구현
  3. C/C++ 언어와 달리 C# 언어는 모든 구문에서 Type-Safe(형식 안전성)를 지원하므로, 대리자도 Type-Safe(형식 안전성)를 지원
  4. 동적 바인딩 기능과 지정된 메서드를 호출하는 대리자 기능을 활용해, .NET 이벤트 패턴 에 적용될 수 있도록 구현

활용 분야

먼저 Delegate (대리자)는 개발자가 매일 사용하는 컴파일러와 프레임워크에 구석구석 적용될 만큼 중요한 기능입니다. 개발자가 눈치채지 못하는 사이에, 컴파일러에 의해 C# 코드가 대리자를 사용하는 코드로 변경되어 컴파일이 진행됩니다.

대표적으로 람다 식(Lamda Expression)공식 문서에 따르면 대리자를 더욱 편리하게 사용하려는 구문에 불과합니다.

아래 코드는 람다 식 예제 코드입니다. 람다 식은 컴파일러에 의해 익명 대리자로 인식되며, 대리자를 상속한 Func<T, TResult>로 해석되어 컴파일됩니다.


// 1. 람다식을 대리자를 상속한 Func 클래스에 할당
 Func<int, bool> func = (i => i % 2 == 0);

// 2. 결과 = false
System.Console.WriteLine(func(1));

람다 식은 C# 언어의 강력한 기능 중 하나인 Language Integrated Query (LINQ)를 구현하는데 활용되고 있습니다.

람다 식말고 대리자를 사용하는 또 다른 예로는 .NET 이벤트 패턴 가 있습니다. .NET 이벤트 패턴 에서 이벤트를 정의하고 이벤트를 구독하고 취소하는 구문은 대리자를 확장한 구문입니다. 구체적으로는 이벤트 패턴의 핵심 클래스인 EventHandler는 대리자를 상속하여 구현된 클래스입니다.

활용 방법

대리자는 알고리즘을 구현하는데 활용되기 보다, 소프트웨어 디자인을 설계할 때 주로 활용됩니다. 왜냐하면 대리자는 클래스 간 결합을 최소화하는데 용이하기 때문입니다. 대표적인 예로, LINQ 구문을 사용할 때 아래와 같이 대리자를 구현한 람다 식을 사용하여 불필요한 코드를 줄일 수 있습니다.

// 람다 식을 LINQ로 넘깁니다.
var smallNumbers = numbers.Where(n => n < 10);

// LINQ에 사용되는 Where 메소드 선언문입니다.
// 'Func<TSource, bool> predicate'로 대리자를 활용합니다.
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);

만약 대리자를 사용하지 않는다면, 아래와 같이 코드가 복잡해지게 됩니다.

// 1. 먼저 인터페이스를 구현합니다.
interface IMyCode
{
    public bool Method(int n);
}

// 2. LINQ가 인터페이스를 활용하도록 확장합니다.
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, IMyCode myCode);

// 3. 실제 사용될 메서드를 구현하기 위해 새로운 클래스를 만듭니다.
class MyCode : IMyCode
{
    public bool Method(int n)
    {
        return n < 10;
    }
}

// 4. 마지막으로 구현한 코드를 활용합니다.
var myCode = new MyCode();
var smallNumbers = numbers.Where(myCode);

위 예제 코드와 같이, 대리자는 다른 코드와의 결합도를 낮추는데 효과적입니다. 대리자 기능 덕분에 메소드 일부분만 변경하려고 기본 클래스에서 파생되는 클래스를 구현할 필요가 없습니다. 또 특정 메소드를 전달하기 위해 일회성의 소규모 인터페이스를 구현하지 않아도 됩니다.

활용 예제

서버에 필수적으로 사용되는 로그 시스템을 대리자를 활용해 구현해보자.

기초적인 로그 시스템

새 로그를 대리자를에 연결될 로그 기록 메소드를 통해 기록하는 간단한 클래스

public static class Logger
{
    // 반환값이 없는 대리자
    public static Action<string> WriteMessage;

    public static void LogMessage(string msg)
    {
        // 대리자와 연결된 메소드 호출
        WriteMessage(msg);
    }
}

// 대리자에 연결될 메서드 정의
public static void LogToConsole(string message)
{
    Console.Error.WriteLine(message);
}

// 대리자에 메서드 연결
Logger.WriteMessage += LogToConsole;

로그 기록 메서드를 Logger 클래스에 구현하지 않았습니다. 대신 로그 기록 메서드를 Logger 클래스 외부에서 구현하여 WriteMessage 대리자에 연결하도록 설계했습니다. 따라서 만약 로그를 기록하는 저장소가 변경되어도 새로운 Logger 클래스를 만들 필요없이 WriteMessage 대리자에 연결될 메서드만 따로 구현하여 연결하면 됩니다.

출력 서식 추가

메시지를 구조화하여 출력하는 서식 추가

// 메세지 중요도 지정
public enum Severity
{
    Verbose,
    Trace,
    Information,
    Warning,
    Error,
    Critical
}

public static class Logger
{
    public static Action<string> WriteMessage;

    // 메세지를 출력하는 최소 레벨 지정
    public static Severity LogLevel {get;set;} = Severity.Warning;

    public static void LogMessage(Severity s, string component, string msg)
    {
        if (s < LogLevel)
            return;

        // 메세지를 구조화하여 출력
        var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
        WriteMessage(outputMsg);
    }
}

WriteMessage 대리자로 로그 기록 메서드를 연결했기 때문에 로그의 출력 서식을 지정하는 코드가 추가되더라도, 로그 기록 메서드는 변경되지 않습니다.

정의

Delegate 대리자는 매개 변수 목록 및 반환 형식, 즉 시그니처가 동일한 메서드에 대한 참조를 나타내는 형식입니다. 따라서 대리자에 시그니처가 동일한 메서드를 연결하면 해당 메서드를 대리자를 통해 호출할 수 있습니다. 그리고 대리자는 내부적으로 클래스로 구현되어 있기 때문에 다른 메서드의 매개 변수로 전달될 수 있습니다. 즉, 위에서 살펴본 활용 방법처럼, 대리자를 기존 클래스에 새로운 코드를 삽입하는 방법으로 활용할 수 있습니다.

// 1. 기본적인 대리자 선언
public delegate void Del(string message);

// 2. 대리자에 연결될 메서드 구현
public static void DelegateMethod(string message)
{
    System.Console.WriteLine(message);
}

// 3. 대리자에게 메서드 연결
Del handler = DelegateMethod;

// 4. 대리자를 통해 메서드 호출
handler("Hello World");

대리자는 MulticastDelegate 클래스로도 구현되어 있어, 대리자는 여러 메서드를 순서대로 호출할 수 있습니다. 대리자가 대리하는 메서드 목록에 메서드를 추가 및 삭제하려면 += 연산자와 -= 연산자를 사용하면 됩니다.

// 메서드 추가
allMethodsDelegate += NewMethod;

// 메서드 삭제
allMethodsDelegate -= OldMethod;

주의사항

서로 다른 형식의 대리자를 비교하면 컴파일 오류가 발생합니다. 그리고 예상치 못한 결과를 가져올 수 있으므로 주의해야 합니다. 그리고 대리자가 여러 메서드를 호출할 경우, 여러 메서드를 호출하는 도중에 메서드에서 오류가 발생하면 이후에 호출되는 메서드는 호출되지 않으니 주의바랍니다.

반응형