Study/C#

이벤트 호출 시 null 확인 코드의 중요성

13.d_dk 2021. 2. 8. 19:51
728x90
반응형

상황에 대한 설명

 이벤트를 정의하고 이벤트에 맞추어 실행하고자하는 함수를 추가 할 수 있다. 예를 들어 이벤트가 발생하였을 때 특정 데이터를 넘겨주는 동작을 하고자하는 경우가 있다. 이를 다이어그램으로 나타내면 아래의 그림과 같다. 

이벤트에 따라 데이터를 넘겨주는 예시의 다이어그램.

 

상황에 대한 코드 작성 1 : 이벤트를 호출하여 데이터를 받아보자

 이러한 경우 아래와 같이 코드를 작성하여 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
public class EventSource
{
   private EventHandler<int> Updated;
 
   public void RaiseUpdates()
   {
      counter++;
      Updated(this, counter);
   }
 
   private int counter;
}
 
cs

 

 위의 코드에서 바로 RaiseUpdates()를 바로 실행하는 경우 예외가 발생한다. 이는 Updated라는 이벤트에 이벤트에 맞추어 실행하고자하는 함수가 결합되어있지 않아 발생한 것이다. 여기서 Updated 이벤트는 함수가 결합되어 있지 않아 null값을 가지고 있다. 이러한 이벤트를 호출하는 경우, NullReferenceException의 예외가 발생한다. 만약, 처음에 설명한 예시와 같이 이벤트에 맞추어 실행할 함수가 등록이 되어 있다면, 예외를 발생하지 않고 등록한 함수를 실행한다.

Updated 이벤트를 호출하였을 때, 2가지 코드 실행 시나리오 예시

 

상황에 대한 코드 작성 2 : 이벤트가 null인지 확인하여 호출하자

 첫번째에서 본 예외 발생으로 인한 문제를 해결할 수 있는 방법이 있다. 이벤트에 함수가 추가되어 있는 경우를 확인하는 코드를 추가하여 해결할 수 있다. 수정한 이벤트를 호출하는 함수인 RaiseUpdates()는 아래와 같다.

1
2
3
4
5
6
public void RaiseUpdates()
{
   counter++;
   if(Updated != null)
       Updated(this, counter);
}
cs

 

 위와 같이 수정된 코드는 상대적으로 오류가 발생할 확률이 적다. 하지만 여전히 문제가 있을 수 있다. 처음 조건문을 통해 Updated가 null이 아님을 확인한 이후 이벤트를 수행하려할 때 다른 스레드가 이를 null로 변경하는 경우 처음과 같은 문제가 발생할 수 있다. 이러한 오류는 하나의 이벤트에 여러 부분에서 이벤트에 맞추어 실행하고자하는 함수를 추가 및 제거할 때 발생할 수 있다. 이러한 버그는 재현하기가 어려우며 문제를 해결하는 것도 쉽지 않다. 아래의 그림은 문제가 발생할 수 있는 상황을 순서에 따라 확인할 수 있는 다이어그램이다.

이벤트가 null임을 확인하였으나 문제가 발생할 수 있는 상황 예시의 다이어그램

 

상황에 대한 코드 작성 3 : 지역변수를 통한 이벤트 핸들러 할당과 null 확인

 이러한 문제를 보완하여 수정한 코드는 아래와 같다.

1
2
3
4
5
6
7
public void RaiseUpdates()
{
   counter++;
   var handler = Updated;
   if(handler != null)
       handler(this, counter);
}
cs

 

 위의 코드는 .NET과 C#에서 안전하게 이벤트를 발생시키기위해 권장되는 코드이다. 이 코드로 이벤트를 동작시키는 경우 멀티스레드 환경에서도 안전하게 동작할 수 있다. 이 코드에서는 새로운 지역변수(handler)에 이벤트 핸들러를 할당한다. 이 지역변수는 할당받은 이벤트의 이벤트 핸들러를 그대로 가지고 있다.(멀티캐스트 델리게이트를 포함하기 때문) 이러한 할당은 오른쪽 객체에 대한 얕은 복사본(shallow copy)를 생성하는 것과 같다. 지역변수에 할당 후 오른쪽 객체에 대해 다른 스레드가 이벤트의 구독을 취소를 발생시킬 수 있다. 이 경우 복사를 수행한 지역변수에는 이전에 복사하였던 이벤트 핸들러가 그대로 남아있어 이벤트를 사용할 수 있다. 이렇게 할당, 얕은 복사를 수행한 지역변수에 대하여 null 여부를 확인하고 이벤트를 수행하면 안전하게 이벤트 핸들러를 호출할 수 있다. 이를 다이어그램으로 표현하면 아래와 같다.

지역변수를 통한 이벤트 핸들러 할당과 null 확인 동작 다이어그램의 예시

 

상황에 대한 코드 작성 4

 여기서 더 나아가서 c# 6.0부터 지원하는 null 조건 연산자(null conditional operator)를 사용하면 더 간결하게 코드를 표현할 수 있다. 

1
2
3
4
5
public void RaiseUpdates()
{
   counter++;
   Updated?.Invoke(this. counter);
}
 
cs

 

 ?. 연산자는 연산자의 왼쪽을 먼저 평가하고 이 값이 null이 아니면 연산자의 오른쪽의 표현식을 동작하도록 되어 있다. 여기서 연산자의 왼쪽이 null이면 아무 동작도 수행하지 않는다. ?. 연산자를 통해 이벤트를 발생시키는 경우 이벤트 이름 뒤에 ()를 통해 호출할 수 없다. 따라서 Invoke 메서드를 통해 이벤트를 발생시킨다. C# 컴파일러는 모든 델리게이트와 이벤트에 대하여 Invoke() 메서드를 타입 안정적 형태로 생성해주므로 이 메서드를 호출하는 것은 ()를 이용하여 이벤트를 발생키는 것과 완전히 동일한다.

 

Reference

 effective C#; 강력한 C# 코드를 구현하는 50가지 전략과 기법 3판

반응형