Priv's Blog
C# EventHandler 중복 호출 방지법 본문
1. 이벤트를 통한 타 클래스 메서드 호출 방식
delegate, event, Action 등의 기능을 활용하여 타 클래스의 메서드를 구독해 두고 이벤트 형태로 호출하는 방법은 클래스 간의 의존도를 줄이고 불필요한 결집도를 피하기 위해 자주 사용되는 방법입니다.
개인적으로 델리게이트 타입을 정의하고, 델리게이트 타입의 이벤트 변수를 정의한 다음, 그 이벤트 변수에 구독할 메서드를 델리게이트 체인 방식으로 등록하는 방법이 손에 익어서 자주 사용합니다.
하지만 이러한 이벤트 호출 방법을 활용해도 '휴먼 에러'를 피하기에는 역부족입니다.
델리게이트 체인에 어떠한 메서드를 구독했다면, 필히 불필요한 상황에서는 해제하는 작업을 별도로 진행해주어야 합니다.
이 등록/해제 작업에 신경 써주지 않으면 어디서 어떻게 메모리가 낭비되는지, 왜 이벤트가 중복으로 호출되는지를 파악하기가 어려워집니다.
특히 이벤트는 등록, 호출, 해제가 각기 다른 곳에서 이루어질 수 있기 때문에 오류를 발생했을 때 원인을 찾는 것도 어려워서 각별히 신경 써야 합니다.
2. 이벤트 등록
이벤트를 등록할 때는 아래와 같이 일반적으로 += 기호를 사용합니다.
private void Init() {
this.gameControlColliderBoundsManager.OnBoundsCalculator += this.CalculateColliderBounds;
this.combinedBounds = new (this.transform.position, Vector3.zero); // Init
}
private void Awake() {
this.Init();
}
말 그대로 이벤트를 '등록'하는 것입니다.
대입이 아니라 '추가'의 개념으로 접근하는 것이 이해하기가 더 쉽습니다.
저렇게 이벤트에 등록되는 메서드들은 델리게이트 이벤트 변수를 이용해 Invoke( )되면 동시 다발적으로 이벤트 발생을 감지하여 클래스 자신이 가지고 있는(등록해 둔) 메서드를 호출해 실행합니다.
일반적으로 위 코드처럼 Awake( ) 메서드나 OnEnable( ) 또는 Start( ) 메서드처럼 게임이 처음 실행되어 초기화 단계를 거칠 때 등록을 하는 것이 관리하기가 쉽습니다.
3. 이벤트 호출
이벤트 호출은 이벤트를 어떻게 구현해 사용 중인지에 따라 조금씩 차이가 발생합니다.
MonoBehaviour에서 제공하는 UnityEvent와 C#에서 제공하는 delegate 방식이 그것인데, UnityEvent와 delegate는 서로 용도가 다르기 때문에 1:1로 명확한 비교는 불가능합니다.
UnityEvent의 경우, Unity 엔진의 에디터 상에서 이벤트를 등록할 수 있기 때문에 별도의 코드를 작성하지 않고 GUI 상에서도 이벤트를 제한적으로나마 관리할 수 있습니다.
즉, UnityEvent는 '게임 디자인' 단계에서 이벤트의 관리가 이루어집니다.
하지만 delegate 방식은 코드 상에서 이벤트의 등록, 호출, 해제 작업이 모두 이루어집니다.
또한 delegate 방식은 이벤트를 생성한 곳에서만 이벤트를 호출할 수 있기 때문에 UnityEvent 방식보다 좀 더 엄격하게 관리될 수 있습니다.
즉, delegate 방식은 '컴파일' 단계에서 이벤트의 관리가 이루어집니다.
여기서 다루는 것은 delegate 방식으로, 코드 상에서 이벤트를 생성하고 호출하고 해제하는 모든 작업을 설계하고 제어하는 환경을 조건으로 합니다.
이벤트를 호출할 때는 아래와 같이 이벤트 변수를 메서드처럼 호출하면 됩니다.
public delegate Bounds BoundsCalculatorHandler();
public event BoundsCalculatorHandler OnBoundsCalculator;
private void Init() {
this.combineColliderBounds = new(this.transform.position, Vector3.zero);
if (OnBoundsCalculator != null) {
this.combineColliderBounds.Encapsulate(this.OnBoundsCalculator());
}
this.combineCollider.center = (this.combineColliderBounds.center - this.combineCollider.transform.position);
this.combineCollider.size = this.combineColliderBounds.size;
}
private void Start() { // Manager
this.Init();
}
this.OnBoundsCalculator( ) 이벤트가 호출되었으므로, 앞서 살펴본 += 연산자로 체인에 등록된 메서드들이 호출될 것입니다.
3. 이벤트 해제
이제 여기서 문제가 발생합니다.
이벤트를 등록하고 호출하는 것까지는 문제가 없었는데, 해제는 어떻게 해야 할까요?
이벤트를 해제하지 않은 상태에서 휴먼 에러로 인해 이미 등록되어 있는 이벤트를 한 번 더 등록해 버리면 어떻게 될까요?
이벤트는 중복을 허용하기 때문에 이전에 등록된 이벤트가 다시 등록된다고 해서 에러를 발생시키거나 따로 경고를 발생시키지도 않습니다.
그냥 말 그대로 휴먼 에러, 로직 상의 문제로 취급해야 합니다.
(자칫 잘못하면 디버깅 난이도가 끔찍해진단 뜻입니다)
이러한 끔찍한 결말을 방지하기 위해 우리는 인간을 의심해야 합니다.
사실 어딜 가나 인간은 인간을 의심했습니다.
아래 내용은 하단의 출처를 참고하여 작성되었습니다.
3.1. 일단 해제하고 시작하기
초기화를 하기 전에 무조건 해제부터 합니다.
등록한 이벤트가 없다고요? 알게 뭐예요. 어차피 예외도 안 생기는데 말입니다.
private void Init() {
this.gameControlColliderBoundsManager.OnBoundsCalculator -= this.CalculateColliderBounds;
this.gameControlColliderBoundsManager.OnBoundsCalculator += this.CalculateColliderBounds;
this.combinedBounds = new (this.transform.position, Vector3.zero); // Init
}
private void Awake() {
this.Init();
}
getter와 setter 블록을 다루던 방법처럼 아예 이벤트 핸들러 내부에서 처리하는 방법도 있습니다.
하지만 저는 로직이 흘러가는 순서에 맞게 코드를 읽기 때문에 위 방식을 좀 더 선호합니다.
3.2. OnDisable( ) 메서드 활용하기
private void Init() {
this.gameControlColliderBoundsManager.OnBoundsCalculator += this.CalculateColliderBounds;
this.combinedBounds = new (this.transform.position, Vector3.zero); // Init
}
private void Awake() {
this.Init();
}
private void OnDisable() {
this.gameControlColliderBoundsManager.OnBoundsCalculator -= this.CalculateColliderBounds;
}
OnDisable( ) 메서드는 Unity 이벤트 메서드로, 오브젝트가 파괴되거나 스크립트 코드를 정리해야 할 때 활용할 수 있습니다.
컴파일이 끝난 이후 스크립트가 비활성화되면(Unity 에디터 상에서 스크립트 체크 박스를 해제하면) 호출되는 이벤트 메서드입니다.
이 메서드 안에 습관적으로 이벤트 해제 코드를 위와 같이 작성해 주면 오브젝트의 생성과 삭제에 맞춰 이벤트 등록과 해제가 이루어지도록 만들 수 있습니다.
4. 원하는 스타일을 선택해서 사용하기
위 방식들은 모두 구현 방식의 차이일 뿐, 목적은 같은 코드들입니다.
자신이 원하는 스타일 중에 하나를 골라서 사용해도 되며, 팀원들과 함께 스타일을 선정하여 사용하면 됩니다.
단일 이벤트를 호출할 때는 그냥 대입 연산자를 사용해도 되지만, 저는 += 연산자로 통일하는 것이 습관이 되어서 잘 사용하지 않는 방식입니다.
이것 또한 스타일의 차이라고 보고 있습니다.
수고하셨습니다!
'Dev. Study Note > Unity' 카테고리의 다른 글
움직임(물리)을 구현하자 (0) | 2024.10.15 |
---|---|
Unity Event vs Delegate Event (0) | 2024.10.06 |
SerializeField 속성과 변수 초기화, Null 참조 에러 (0) | 2024.07.28 |
세션 간의 데이터 지속성 (JsonUtility & NewtownSoft Json) (0) | 2024.06.14 |
OOP의 다형성, Overloading과 Overriding 그리고 global:: (0) | 2024.05.13 |