본문 바로가기

Develop/.NET 가이드

[C#] Events 이벤트

반응형

Events 이벤트
Events

Events 이벤트

먼저 용어부터 정리해보도록 합시다. 프로그래밍 세계에서 이벤트는 하나의 개념이 아니라 하나의 시스템입니다. 이벤트는 사용자의 클릭과 같은 행동에 대해 프로그램이 순차적으로 반응을 일으킬 수 있도록 고안된 시스템입니다. 디자인 세계에서는 이를 인터렉션이라 하죠.

옛날 옛적에 프로그램이 순차적으로 실행되는 코드에 불과하던 어셈블리어 시절, 이벤트라는 시스템은 불필요했습니다. 왜냐하면 사용자는 프로그램에게 명령어를 지정해 실행하는 행동만 취할 수 있었으니까요. 프로그램이 처리해야 할 이벤트는 '실행'에 따른 '결과'를 검은 배경에 하얀 글자로만 출력하면 되었습니다.

dos

하지만 그래픽 인터페이스의 등장으로 사용자는 마우스로 다양한 버튼을 클릭할 수 있게 됩니다. 즉 사용자는 프로그램과 복잡한 상호작용이 가능해졌습니다. 그에 따라 프로그램이 처리해야 할 상호작용 수는 엄청나게 늘어났고 비동기 개념도 등장하면서 이제 하나의 행동에 하나의 반응이 아닌 여러 반응을 처리해야 됩니다. 상호작용이 복잡해지니 상호작용에 적절히 반응하는 품질 좋은 프로그램을 개발하기가 어려워지게 됩니다. 왜냐하면 사용자는 프로그램과 상호작용하면서 자연스러운 반응을 기대하기 때문에, 복잡해진 상호작용 속에서 프로그램이 하나의 반응이라도 놓치게 되면 사용자는 프로그램에 오류가 있다 생각하게 됩니다. 따라서 개발자는 프로그래밍할 때 사용자의 행동에 적절한 반응을 할 수 있도록 프로그램을 설계하기 위하여 이벤트 시스템을 고안하게 됩니다.

공급자와 구독자

이벤트의 핵심 구성요소는 공급자구독자 입니다.

공급자 : 사용자의 행동을 관찰하며 구독자에게 알리는 역할
구독자 : 공급자를 구독하며 사용자의 행동을 전달받아 반응을 하는 역할

사용자의 행동은 공급자를 통해 구독자에게 전달되고, 구독자는 전달받은 행동에 대해 적절히 반응합니다.

관찰자 디자인 패턴

관찰자 디자인 패턴은 공급자와 구독자의 역할 분담을 프로그래밍 디자인 패턴으로 구현한 개념입니다. 따라서 이벤트 시스템은 내부적으로 관찰자 디자인 패턴을 반영하여 구현되어 있습니다.

Delegate 대리자와 Event 이벤트의 관계

C# 에서 이벤트는 Delegate 대리자를 확장한 개념일 뿐입니다. C#에서 이벤트는 대리자의 동적 바인딩덕분에 구현이 유연해졌습니다. 이벤트의 구현은 정의에 부합하는 함수를 런타임에 연결하기만 하면 됩니다. 런타임에 이벤트에 연결된 함수를 교체할 수도 있습니다. 또 하나의 공급자에 여러 구독자를 연결할 수 있는 이유는 이벤트가 MulticaseDelegate의 확장이기 때문입니다. 따라서 이벤트를 구독 또는 구독 취소하는 연산자 또한 대리자로 구현된 개념을 활용한 것에 불과합니다.

이벤트 사용

이벤트 정의

// event 키워드와 EventHandler 클래스
public event EventHandler<FileListArgs> Progress;
// 이벤트 구독과 구독 취소에 속성의 get/set 처럼 커스터마이징할 수 있습니다.
internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
{
    add { directoryChanged += value; }
    remove { directoryChanged -= value; }
}
private EventHandler<SearchDirectoryArgs> directoryChanged;

이벤트 호출

// 이벤트는 Invoke 함수를 통해 호출되며 구독자 목록을 순차적으로 실행하게 됩니다.
Progress?.Invoke(this, new FileListArgs(file));

이벤트 구독

EventHandler<FileListArgs> onProgress = (sender, eventArgs) => 
    Console.WriteLine(eventArgs.FoundFile);

// += 연산자로 이벤트를 구독합니다.
fileLister.Progress += onProgress;

이벤트 구독 취소

// -= 연산자로 이벤트를 구독 취소합니다.
fileLister.Progress -= onProgress;

표준 .NET 이벤트 패턴

표준 이벤트 구독자 시그니처

void OnEventRaised(object sender, EventArgs args);

구독자 표준 함수명은 On으로 시작하고 EventRaised 부분은 행동을 잘 표현하는 동사와 시제로 이름을 부여합니다.

구독자 표준 반환값은 void 입니다. void 외에도 반환 가능하지만 마지막으로 호출된 구독자의 값이 공급자를 호출한 코드로 반환되는데, 런타임에 구독자가 공급자에 연결되므로 어떤 구독자가 마지막 구독자인지 예측하기 어렵습니다. 그리므로 공급자와 구독자 간의 데이터 교환은 EventArgs를 사용해주시길 강력히 권장합니다.

구독자 표준 매개변수는 objectEventArgs 입니다. object는 공급자를 호출하여 이벤트를 발생시킨 객체이며 EventArgs는 이벤트와 관련한 변수가 담기는 상자입니다.

// EventArgs를 상속하여 원하는 변수를 교환할 수 있도록 정의한 SearchDirectoryArgs
internal class SearchDirectoryArgs : EventArgs
{
    internal string CurrentSearchDirectory { get; }
    internal int TotalDirs { get; }
    internal int CompletedDirs { get; }

    internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs)
    {
        CurrentSearchDirectory = dir;
        TotalDirs = totalDirs;
        CompletedDirs = completedDirs;
    }
}

업데이트된 .NET Core 이벤트 패턴

공식문서에 따르면 .NET Core 에서는 EventArgs를 상속하지 않는 구조체를 이벤트 변수로 사용할 수 있게 되었습니다.

// EventArgs 를 상속하는 대신 구조체를 정의해 사용합니다.
// 구조체는 Value Type 이기 때문에 전달할 때 참조값이 아닌 실제 값을 모두 복사해 전달됩니다.
internal struct SearchDirectoryArgs
{
    internal string CurrentSearchDirectory { get; }
    internal int TotalDirs { get; }
    internal int CompletedDirs { get; }

    // this() 를 호출하여 속성에 기본값만 설정하면 EventArgs 를 상속하지 않고 사용 가능합니다.
    internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs) : this()
    {
        CurrentSearchDirectory = dir;
        TotalDirs = totalDirs;
        CompletedDirs = completedDirs;
    }
}

비동기 구독자가 포함된 이벤트

먼저 비동기 프로그래밍에 익숙하지 않은 개발자에게 추천하지 않습니다. 왜냐하면 비동기에서 파생된 버그는 디버깅하기가 매우 어렵기 때문입니다. 따라서 비동기를 학습하지 않은 개발자에게 크나큰 고통이 되기 쉽습니다.

따라서 이 글을 정독하면서 여러 실험 코드를 작성해 비동기의 동작 방식과 특징을 이해하신 후에 비동기 이벤트 구독자를 시도해보시길 바랍니다.

먼저 비동기 이벤트 구독자의 예제 코드는 아래와 같습니다.

worker.StartWorking += async (sender, eventArgs) =>
{
    try 
    {
        await DoWorkAsync();
    }
    catch (Exception e)
    {
        //Some form of logging.
        Console.WriteLine($"Async task failure: {e.ToString()}");
        // Consider gracefully, and quickly exiting.
    }
};

이벤트 구독자는 Task 를 반환하지 않으므로 이벤트 구독자 코드 내에 심각한 오류가 발생하여도 스레드만 종료되고 프로세스는 오류에 대응하지 못한 채로 실행되는 사태가 발생할 수 있습니다. 예를 들어 이벤트에 대한 반응으로 데이터베이스를 수정하는 작업을 비동기로 실행하던 중 오류가 발생하여 데이터가 수정되지 못했더라도, 프로세스는 데이터의 수정이 정상적으로 처리되었다는 가정 하에 잘못 수정된 데이터를 신뢰하게 됩니다. 만약 프로세스가 잘못 수정된 데이터를 기반으로 사용자 인증이라도 수행하게 된다면 보안 사고로 이어질 수 있습니다.

따라서 예제 코드처럼 실제 비동기 작업을 수행하는 DoWorkAsync() 코드를 Try-Catch 블록으로 감싸 이벤트 코드 내에 발생할 수 있는 심각한 오류에 대응할 수 있도록 조치합니다.

반응형