[Effective C#] 이벤트 호출 시 Invoke( )를 사용하는 이유

PikminProtectionAssociation·2024년 11월 9일

행성 탈출기

목록 보기
12/21

Unity에서서 C#으로 게임을 구현하다보면 델리게이트와 event를 사용할 일이 참 많다

처음에는 잘 이해가 되지 않았지만 여러 번 사용하면서 익숙해지니까 이만큼 편한게 또 없다!

난 주로 UI를 구현할 때 사용하는데, 클래스 간 결합도도 낮아지고 코드 가독성도 좋아서 애용한다


보통 아래와 같이 선언하고 사용하는 경우가 많다

public event Action OnTimeUpdated;

private void UpdateTime()
{
	OnTimeUpdated?.Invoke();
}

Q. 왜 Invoke로 이벤트를 호출하죠?
A.
그러게요..?

라고 하면 당연히 안되겠죠
그치만 모르는걸
그치만


그래서 오늘은 책 "Effective C#"에서 이야기하는 이벤트 호출에 대해 다뤄보겠다




이벤트 호출 시에는 null 조건 연산자를 이용하라

이벤트에 결합된 이벤트 핸들러가 없다면?

  • 단순하게 생각하면 이벤트 핸들러가 결합되어 있는지 확인하는 코드를 추가하면 될 것 같다
    → 하지만 이 방식은 이벤트 핸들러가 결합되어 있는지 확인하는 코드와 이벤트를 발생시키는 코드 사이에 경쟁 조건(race condition)이 발생할 가능성이 있다
  • 예전 방식
    public class EventSource
     {
     	private EventHandler<int> Updated;
      
      	public void RaiseUpdates()
        {
          	counter++;
            Updated(this, counter);
        }
         	
        private int counter;
     }
    • 위 코드에서 Updated 이벤트에 이벤트 핸들러가 결합되어 있지 않다면 NullReferenceException이 발생한다
    • 따라서 아래와 같이 코드를 수정할 수 있다
    public void RaiseUpdates()
     {
    	counter++;
      	if (Updated != null)
          	Updated(this, counter);
     }
    • 잘 해결된 것 같지만 여전히 문제가 있다
    • 멀티스레드 환경을 생각해보자. 만약 if문을 통해 이벤트 핸들러 결합 여부를 확인한 뒤, 이벤트를 호출하기 직전 다른 스레드에서 이벤트 핸들러의 등록을 취소한다면 어떻게 될까?
      → 역시 NullReferenceException이 발생한다

권장되는 이벤트 호출 방식

  • 아래 코드는 .NET과 C#을 이용하여 안전하게 이벤트를 발생시키기 위한 권장 코드이다
public void RaiseUpdates()
{
	counter++;
	var handler = Updated;
	if (handler != null)
		handler(this, counter);
}
  • 이벤트에 대한 할당 구문은 오른쪽 객체에 대한 얕은 복사본을 만든다
    → 이 복사본은 여러 개의 이벤트 핸들러가 포함된 리스트의 복사본을 생성한다
  • 다른 스레드가 이벤트에 대한 구독을 취소하더라도 지역변수 handler의 내용은 변경되지 않는다

그래서 왜 Invoke()를 사용하는데?

  • 위의 권장 코드는 잘 수행되지만 가독성이 떨어진다
  • 또한 이벤트를 발생시킬 때마다 해당 코드를 반복해서 사용하거나, 혹은 유사한 코드를 포함하는 private 메서드를 만들고 이를 이용해 이벤트를 발생시켜야 한다
    → 어우 귀찮아!!!
  • 그래서 우리는 null 조건 연산자를 사용하여 다음과 같이 간단하게 표현하기로 한다
public void RaiseUpdates()
{
	counter++;
    Updated?.Invoke(this, counter);
}
  • if 문을 사용하는 것과 비슷해보이지만 다른 점은 '?' 연산자의 왼쪽 식을 평가하고 메서드를 수행하는 과정이 원자적으로 수행된다는 것이다
  • '?.' 연산자를 이용할 때 이벤트 이름 뒤에 ( )를 붙여 호출할 수 없으므로 Invoke 메서드를 사용해야 한다
    • C# 컴파일러는 모든 델리게이트와 이벤트에 대하여 Invoke() 메서드를 타입 안정적 형태로 생성해준다
    • 즉 ( )를 이용하여 이벤트를 직접 발생시키는 코드와 완전히 동일하다
  • 이렇게 우리는 멀티스레드 환경에서도 안전하며 간결하게 이벤트를 호출할 수 있게 되었다!


참고 자료 : Effective C#



습관처럼 작성하던 코드라도 매번 물음표를 던지는 것이 개발자의 숙명인 것 같다

끗~

0개의 댓글