이벤트를 발생하는 작업은 그리 어려운 부분이 없어 보이지만, 이벤트를 호출하는 경우에도 다양한 문제가 발생합니다.
namespace Delegate { class EventSource { private EventHandler<int> Updated; public void RaiseUpdates() { Counter++; Updated(this, Counter); } private int Counter; } }
우선, 위의 코드에서 Updated에 결합된 이벤트 핸들러가 없다면, NullReferenceException이 발생합니다. 이벤트 핸들러가 결합돼 있지 않다면 null 값을 가지기 때문입니다. 따라서 이벤트를 발생시키기 이전에 이벤트 핸들러가 유효한지 확인해야 합니다.
public void RaiseUpdates() { Counter++; if (null != Updated) Updated(this, Counter); }
이벤트 핸들러가 결합되어 있는지 확인 후, 호출하도록 위와 같이 수정하였습니다. 이렇게 수정하면 대부분의 경우 잘 동작하지만 여전히 문제는 있습니다.
if문을 호출하여 Updated 이벤트가 null이 아님을 확인했지만, 이벤트를 발생시키는 코드를 수행하기 직전에 다른 스레드가 이벤트 핸들러의 등록을 취소하게 된다면.... 다시 NullReferenceException 예외가 발생하게 됩니다.
흔히 나타나지 않는 문제이지만, 증상을 재현하기도 쉽지 않고 이런 버그는 분석도 어렵고 고치기도 까다롭습니다. 코드 상의 문제가 쉽게 찾아지지 않기 때문입니다.
public void RaiseUpdates() { Counter++; var handler = Updated; if (null != handler) handler(this, Counter); }
위 예제가 .NET과 C#을 이용하여 안전하게 이벤트를 발생시키는 권장 코드입니다.
이벤트 핸들러를 새로운 지역 변수에 할당하면, 이 지역변수는 원래 이벤트에서 이벤트 핸들러의 얕은 복사본을 생성합니다.
멀티캐스트 델리게이트를 포함할 수 있고, 내부적으로 원래 이벤트의 이벤트 핸들러 목록을 그대로 가지고 있습니다.
만약 다른 스레드가 이벤트에 대한 구독을 취소하더라도 기존 객체에 포함된 이벤트 필드의 내용은 수정되지만, 복사된 지역 변수의 내용은 변하지 않습니다.
위의 코드를 null 조건 연산자를 사용하면 보다 더 간단한 작성도 가능해집니다.
public void RaiseUpdates() { Counter++; Updated?.Invoke(this, Counter); }
null 조건 연산자(?.)는 왼쪽을 평가하여 이 값이 null이 아닌 경우에만 연산자 오른쪽의 표현식을 실행시킵니다. 만약 연산자 왼쪽이 null일 경우, 예외도 발생하지 않으면 아무 작업 없이 다음 단락으로 이동합니다.
연산자의 왼쪽을 평가하고 메서드를 수행하는 과정이 원자적으로(한 번에) 수행된다는 것입니다.
위와 같이 이벤트 호출에 null 조건 연산자를 사용하면 멀티스레드 환경에서도 안전할 뿐 아니라 훨씬 간결하게 작성할 수 있다.
null 조건 연산자 : https://loveme-do.tistory.com/7
참 고 : Effective C#
'책 > Effective C#' 카테고리의 다른 글
Item 10. 베이스 클래스가 업그레이드된 경우에만 new 한정자를 사용하라. (0) | 2019.04.08 |
---|---|
Item 9. 박싱과 언박싱을 최소화하라. (0) | 2019.04.05 |
Item 7. 델리게이트를 이용하여 콜백을 표현하라. (0) | 2019.03.27 |
Item 6. nameof( ) 연산자를 적극 활용하라. (0) | 2019.01.31 |
Item 5. 문화권별로 다른 문자열을 생성하려면 FormattableString을 사용하라. (0) | 2019.01.30 |