[C#] 9.2 이벤트

이벤트는 특정 사건이 발생하는 것을 감시하는 개체가 이를 처리하는 개체에게 이벤트가 발생하였을 때 필요한 인자들과 함께 발생 사실을 통보하기 위한 특별한 멤버입니다. 이벤트를 감시하여 다른 개체에게 발생한 사실을 통보하는 개체를 이벤트 게시자라고 하며 이벤트가 발생하였을 때 이벤트 게시자로부터 통보받아 처리하는 개체를 이벤트 구독자라고 합니다.

C#에서 이벤트는 대리자 멤버를 캡슐화할 때 event 키워드를 명시하면 됩니다. 그리고 이벤트로 만들기 위해 정의한 대리자는 이벤트를 통보한 개체와 이벤트 처리에 필요한 인자를 포함하여 시그니쳐를 정의하도록 가이드하고 있습니다.

▶ 이벤트를 위한 대리자 정의

//obj: 이벤트를 통보한 개체 , e: 이벤트 처리에 필요한 인자
public delegate void AddMemberEvent(object obj, AddMemberEventArgs e);

▶ EventArgs 기반의 파생 형식 정의

class AddMemberEventArgs:EventArgs
{
    public string Name
    {
        get;
        private set;
    }
    public string Addr
    {
        get;
        private set;
    }
    public Member Member
    {
        get;
        private set;
    }
    public AddMemberEventArgs(Member member)
    {
        Name = member.Name;
        Addr = member.Addr;
        this.Member = member;
    }
}

이벤트 게시자에는 이벤트 구독자가 이벤트 핸들러를 등록할 수 있게 이벤트 멤버의 가시성을 public으로 지정하는 것이 일반적입니다. 그리고 이벤트 게시자는 특정한 상황이 발생하면 이벤트 인자를 생성하여 등록된 이벤트 핸들러를 호출하여 전달합니다.

▶ 이벤트 게시자

class MemberManager
{
    public event AddMemberEvent AddMemberEventHandler = null;
    ... 중략...
    protected virtual void OnAddMember(Member member)
    {
        if (AddMemberEventHandler != null) //등록된 이벤트 핸들러가 있을 때
        {
            AddMemberEventHandler(this, new AddMemberEventArgs(member));
        }
    }
}

이벤트 구독자는 이벤트 처리기를 이벤트 게시자에게 등록하는 부분이 있어야 하며 등록이 되어 있으면 이벤트가 발생하면 이벤트 게시자가 등록한 이벤트 처리기를 호출해 줍니다. 이벤트 처리기에서는 해당 이벤트가 발생했을 때 처리할 구문을 작성하는 메서드입니다.

▶ 이벤트 구독자

class Application
{
    Application()
    {
        mm.AddMemberEventHandler +=
            new AddMemberEvent(OnAddMemberEventHandler); //등록
    }
    // 이벤트 처리기
    void OnAddMemberEventHandler(object obj, AddMemberEventArgs e) 
    {
        // 이벤트 처리 구문
    }
}

다음은 이벤트를 사용하는 예를 보여주는 회원 관리 프로그램입니다.

프로그램에는 회원 관리 프로그램을 구동하는 Application이 있으며 회원 정보를 추가하는 MemberAdder가 있습니다. 그리고 회원 정보를 출력하는 MemberViewer가 있고 회원 정보를 관리하는 MemberManager가 있습니다.

MemberManager는 회원 추가 요청이 있을 때 새로운 회원 정보일 때 회원을 추가합니다. 그리고 회원이 추가되면 MemberViewer가 화면에 추가된 회원 정보를 출력합니다. 이를 위해 MemberManager는 회원 정보 추가 이벤트를 멤버로 캡슐화합니다. 즉, 이벤트 게시자 역할을 수행합니다. MemberViewer에는 MemberManager에게 이벤트 처리기를 등록하여 회원 정보가 추가될 때 이를 통보받아 화면에 회원 정보를 출력하는 이벤트 구독자입니다.

클래스 다이어그램
클래스 다이어그램

먼저, 간단하게 Member 클래스를 정의합시다. 여기에서는 회원의 이름과 주소를 멤버 속성으로 갖게 정의할게요.

▶ Member 클래스

public class Member
{
    public string Name
    {
        get;
        private set;
    }
    public string Addr
    {
        get;
        private set;
    }
    public Member(string name, string addr)
    {
        Name = name;
        Addr = addr;
    }
}

그리고 회원이 추가될 때 이벤트 처리에 필요한 이벤트 인자를 정의합시다. 이벤트 처리기에서는 추가된 회원 정보가 필요하겠죠. 그리고 회원 이름과 주소를 바로 접근할 수 있으면 좀 더 편의성이 높아질 것입니다.

▶ 이벤트 인자 형식 정의

public class AddMemberEventArgs:EventArgs
{
    public string Name
    {
        get
        {
            return Member.Name;
        }
    }
    public string Addr
    {
        get
        {
            return Membmer.Addr;
        }
    }
    public Member Member
    {
        get;
        private set;
    }
    public AddMemberEventArgs(Member member)
    {
        Name = member.Name;
        Addr = member.Addr;
        this.Member = member;
    }
}

회원이 추가될 때 이벤트를 사용할 것이므로 대리자 형식을 하나 정의해야겠죠.

▶ 대리자 정의

//obj: 이벤트를 통보한 개체, e: 이벤트 처리에 필요한 인자
public delegate void AddMemberEvent(object obj, AddMemberEventArgs e);

이벤트 게시자는 MemberManager 개체입니다. 멤버 이벤트를 캡슐화해야겠죠.

public event AddMemberEvent AddMemberEventHandler = null;

그리고 회원 정보를 보관하는 컬렉션을 멤버로 캡슐화합시다. 여기서는 회원 이름을 키로하고 멤버 개체를 값으로 하는 Dictionary를 사용할게요.

Dictionary<string, Member> members = = new Dictionary<string, Member>();

회원을 추가하는 메서드를 추가합시다. 입력 인자로 이름과 주소를 받게 합시다. 그리고 인자로 받은 이름이 이미 컬렉션에 있다면 추가 실패를 반환하고 그렇지 않으면 회원 추가하고 참을 반환하면 되겠죠.

internal bool AddMember(string name, string addr)
{
    if (members.ContainsKey(name))
    {
        return false;
    }
    OnAddMember(new Member(name, addr));
    return true;
}

OnAddMember 메서드에서는 컬렉션에 회원 개체를 보관하는 작업과 등록된 이벤트 핸들러가 있는지 확인하여 이를 호출해 주어야겠죠.

protected virtual void OnAddMember(Member member)
{
    members[member.Name] = member;
    if (AddMemberEventHandler != null)
    {
        AddMemberEventHandler(this, new AddMemberEventArgs(member));
    }
}

그리고 여기서 MemberManager는 단일체로 정의할게요.

static public MemberManager Singleton
{
     get;
    private set;
}
static MemberManager()
{
   Singleton = new MemberManager();
}
private MemberManager()
{
}

▶ MemberManager 클래스

class MemberManager
{
    public event AddMemberEvent AddMemberEventHandler = null;
    Dictionary<string, Member> members = new Dictionary<string, Member>();
    static public MemberManager Singleton
    {
        get;
        private set;
    }

    static MemberManager()
    {
        Singleton = new MemberManager();//단일체 생성
    }
    private MemberManager(){    } //단일체로 구현하기 위하여 private으로 접근 지정
    protected virtual void OnAddMember(Member member)
    {
        members[member.Name] = member;
        if (AddMemberEventHandler != null) //이벤트 핸들러가 등록되어 있을 때
        {
            AddMemberEventHandler(this, new AddMemberEventArgs(member));
        }
    }
    internal bool AddMember(string name, string addr)
    {
        if (members.ContainsKey(name))
        {
            return false;
        }
        OnAddMember(new Member(name, addr));
        return true;
    }
}

이벤트 구독자인 MemberViewer에서는 생성자에서 이벤트 게시자인 MemberManager 단일체를 참조하여 이벤트 처리기를 등록해야겠죠.

public MemberViewer()
{
    MemberManager mm = MemberManager.Singleton;
    mm.AddMemberEventHandler +=
        new AddMemberEvent(mm_AddMemberEventHandler);
}

그리고 이벤트 처리기에서는 이벤트 처리 인자로 회원 정보를 얻어와서 화면에 출력합시다.

▶ MemberViewer 클래스

class MemberViewer
{
    public MemberViewer()
    {
        MemberManager mm = MemberManager.Singleton;

        //이벤트 처리기 등록
        mm.AddMemberEventHandler +=
            new AddMemberEvent(mm_AddMemberEventHandler);
    }
    void mm_AddMemberEventHandler(object obj, AddMemberEventArgs e)
    {
        Console.WriteLine("회원이 추가되었습니다.");
        Console.WriteLine("이름:{0} 주소:{1}", e.Name, e.Addr);
    }
}

나머지 부분도 구현해 봅시다.

MemberAdder에서는 회원을 추가하기 위해 사용자와 상호 작용하는 부분을 작성할게요. 왜 이렇게 기능이 작게 분리하여 구현하는 것인지 의문인 분들도 계실겁니다. 지금 예제는 이벤트를 설명하기 위한 프로그램으로 큰 의미를 부여할 필요는 없습니다. 다만 윈도우즈 응용 프로그램이라고 생각하시면 메인 창과 회원 정보 추가 창을 분리하여 구현할 수 있을 것이며 프로그램 구조를 비슷하게 콘솔 응용으로 표현한 것이라 생각하세요.

▶ MemberAdder 클래스

class MemberAdder
{
    public void AddMember()
    {
        MemberManager mm = MemberManager.Singleton;

        Console.WriteLine("이I름을 입력하세요.");
        string name = Console.ReadLine();
        Console.WriteLine("주소를 입력하세요.");
        string addr = Console.ReadLine();
        if (mm.AddMember(name, addr) == false)
        {
            Console.WriteLine("{0} 정보는 이미 존재합니다.", name);
        }
    }
}

마지막으로 Application 클래스와 진입점을 작성해 봅시다.

Application 클래스도 단일체로 정의할게요. 그리고 멤버 필드에 MemberAdder 개체와 MemberViewer 개체를 캡슐화할게요. Application 개체가 생성될 때 이들 개체는 생성하고 구동이 되면 최종 사용자와 상호 작용을 하면 회원을 추가해 나갈게요.

사용자와 상호 작용을 하는 부분은 아주 간단하게 회원 추가를 할 것인지 프로그램을 종료할 것인지만 선택할 수 있게 하겠습니다.

▶ Application 클래스

class Application
{
    MemberAdder ma = null;
    MemberViewer mv = null;

    internal static Application Singleton //단일체에 대한 속성
    {
        get;
        private set;
    }

    static Application()
    {
        Singleton = new Application();//단일체 생성
    }
    private Application()//단일체로 표현하기 위해 private으로 접근 지정
    {
        ma = new MemberAdder();
        mv = new MemberViewer();
    }

    internal void Run()
    {
        bool check = false;
        
        do
        {
            Console.WriteLine("회원 추가:[1]");
            Console.WriteLine("다른 키를 누르면 종료됩니다.");

            check = (Console.ReadKey().Key == ConsoleKey.D1);
            if (check)
            {
                Console.WriteLine();
                ma.AddMember();
            }
        } while (check);
    }
}

class Program
{
    static void Main(string[] args)
    {
        Application application = Application.Singleton;
        application.Run();
    }
}