CodeGym /행동 /C# SELF /이벤트 구독 해제 ( -=) 및 메모리 ...

이벤트 구독 해제 ( -=) 및 메모리 누수

C# SELF
레벨 53 , 레슨 1
사용 가능

1. 도입

어떤 면에서 C#에서 이벤트에 구독하는 건 친구의 밈 구독을 신청하는 것과 비슷해: 구독하는 동안은 소식이 계속 오고, 네가 "그만"이라고 말하고 구독 해제하기 전까지는 멈추지 않아. 프로그래밍에서 이건 중요해 — 구독을 잊으면 단순히 "또 다른 밈"이 아니라 메모리 누수가 발생해!

예를 들어 앱에 어떤 폼(예: 설정 창)이 있다고 해보자. 이 폼은 메인 창의 이벤트에 구독해서 변경에 반응해. 사용자가 폼을 닫아 폼이 파괴된 줄 알지만, 핸들러가 여전히 등록되어 있다면 폼은 메모리에서 해제되지 않아! 발행자가 이벤트를 통해 해당 폼을 참조하고 있기 때문이지.

결론: 구독자가 발행자의 이벤트에 구독하고 구독 해제를 "잊으면", 발행자가 살아 있는 동안 가비지 컬렉터는 그 구독자를 메모리에서 제거하지 못해.

연산자 += 를 복습하고 -= 를 보여주자

  • += — 구독: 이벤트 호출 목록에 핸들러를 추가한다.
  • -= — 구독 해제: 호출 목록에서 핸들러를 제거한다.

대충 이렇게 보일 거야:


worker.WorkCompleted += handler; // 구독
worker.WorkCompleted -= handler; // 구독 해제

핸들러가 두 번 추가되었다면, 정확히 사라지게 하려면 그만큼 제거해야 해.

내부 동작을 조금 살펴보자

백그라운드에서 C#의 이벤트는 delegate 필드(또는 delegate 목록)이고, += 연산자는 사실상 Delegate.Combine을 호출하고, -=Delegate.Remove를 호출해. 구독한 객체는 참조 그래프의 일부가 돼. 그래서 구독을 잊으면 = 메모리 누수야.

2. 이벤트로 인한 메모리 누수: 작동 원리

클래식한 상황


class Window
{
    public event EventHandler Updated;

    public void SimulateUpdate()
    {
        // 흉내: 모든 구독자에게 알림 전송
        Updated?.Invoke(this, EventArgs.Empty);
    }
}

class SettingsForm
{
    public void OnWindowUpdated(object sender, EventArgs e)
    {
        Console.WriteLine("SettingsForm이 창 업데이트에 반응함");
    }
}

단계별로 보자:


var window = new Window();
var settingsForm = new SettingsForm();

window.Updated += settingsForm.OnWindowUpdated;

window.SimulateUpdate(); // SettingsForm이 반응함

// 사용자가 폼을 닫았다. 우린 그에 대한 모든 참조를 잃음:
settingsForm = null;

// 하지만 SettingsForm 객체는 window가 살아있는 한 가비지 컬렉터에 의해 제거되지 않는다,
// 왜냐하면 window.Updated가 여전히 OnWindowUpdated 메서드를 참조하고 있고,
// 그건 곧 SettingsForm 객체 자체를 참조하기 때문이야.

어떻게 해야 할까?
구독 해제해:


// 그러려면 핸들러나 객체에 대한 참조를 가지고 있어야 해:
window.Updated -= settingsForm.OnWindowUpdated;
settingsForm = null; // 이제 객체가 해제될 수 있어

표: 누가 누구를 참조하는가

동작 누가 참조를 보유하나 메모리 해제가 가능한가?
이벤트 구독 (+=) 발행자가 구독자를 참조 아니오, 발행자가 살아 있는 동안
구독 해제 (-=) 아니오 예, 모든 외부 참조가 제거된 후
구독 없음 아니오

3. 올바른 구독 해제 조직 방법

명시적으로 핸들러 제거하기

예: 창이나 폼이 닫힐 때 이렇게 할 수 있어:


class SettingsForm
{
    private readonly Window _window;

    public SettingsForm(Window window)
    {
        _window = window;
        _window.Updated += OnWindowUpdated;
    }

    public void Close()
    {
        _window.Updated -= OnWindowUpdated; // 구독 해제!
        // 여기서 닫기 관련 코드 (예: Dispose, GC.SuppressFinalize 등)
    }

    public void OnWindowUpdated(object sender, EventArgs e)
    {
        // 이벤트 처리
    }
}

SettingsForm이 "닫기" 버튼으로 파괴될 때는, 구독 해제를 수행하는 메서드(예: Close())를 호출하는 걸 잊지 않는 게 중요해.

IDisposable 인터페이스 사용

이벤트에 구독하고 자신의 라이프사이클을 제어하는 복잡한 객체에는 IDisposable을 구현하는 게 편리해. Dispose() 안에서 필요한 모든 구독 해제를 수행하면 돼.


class SettingsForm : IDisposable
{
    private readonly Window _window;

    public SettingsForm(Window window)
    {
        _window = window;
        _window.Updated += OnWindowUpdated;
    }

    public void OnWindowUpdated(object sender, EventArgs e)
    {
        // ...
    }

    public void Dispose()
    {
        _window.Updated -= OnWindowUpdated;
        // 여기서 다른 리소스들도 해제
    }
}

이제 SettingsForm을 using 블록 안에서 사용하거나, 수동으로 Dispose()를 호출하거나, 필요한 경우 파이널라이즈 가능한 타입에서 GC.SuppressFinalize를 사용해 리소스 해제를 자동화할 수 있어.

4. 람다 표현식과의 상호작용: 위험과 팁

익명 람다로 이벤트에 구독하고 그 람다를 변수에 저장하지 않으면, 나중에 구독 해제를 할 수 없어!


// 구독 — 익명 람다
window.Updated += (s, e) => Console.WriteLine("람다가 호출됨!");

// 이제 어떻게 구독 해제하지? — 못해!
window.Updated -= (s, e) => Console.WriteLine("람다가 호출됨!"); // 이건 다른 delegate야!

해결 방법?
람다를 delegate 변수에 저장해라:


EventHandler handler = (s, e) => Console.WriteLine("람다가 호출됨!");
window.Updated += handler;

// ... 이제 구독 해제가 가능해!
window.Updated -= handler;

5. 유용한 뉘앙스

객체 수명 주기와 이벤트의 특성

또 자주 보는 문제 중 하나는 두 개의 "장수" 객체 사이에서 이벤트를 통한 교차 참조가 발생하는 경우야. 예를 들어, 하나의 창이 다른 창의 이벤트에 구독했고 둘 다 활발히 사용되어 삭제되지 않으면 메모리는 계속 증가하게 돼.

권장: 누가 누구를 구독하는지, 언제 구독을 해제해야 하는지를 항상 의식하자. 구독의 수명주기가 발행자와 같다면 괜찮지만, 구독자가 발행자보다 짧게 살 수 있다면 명시적인 구독 해제를 구현해라.

보편적 규칙: "구독했으면 — 해제하자!"

  • 장수하는 발행자(예: 전역(singleton), 메인 창 등)를 대상으로 할 때는 구독자 쪽에서 항상 구독 해제를 구현해라.
  • 일회성 객체(예: 일회성 알림)나 구독자가 발행자보다 오래 사는 경우에는 조금 덜 엄격해도 되지만, 맥락을 잘 파악하자.
  • 수동으로 관리하기 싫다면 WeakEvent 같은 약한 이벤트 패턴이나 관련 프레임워크를 사용해라.

6. 구독 해제 시 흔한 실수

잘못된 구독 해제: 핸들러는 동일해야 함

구독을 해제할 때는 구독했을 때와 똑같은 핸들러를 지정하는 것이 매우 중요해. 그렇지 않으면 해제가 동작하지 않아.

실수 예:


window.Updated += settingsForm.OnWindowUpdated;
// ...
window.Updated -= new SettingsForm().OnWindowUpdated; // 동작하지 않아! 이건 다른 인스턴스와 다른 delegate야!

올바른 방법:


window.Updated -= settingsForm.OnWindowUpdated;

만약 익명 람다를 저장하지 않고 구독했다면, 그것은 해제할 수 없기 때문에 이미 다른 인스턴스의 delegate가 되어 버려서:


// 구독
window.Updated += (s, e) => Console.WriteLine("람다!");

// 구독 해제 시도 — 동작하지 않음!
window.Updated -= (s, e) => Console.WriteLine("람다!");

구독 해제를 "잊음"

구독 해제를 잊는 경우가 매우 흔한데, 특히 구독자가 발행자보다 오래 살거나 개발자가 이벤트 동작 원리를 완전히 이해하지 못할 때 그렇다. 결과적으로 구독자 객체들이 필요 이상으로 메모리에 남아 있게 되고, 메모리 누수와 성능 문제로 이어져.

2
과제
C# SELF, 레벨 53, 레슨 1
잠금
이벤트 구독과 구독 해제
이벤트 구독과 구독 해제
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION