아이템 8: 이벤트 호출 시에는 null 조건 연산자를 사용하라

이벤트를 발생시키기 위해서 이벤트를 정의하고, 이벤트를 호출하기만 하면 된다.

호출하는 순간 이벤트에 결합되어 있는 이벤트 핸들러가 호출될 것이기 때문이다.

멀티캐스트 델리게이트의 경우 이벤트에 결합된 이벤트 핸들러가 여러 개지만 내부적으로 이를 순차적으로 호출해줄 것이므로 복잡성이 외부로 드러나지 않는다.

하지만 만약 이벤트에 결합된 이벤트 핸들러가 없다면 어떻게 될까?

이벤트 핸들로가 결합되어 있는지 확인하는 코드를 추가하면 된다고 생각할 수 있지만, 이벤트 핸들러가 결합되어 있는지를 확인하는 코드와 이벤트를 발생시키는 코드 사이에 경쟁 조건이 발생할 가능성이 있다.

이는 null 조건 연산자를 사용하면 쉽게 해결된다.

실제로도 많이 사용중

먼저 가장 쉽게 사용되는 이벤트 예제코드를 본다.

public class EventSource
{
    private EventHandler<int> Update;
    private int counter;

    public void RaiseUpdates()
    {
        counter++;
        Update(this, value);
    }
}

우선 이 코드는 치명적인 문제가 있다.

Updated 이벤트에 이벤트 핸들러가 결합돼 있지 않다면 Update(this, value)에서 NullReferenceException이 발생한다.

따라서 대부분 이벤트를 발생시키는 코드는 다음과 같이 작성한다.

public void RaiseUpdates()
{
    counter++;
    if (Update != null)
    {
        Update(this, value);
    }
}

하지만 여전히 숨어있는 버그가 있는데 if문을 통해 Update가 null이 아닌지 확인했다고 해도 경쟁조건이 발생할 수 있다. 즉, 불변을 보장할 수 없다.

이벤트를 발생시키는 코드에서 코드를 수행하기 직전에 다른 스레드가 이벤트 핸들러의 등록을 취소했다고 한다면 이벤트핸들러는 null값을 가지게 되므로 예외가 발생한다.

이런 버그는 쉽게 발생하지 않으며 재현, 테스트하기도 쉽지 않다.

스레드 환경에 대한 이해와 발생 가능성에 대해 미리 인지해야 이런 버그를 무사히 넘어갈 수 있다.

public void RaiseUpdates()
{
    counter++;
    var handler = Update;
    if (handler != null)
    {
        handler(this, value);
    }
}

이 코드가 .NET C#에서 안전하게 이벤트를 발생시키는 권장 코드이다.

멀티스레드 환경에서도 안전하게 동작한다.

하지만 가독성 측면에서 좋은 코드라고 말할 수는 없다.

먼저 할당문을 통해 현재 이벤트 핸들러를 새로운 지역변수에 할당한다.

이 지역변수는 멀티캐스트 델리게이트를 포함할 수 있으며 내부적인 원래 이벤트의 이벤트핸들러를 그대로 가지고 있을 것이다.

즉, 얕은 복사를 진행한다.

만약 이때 다른 스레드에서 구독을 취소하면 기존 객체에 포함된 이벤트 필드의 내용은 수정되겠지만 복사된 내용의 지역변수는 변경되지 않는다.

따라서 정상적인 수행이 가능하다.

이제 드디어 null 조건 연산자를 사용해보자

public void RaiseUpdates()
{
    counter++;
    Update?.Invoke(this, value);
}

이 코드는 Update가 null이 아니라면 Update(this, value)를 호출하고 null이라면 아무것도 하지 않는다.

이 과정에서 이전 예제와 비슷해 보일 수 있지만 차이점은 null을 검사하고 수행하는 과정이 원자적으로 이루어진다는 점이다.

?.연산자를 사용하여 이벤트를 발생시킬 땐 이벤트 이름 뒤에 ()를 붙여 호출할 수 없으므로 ?.Invoke()를 사용해야 한다.

정리하자면 이 방식은 안전하고 단순하고 명확하다.

댓글남기기