다음을 통해 공유


대리자를 위한 일반적인 패턴

이전

대리자는 구성 요소 간의 최소 결합을 포함하는 소프트웨어 설계를 가능하게 하는 메커니즘을 제공합니다.

이러한 디자인의 한 가지 훌륭한 예는 LINQ입니다. LINQ 쿼리 식 패턴은 모든 기능에 대한 대리자를 사용합니다. 다음 간단한 예제를 고려하세요.

var smallNumbers = numbers.Where(n => n < 10);

이렇게 하면 숫자 시퀀스가 값 10보다 작은 값으로만 필터링됩니다. Where 메서드는 시퀀스의 요소가 필터를 통과하는지 결정하는 대리자를 사용합니다. LINQ 쿼리를 만들 때 이 특정 목적을 위해 대리자의 구현을 제공합니다.

Where 메서드의 프로토타입은 다음과 같습니다.

public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);

이 예제는 LINQ의 일부인 모든 메서드와 함께 반복됩니다. 모두 특정 쿼리를 관리하는 코드에 대한 대리자를 사용합니다. 이 API 디자인 패턴은 학습하고 이해할 수 있는 강력한 패턴입니다.

이 간단한 예제에서는 대리자가 구성 요소 간의 결합을 거의 요구하지 않도록 하는 방법을 보여 줍니다. 특정 기본 클래스에서 파생되는 클래스를 만들 필요가 없습니다. 특정 인터페이스를 구현할 필요가 없습니다. 유일한 요구 사항은 현재 작업에 기본적인 하나의 메서드의 구현을 제공하는 것입니다.

대리자를 사용하여 사용자 고유의 구성 요소 빌드

대리자를 사용하는 디자인을 사용하여 구성 요소를 만들어 이 예제를 빌드해 보겠습니다.

대규모 시스템의 로그 메시지에 사용할 수 있는 구성 요소를 정의해 보겠습니다. 라이브러리 구성 요소는 여러 다른 플랫폼의 다양한 환경에서 사용할 수 있습니다. 구성 요소에는 로그를 관리하는 많은 일반적인 기능이 있습니다. 시스템의 모든 구성 요소에서 메시지를 수락해야 합니다. 이러한 메시지에는 핵심 구성 요소가 관리할 수 있는 다른 우선 순위가 있습니다. 메시지에는 최종 보관된 형식의 타임스탬프가 있어야 합니다. 고급 시나리오의 경우 원본 구성 요소별로 메시지를 필터링할 수 있습니다.

자주 변경되는 기능의 한 가지 측면은 메시지가 기록되는 위치입니다. 일부 환경에서는 오류 콘솔에 기록될 수 있습니다. 다른 경우에는 파일입니다. 다른 가능성으로는 데이터베이스 스토리지, OS 이벤트 로그 또는 기타 문서 스토리지가 있습니다.

다양한 시나리오에서 사용할 수 있는 출력 조합도 있습니다. 콘솔 및 파일에 메시지를 쓸 수 있습니다.

대리자를 기반으로 하는 디자인은 많은 유연성을 제공하고 나중에 추가될 수 있는 스토리지 메커니즘을 쉽게 지원할 수 있도록 합니다.

이 디자인에서 기본 로그 구성 요소는 가상이 아닌 봉인된 클래스일 수 있습니다. 대리자 집합을 연결하여 메시지를 다른 스토리지 미디어에 쓸 수 있습니다. 멀티캐스트 대리자를 기본적으로 지원하므로 메시지를 여러 위치(파일 및 콘솔)에 기록해야 하는 시나리오를 쉽게 지원할 수 있습니다.

첫 번째 구현

작게 시작해 보겠습니다. 초기 구현에서는 새 메시지를 수락하고 연결된 대리자를 사용하여 작성합니다. 콘솔에 메시지를 쓰는 하나의 대리자로 시작할 수 있습니다.

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

    public static void LogMessage(string msg)
    {
        if (WriteMessage is not null)
            WriteMessage(msg);
    }
}

위의 정적 클래스는 가장 간단한 작업입니다. 콘솔에 메시지를 쓰는 메서드에 대한 단일 구현을 작성해야 합니다.

public static class LoggingMethods
{
    public static void LogToConsole(string message)
    {
        Console.Error.WriteLine(message);
    }
}

마지막으로, 로거에 선언된 'WriteMessage' 대리자에 연결하여 대리자를 설정해야 합니다.

Logger.WriteMessage += LoggingMethods.LogToConsole;

관행

지금까지의 샘플은 매우 간단하지만 대리자가 포함된 디자인에 대한 몇 가지 중요한 지침을 보여 줍니다.

핵심 프레임워크에 정의된 대리자 형식을 사용하면 사용자가 대리자를 더 쉽게 작업할 수 있습니다. 새 형식을 정의할 필요가 없으며 라이브러리를 사용하는 개발자는 특수화된 새 대리자 형식을 학습할 필요가 없습니다.

사용되는 인터페이스는 최대한 최소화되고 유연합니다. 새 출력 로거를 만들려면 하나의 메서드를 만들어야 합니다. 해당 메서드는 정적 메서드 또는 인스턴스 메서드일 수 있습니다. 액세스 권한이 있을 수 있습니다.

출력 형식 지정

이 첫 번째 버전을 좀 더 강력하게 만든 다음 다른 로깅 메커니즘을 만들어 보겠습니다.

다음으로, 로그 클래스가 더 구조화된 메시지를 만들 수 있도록 메서드에 몇 가지 인수 LogMessage() 를 추가해 보겠습니다.

public enum Severity
{
    Verbose,
    Trace,
    Information,
    Warning,
    Error,
    Critical
}
public static class Logger
{
    public static Action<string>? WriteMessage;

    public static void LogMessage(Severity s, string component, string msg)
    {
        var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
        if (WriteMessage is not null)
            WriteMessage(outputMsg);
    }
}

다음으로, 해당 Severity 인수를 사용하여 로그의 출력으로 전송되는 메시지를 필터링해 보겠습니다.

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}";
        if (WriteMessage is not null)
            WriteMessage(outputMsg);
    }
}

관행

로깅 인프라에 새 기능을 추가했습니다. 로거 구성 요소는 모든 출력 메커니즘에 매우 느슨하게 결합되므로 로거 대리자를 구현하는 코드에 영향을 주지 않고 이러한 새 기능을 추가할 수 있습니다.

이를 계속 빌드할 때 이 느슨한 결합을 통해 다른 위치를 변경하지 않고 사이트의 일부를 보다 유연하게 업데이트할 수 있는 방법에 대한 더 많은 예제가 표시됩니다. 실제로 더 큰 애플리케이션에서는 로거 출력 클래스가 다른 어셈블리에 있을 수 있으며 다시 작성할 필요도 없습니다.

두 번째 출력 엔진 빌드

로그 구성 요소가 잘 진행되고 있습니다. 파일에 메시지를 기록하는 출력 엔진을 하나 더 추가해 보겠습니다. 이것은 조금 더 복잡한 출력 장치가 될 것입니다. 이 클래스는 파일 작업을 캡슐화하고 각 쓰기 후에 파일이 항상 닫혀 있는지 확인하는 클래스입니다. 이렇게 하면 각 메시지가 생성된 후 모든 데이터가 디스크로 플러시됩니다.

파일 기반 로거는 다음과 같습니다.

public class FileLogger
{
    private readonly string logPath;
    public FileLogger(string path)
    {
        logPath = path;
        Logger.WriteMessage += LogMessage;
    }

    public void DetachLog() => Logger.WriteMessage -= LogMessage;
    // make sure this can't throw.
    private void LogMessage(string msg)
    {
        try
        {
            using (var log = File.AppendText(logPath))
            {
                log.WriteLine(msg);
                log.Flush();
            }
        }
        catch (Exception)
        {
            // Hmm. We caught an exception while
            // logging. We can't really log the
            // problem (since it's the log that's failing).
            // So, while normally, catching an exception
            // and doing nothing isn't wise, it's really the
            // only reasonable option here.
        }
    }
}

이 클래스를 만든 후에는 인스턴스화할 수 있으며 LogMessage 메서드를 로거 구성 요소에 연결합니다.

var file = new FileLogger("log.txt");

이 두 가지는 상호 배타적이지 않습니다. 두 로그 메서드를 모두 연결하고 콘솔 및 파일에 메시지를 생성할 수 있습니다.

var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier

나중에 동일한 애플리케이션에서도 시스템에 대한 다른 문제 없이 대리자 중 하나를 제거할 수 있습니다.

Logger.WriteMessage -= LoggingMethods.LogToConsole;

관행

이제 로깅 하위 시스템에 대한 두 번째 출력 처리기를 추가했습니다. 파일 시스템을 올바르게 지원하려면 좀 더 많은 인프라가 필요합니다. 대리자는 인스턴스 메서드입니다. 프라이빗 메서드이기도 합니다. 대리자 인프라가 대리자를 연결할 수 있으므로 더 큰 접근성이 필요하지 않습니다.

둘째, 대리자 기반 디자인을 사용하면 추가 코드 없이 여러 출력 메서드를 사용할 수 있습니다. 여러 출력 방법을 지원하기 위해 추가 인프라를 빌드할 필요가 없습니다. 호출 목록의 다른 메서드가 됩니다.

파일 로깅 출력 메서드의 코드에 특히 주의하세요. 예외가 발생하지 않도록 코드화됩니다. 이것이 항상 엄격하게 필요한 것은 아니지만 종종 좋은 관행입니다. 대리자 메서드 중 하나가 예외를 발생시키는 경우, 호출 목록에 있는 나머지 대리자들은 호출되지 않습니다.

마지막으로, 파일 로거는 각 로그 메시지에서 파일을 열고 닫아 리소스를 관리해야 합니다. 파일을 열어 두고 완료되면 파일을 닫도록 구현 IDisposable 하도록 선택할 수 있습니다. 두 방법 모두 장점과 단점이 있습니다. 둘 다 클래스 간에 좀 더 많은 결합을 만듭니다.

두 시나리오를 지원하기 위해 클래스의 Logger 코드를 업데이트할 필요가 없습니다.

널(Null) 대리자 처리

마지막으로, 출력 메커니즘이 선택되지 않은 경우 이러한 경우에 대해 견고하도록 LogMessage 메서드를 업데이트해 보겠습니다. 현재 구현은 대리자에 호출 목록이 연결되어 있지 않은 경우 NullReferenceException 예외를 발생시킵니다. 메서드가 연결되지 않은 경우 자동으로 계속되는 디자인을 선호할 수 있습니다. 이 방법은 메서드와 결합된 null 조건부 연산자를 쉽게 사용할 수 Delegate.Invoke() 있습니다.

public static void LogMessage(string msg)
{
    WriteMessage?.Invoke(msg);
}

왼쪽 피연산자(?.)가 null인 경우, null 조건부 연산자(WriteMessage)는 단락 처리되어 메시지를 기록하려고 시도하지 않습니다.

Invoke()System.Delegate의 설명서에는 System.MulticastDelegate 메서드가 나열되어 있지 않습니다. 컴파일러는 선언된 대리자 형식에 대해 형식 안전 Invoke 메서드를 생성합니다. 이 예제에서 Invoke는 단일 string 인수를 사용하고, 반환 형식은 void입니다.

사례 요약

다른 작성기 및 기타 기능으로 확장할 수 있는 로그 구성 요소의 시작 부분을 살펴보았습니다. 디자인에서 대리자를 사용하면 이러한 다양한 구성 요소가 느슨하게 결합됩니다. 이렇게 하면 몇 가지 이점이 있습니다. 새 출력 메커니즘을 쉽게 만들어 시스템에 연결할 수 있습니다. 이러한 다른 메커니즘에는 로그 메시지를 작성하는 메서드인 하나의 메서드만 필요합니다. 새 기능이 추가될 때 복원력이 있는 디자인입니다. 작가에게 필요한 계약은 하나의 메서드를 구현하는 것입니다. 해당 메서드는 정적 또는 인스턴스 메서드일 수 있습니다. 공용, 비공개 또는 기타 법적 액세스일 수 있습니다.

로거 클래스는 호환성이 손상되는 변경을 도입하지 않고도 여러 가지 향상된 기능 또는 변경 내용을 만들 수 있습니다. 다른 클래스와 마찬가지로 변경 내용이 손상될 위험이 없으면 공용 API를 수정할 수 없습니다. 그러나 로거와 출력 엔진 간의 결합은 대리자를 통해서만 가능하기 때문에 다른 형식(예: 인터페이스 또는 기본 클래스)은 포함되지 않습니다. 결합은 가능한 한 작습니다.

다음