CodeGym /행동 /C# SELF /이벤트 처리: 람다 식

이벤트 처리: 람다 식

C# SELF
레벨 52 , 레슨 4
사용 가능

1. 소개

많은 현대 C# 프로젝트에서 이벤트 처리를 위한 "평범한" 메서드는 거의 안 써. 이유는 람다 식으로 구독하는 게 빠르고 간결해서, 처리 내용이 단순하고 다른 곳에서 재사용하지 않을 때 구독하는 자리에서 바로 핸들러를 선언할 수 있기 때문이야 (연산자 +=). 마치 긴 매뉴얼을 따로 쓰지 않고 커피머신에 "이 버튼을 눌러"라고 작은 메모를 붙여두는 것과 비슷해. 작업이 로컬하고 일회성이라면 람다가 딱 좋아!

실무에서 주로 쓰이는 곳

  • ASP.NET에서 (예: 페이지 생명주기 이벤트 처리 시),
  • WPF/WinForms UI에서 (예: 버튼 클릭 시),
  • 서버 사이드 로직에서 (예: pipeline 내부 로직),
  • 테스트에서 핸들러에 별도 이름이 필요 없을 때.

문법: 코드에서 이렇게 보임

먼저 일반적인 이벤트 처리 예제를 보자:


// 이벤트 선언
public event EventHandler? MyEvent;

// 일반 메서드로 구독
void Handler(object? sender, EventArgs e)
{
    Console.WriteLine("이벤트가 발생했어요!");
}

public void Subscribe()
{
    MyEvent += Handler;
}

이제 — 람다 식(익명 함수) 버전:


public void Subscribe()
{
    MyEvent += (sender, e) => Console.WriteLine("이벤트가 발생했어요 (람다)!");
}

주의할 점: 별도 메서드를 만들지 않고 구독하는 자리에서 바로 핸들러를 지정해. 람다 시그니처는 이벤트 타입과 자동으로 맞아떨어져 (EventHandler).

접근 방식 비교

일반 메서드 람다
코드량 더 많음 (메서드 + 구독) 더 적음, 모든 게 한곳에
재사용성 재사용 가능 보통 불가능
코드의 위치 분산됨 한곳에 집중
명확성/가독성 복잡한 로직에 좋음 간단한 케이스에 최고

2. 이벤트 처리와 람다 예제 애플리케이션

기본 구조


public class Menu
{
    public event EventHandler? ItemSelected;

    public void SelectItem(int index)
    {
        Console.WriteLine($"메뉴 항목 {index} 선택됨.");
        ItemSelected?.Invoke(this, EventArgs.Empty);
    }
}

람다식으로 구독하기


class Program
{
    static void Main()
    {
        var menu = new Menu();

        // 람다로 이벤트 구독
        menu.ItemSelected += (sender, e) =>
        {
            Console.WriteLine("선택해줘서 고마워요! 람다 핸들러가 실행되었어요.");
        };

        menu.SelectItem(1);
    }
}

예상 출력:

메뉴 항목 1 선택됨.
선택해줘서 고마워요! 람다 핸들러가 실행되었어요.

변수 캡처(클로저)

람다 식의 장점 중 하나는 외부 스코프의 값을 "기억"할 수 있다는 거야. 예를 들어 메뉴 항목이 몇 번 선택되었는지 세는 상황: counter 변수는 클로저에 의해 캡처돼 계속 유지돼.


static void Main()
{
    var menu = new Menu();
    int counter = 0;

    menu.ItemSelected += (s, e) =>
    {
        counter++;
        Console.WriteLine($"항목이 {counter}번 선택됨!");
    };

    menu.SelectItem(1);
    menu.SelectItem(2);
}

예상 출력:

메뉴 항목 1 선택됨.
항목이 1번 선택됨!
메뉴 항목 2 선택됨.
항목이 2번 선택됨!

이게 바로 클로저의 힘: counter는 람다 안에서 계속 살아 있어!

EventHandler<T> 파라미터 예제

이벤트가 EventHandler<T>를 사용하고, 여기서 T가 추가 정보를 담은 사용자 정의 클래스라면 람다 식은 아무 문제 없이 필요한 시그니처에 맞춰진다.


public class MenuItemSelectedEventArgs : EventArgs
{
    public int ItemIndex { get; }
    public string Description { get; }

    public MenuItemSelectedEventArgs(int itemIndex, string description)
    {
        ItemIndex = itemIndex;
        Description = description;
    }
}

public class Menu
{
    public event EventHandler<MenuItemSelectedEventArgs>? ItemSelected;

    public void SelectItem(int index, string description)
    {
        Console.WriteLine($"메뉴 항목 {index}: {description} 선택됨.");
        ItemSelected?.Invoke(this, new MenuItemSelectedEventArgs(index, description));
    }
}

// 사용 예
static void Main()
{
    var menu = new Menu();

    // 이벤트 인자 언패킹을 사용하는 람다
    menu.ItemSelected += (sender, args) =>
    {
        Console.WriteLine($"선택된 항목 #{args.ItemIndex}: {args.Description.ToUpper()}");
    };

    menu.SelectItem(3, "프로그램 정보");
}

예상 출력:

메뉴 항목 3: 프로그램 정보 선택됨.
선택된 항목 #3: 프로그램 정보

3. 로컬 핸들러와 람다

람다는 다음 경우에 이상적이야:

  • 처리 로직이 짧고 명확할 때,
  • 핸들러가 한 곳에서만 쓰일 때,
  • 로컬 컨텍스트의 변수를 캡처해야 할 때.

처리가 복잡하거나 재사용이 필요하거나 선언 위치 밖에서 호출될 가능성이 있으면, 별도 이름 있는 메서드를 쓰는 편이 좋아.

예: 로직을 람다 안에 넣을지 밖으로 뺄지

람다 (적합한 경우):


button.Click += (s, e) => MessageBox.Show("버튼이 눌렸어요!");

메서드로 분리 (복잡하거나 재사용 필요할 때):


button.Click += Button_Click;

void Button_Click(object sender, EventArgs e)
{
    if (UserConfirmed())
    {
        SaveData();
        MessageBox.Show("데이터가 저장되었습니다!");
    }
}

4. 내부적으로: 람다 핸들러에 무슨 일이 일어나는가

람다는 결국 delegate야, 일반 핸들러와 동일해. 컴파일러는 익명 메서드를 생성하고, 캡처된 변수가 있으면 그 변수를 보관할 숨겨진 클래스를 만들어.

자주 빠지는 함정 중 하나: 루프 안에서 람다를 만들고 이벤트에 구독하면, 모든 반복이 같은 루프 변수를 캡처해서 의도와 다른 동작을 할 수 있어.


for (int i = 0; i < 5; i++)
{
    buttons[i].Click += (sender, e) =>
    {
        Console.WriteLine($"버튼 #{i} 클릭");
    };
}

이 경우 모든 핸들러가 버튼 번호를 5로 출력할 수도 있어! 이를 피하려면 루프 안에서 변수를 복사해 사용해:


for (int i = 0; i < 5; i++)
{
    int buttonIndex = i; // 로컬 복사
    buttons[i].Click += (sender, e) =>
    {
        Console.WriteLine($"버튼 #{buttonIndex} 클릭");
    };
}

이제 기대한 대로 동작해.

람다 핸들러의 진짜 가치: 실무

람다 식은 시간이 부족한 상황에서 개발 속도를 빠르게 해주고, 로직이 단순할 때 코드가 문제 영역과 더 가까워지게 해줘. 실무에서 UI 이벤트 처리부터 비동기 이벤트 버스 구독까지 다양한 곳에서 자주 보게 될 거야.

5. 흔한 실수들

실수 #1: 수명이 긴 객체에서 구독 해제(-=)를 잊음.
구독자가 발행자에 구독을 해제하지 않으면, 델리게이트 참조가 구독자를 잡아두어 가비지 컬렉터가 해제하지 못해. 결과적으로 메모리 누수나 의존성 잔존이 발생할 수 있어, 특히 발행자가 오래 살 때(정적 이벤트, 싱글톤, 서비스 등).
회피법: 해제/파괴 시점에 항상 구독을 해제해(예: Dispose, OnDisable, OnDestroy). 복잡한 시나리오라면 약한 참조(weak events / WeakEventManager), 이벤트 매니저 패턴 또는 IObservable/Rx 같은 접근을 고려해.

실수 #2: 루프에서 로컬 복사 없이 변수를 캡처함.
루프 내부에 구독 코드를 쓰면 클로저가 같은 루프 변수를 잡아, 모든 핸들러가 최종 값을 보게 되는 전형적인 함정이야. 그 결과 모든 핸들러가 같은 숫자를 찍거나 의도치 않은 동작을 해.
회피법: 루프 안에서 값을 로컬 복사하고 그걸 캡처해:


for (int i = 0; i < n; i++)
{
    int current = i;
    button.Click += (s, e) => Handle(current);
}

또는 필요한 값을 메서드 래퍼에 전달하는 방식도 좋아. 이렇게 하면 의도가 명확해지고 안전해.

실수 #3: 구독 지점에 거대한 람다를 넣음.
구독 지점에 복잡한 비즈니스 로직을 집어넣으면 코드가 읽기 어렵고 테스트하기 힘들어. 게다가 익명 람다에선 정확히 같은 델리게이트 인스턴스를 확보하기 어려워서 구독 해제도 까다로워. 결과적으로 로직이 여러 군데로 흩어질 수 있어.
회피법: 복잡한 로직은 이름 있는 메서드나 서비스로 빼고, 필요하면 델리게이트를 변수/필드로 저장해서 나중에 구독 해제할 수 있게 해. 람다에는 가벼운 래퍼나 리다이렉션만 남기는 게 가독성과 유지보수에 좋아.

1
설문조사/퀴즈
C#에서의 이벤트, 레벨 52, 레슨 4
사용 불가능
C#에서의 이벤트
delegate로 이벤트 만들기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION