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#
습관처럼 작성하던 코드라도 매번 물음표를 던지는 것이 개발자의 숙명인 것 같다
끗~