1. 소개
C#(그리고 .NET 전반)에서 이벤트는 객체들끼리 소통하는 한 방법이야. 예를 들어 버튼이 있는데, 그 버튼을 누르면 뭔가 실행되도록 하고 싶을 때가 있지. 이벤트 처리를 위해 별도의 메서드를 선언하고 그 메서드를 이벤트에 명시적으로 구독시키는 방식으로 할 수도 있어... 하지만 간단한 로직이라면 굳이 파일 여기저기에 메서드를 흩어놓고 싶지 않을 때가 많아.
람다식이 이런 경우에 딱 맞아 — 이벤트에 구독하는 자리에서 바로 반응을 작성할 수 있게 해줘. 최소한의 의례로 최대의 효율을 얻을 수 있어.
콜백(callback)도 마찬가지야: 어떤 메서드에 나중에 실행할 로직을 넘겨주고 싶을 때가 있는데, 람다식은 그걸 간단하고 직관적으로 만들어줘. 필요한 로직을 바로 호출 지점에 기술하면 돼.
이벤트와 콜백: 핵심만 짧게, 람다가 왜 필요한가
이벤트는 객체에 붙은 “표지판” 같은 거야: “여기 누군가 구독해서 어떤 일이 일어날 때 실행할 수 있어”.
Callback (역호출)은 “너에게 함수를 줄게, 필요할 때 그걸 호출해줘”라는 개념이야. 전화번호 남기고 나중에 연락받는 것과 유사해.
델리게이트와 이벤트를 처음 배울 때 다음 같은 문법을 봤을 거야:
button.Click += MyButtonClickHandler;
여기서 MyButtonClickHandler는 어딘가에 선언된 특정 메서드야. 하지만 로직이 단순하면 굳이 메서드를 하나 더 만들고 싶지 않을 때가 있지.
그럴 때 람다가 등장해:
button.Click += (sender, args) => { Console.WriteLine("버튼이 눌렸어!"); };
간결하고 바로 이해 가능하지!
2. 이벤트 핸들러로서의 람다: 간단한 것부터 흥미로운 것까지
간단한 형태: 전구 깜빡이면 콘솔에 출력
가정해보자, Button 클래스가 있고, 우리는 그 Click 이벤트에 반응하고 싶어.
// 이런 버튼이 있다고 생각해보자:
public class Button
{
public event EventHandler? Click;
public void SimulateClick() // 예제용으로: 프로그래밍 방식으로 "클릭" 발생시킴
{
// 누군가 구독했다면 핸들러들을 호출
Click?.Invoke(this, EventArgs.Empty);
}
}
이제 이 버튼을 사용해보자:
var button = new Button();
// 람다로 버튼 이벤트에 구독해보자!
button.Click += (sender, args) =>
{
Console.WriteLine("야호! 버튼이 눌렸어!");
};
button.SimulateClick(); // => 야호! 버튼이 눌렸어!
설명:
— => 뒤에 있는 게 당신의 “미니 함수” 본문이며, 이벤트가 발생하면 호출될 거야.
— 핸들러의 생애주기에 대해 고민할 필요 없어: 핸들러는 당신의 구독(+=)이 유지되는 동안 살아있어. 중요: 람다가 외부 변수를 캡처하고 있고 이벤트에서 언구독하지 않으면 메모리 누수가 발생할 수 있어.
변수를 캡처하는 람다 핸들러
int clickCount = 0;
button.Click += (sender, args) =>
{
clickCount++;
Console.WriteLine($"버튼이 이미 {clickCount}번 눌렸어!");
};
button.SimulateClick(); // => 버튼이 이미 1번 눌렸어!
button.SimulateClick(); // => 버튼이 이미 2번 눌렸어!
무슨 일이 일어나나:
람다가 외부의 clickCount 변수를 "캡처"해서 호출들 사이에 그 값을 기억하고 있어.
3. callbacks에 람다식 적용하기
콜백은 어떤 코드가 나중에 당신의 함수를 호출해야 할 때 자주 쓰여 — 예를 들어 긴 작업이 끝났을 때 호출하는 식으로.
람다를 callback으로 전달하기
어떤 메서드가 델리게이트나 람다를 인수로 받는다고 해보자:
void DoWork(Action callback)
{
Console.WriteLine("작업을 시작합니다...");
// 작업 흉내내기
System.Threading.Thread.Sleep(500); // (실제 앱에서는 이렇게 하지 마, 블로킹이야!)
callback(); // 작업 후 callback 호출
}
DoWork(() => Console.WriteLine("작업 완료!"));
출력:
먼저 콘솔에 "작업을 시작합니다..."가 찍히고, 그다음에 "작업 완료!"가 찍혀.
파라미터가 있는 람다도 전달할 수 있어:
void Calculate(int a, int b, Action<int> onResult)
{
int sum = a + b;
onResult(sum);
}
Calculate(5, 8, result => Console.WriteLine($"결과: {result}"));
4. 람다 사용 시 흔한 실수들
이벤트에서 언구독하지 않음
람다로 이벤트에 구독하면 보통 “핸들러 이름”이 없어서 나중에 간편하게 언구독하기가 어려워:
button.Click += (s, e) => Console.WriteLine("...");
button.Click -= ??? // 어떻게 이 람다를 전달하지? 저장해두지 않았다면 불가능해.
언구독이 필요하다면 람다를 변수에 저장해라:
EventHandler handler = (s, e) => Console.WriteLine("...");
button.Click += handler;
// ...그리고 나중에
button.Click -= handler;
이건 수명이 긴 객체에 특히 중요해: 언구독하지 않으면 참조가 남아 메모리 누수를 유발할 수 있어.
루프 변수 캡처
클래식한 문제: 루프 내부에서 이벤트를 구독할 때:
for (int i = 0; i < 3; i++)
button.Click += (s, e) => Console.WriteLine(i); // 모든 람다가 동일한 i를 "기억"할 수 있음
최신 C#에서는 foreach에 대한 동작이 수정된 경우가 있지만, for에서는 여전히 미묘한 점이 있을 수 있어 — 캡처된 변수에 항상 주의하자!
각 람다가 자기만의 값을 기억하게 하고 싶으면 로컬 복사본을 만들어라:
for (int i = 0; i < 3; i++)
{
int localI = i;
button.Click += (s, e) => Console.WriteLine(localI);
}
GO TO FULL VERSION