paint-brush
비동기 이벤트 핸들러를 위한 간단한 안전망by@devleader
750
750

비동기 이벤트 핸들러를 위한 간단한 안전망

Dev Leader8m2023/02/14
Read on Terminal Reader
Read this story w/o Javascript

Async void는 두려운 비동기 EventHandler 설정을 허용하는 유일한 예외입니다. 이 기사에서는 자신의 코드에서 시험해 볼 수 있는 또 다른 솔루션을 제시하겠습니다. 우리는 그것이 어떻게 사용될 수 있는지에 관해 내 관점에서 찬반 양론을 다룰 것이므로 그것이 의미가 있는지 결정할 수 있습니다.
featured image - 비동기 이벤트 핸들러를 위한 간단한 안전망
Dev Leader HackerNoon profile picture

비동기 EventHandler에 대해 논의할 때 많은 사람들이 가장 먼저 생각하는 것은 이것이 우리가 허용하는 유일한 예외라는 것입니다. 두려운 비동기 무효 설정 .


이전에 이에 대해 글을 썼을 때 실제로 비동기 공백이 존재하도록 허용하는(나머지 머리카락을 뽑고 싶지 않은 상태에서) 솔루션을 탐색하고 있다는 사실에 기뻤습니다.


나에게 이것은 문제를 완전히 피하기 위한 솔루션을 제공하는 것보다 비동기 EventHandler를 극복하는 데 사용할 수 있는 몇 가지 영리한 트릭에 관한 것입니다.


하지만 기사에 많은 관심이 있었고, 정말 감사합니다. 일부 사람들은 비동기 EventHandler를 다른 방식으로 해결하고 싶다는 의견을 표명했습니다.


나는 이것이 좋은 점이라고 생각했기 때문에 async void를 수정하지 않지만 일부 과제를 해결하면서 이를 완전히 피할 수 있는 대체 접근 방식을 찾고 싶었습니다(내가 무엇을 했는지 보시겠습니까?). 비동기 EventHandler를 사용합니다.


이 기사에서는 자신의 코드에서 시험해 볼 수 있는 또 다른 솔루션을 제시하겠습니다. 귀하의 사용 사례에 적합한지 결정할 수 있도록 사용 방법과 관련하여 제 관점에서 장단점을 다룰 것입니다.


.NET 바이올린에서 상호작용 가능한 코드를 찾을 수도 있습니다. 여기 . 그렇지 않으면 다음을 수행할 수 있습니다. GitHub에서 코드를 확인하세요 로컬에서 복제하여 시험해보고 싶다면.

동반 영상!

여기를 클릭하세요 영상을 확인해보세요!

문제

비동기 EventHandler에서 직면하는 문제는 기본적으로 C#에서 구독할 수 있는 이벤트의 서명이 다음과 같다는 것입니다.


 void TheObject_TheEvent(object sender, EventArgs e);


그리고 이 서명 앞에 void를 지정하면 이벤트를 구독하기 위해 자체 처리기에서 void를 사용해야 한다는 점을 알 수 있습니다.


즉, 핸들러가 async/await 코드를 실행하도록 하려면 void 메서드 내부에서 wait를 수행해야 합니다. 이는 전염병처럼 피해야 하는 크고 무서운 비동기 void 패턴을 소개합니다.


그리고 왜? async void는 예외가 적절하게 발생하는 기능을 방해하고 결과적으로 엄청난 골칫거리를 유발할 수 있기 때문입니다.


이전 기사 호출 측면에서 창의력을 발휘할 수 있도록 하여 이 문제를 해결했지만…


  • 이벤트 호출을 제어하지 않는 객체에 대해 이에 대한 지원이 필요할 수 있습니다(즉, 즐겨 사용하는 UI 프레임워크에서 버튼의 클릭 이벤트에 연결하는 경우).


  • 어떤 사람들은 해당 솔루션 내부의 컨텍스트 사용을 해킹으로 간주합니다(저도 이에 동의하지 않습니다).


  • … 구체적으로, 이벤트 핸들러를 사용하면 비동기 EventHandler를 지원하기 위해 수행할 수 있는 몇 가지 더 간단한 트릭이 있습니다!


제 생각에는 단순한 것이 더 좋습니다. 따라서 async void에 대한 이전 기사를 읽고 실제로 목표가 EventHandler를 처리하는 것뿐이었다면 이 내용이 도움이 될 것입니다.

Try/Catch를 사용하여 비동기 EventHandler 해결

이전에 설명한 조건에 따라 예외 처리는 비동기 무효의 경계를 넘어 분해됩니다. 이 경계를 넘어 버블링해야 하는 예외가 있는 경우 즐거운 시간을 보내게 될 것입니다.


재미있게도, 왜 작동하지 않는지 디버깅하는 것을 즐기고 무엇이 문제인지 명확하게 알 수 없다면 정말 즐거운 시간을 보낼 수 있다는 뜻입니다.


그렇다면 이 문제를 해결하는 가장 쉬운 방법은 무엇입니까?


먼저 액세스할 수 있는 간단한 도구인 try/catch를 사용하여 예외가 이 경계를 넘지 못하도록 방지해 보겠습니다.


 objectThatRaisesEvent.TheEvent += async (s, e) => { // if the try catch surrounds EVERYTHING in the handler, no exception can bubble up try { await SomeTaskYouWantToAwait(); } catch (Exception ex) { // TODO: put your exception handling stuff here } // no exception can escape here if the try/catch surrounds the entire handler body }


위 코드에서 언급한 것처럼 이벤트 핸들러의 전체 본문 주위에 try/catch 블록을 배치하면 해당 async void 경계를 넘어 예외가 발생하는 것을 방지할 수 있습니다. 표면적으로는 매우 간단하며 이를 구현하는 데 화려한 것이 필요하지 않습니다.


장점:

  • 매우 간단합니다. 이해해야 할 복잡한 메커니즘이 없습니다.


  • 패키지가 필요하지 않습니다.


  • 이 작업을 수행하기 위해 이벤트를 발생시키는 클래스의 소유자일 필요는 없습니다. 이는 이 접근 방식이 WinForms 및 WPF UI 구성 요소를 포함하여 기존의 모든 이벤트 발생 개체에 대해 작동한다는 것을 의미합니다.


단점:

  • 이 작업을 수행하는 것을 기억해야 합니다... 어디에서나.


  • 시간이 지남에 따라 코드가 발전함에 따라 누군가 실수로 이벤트 핸들러의 try-catch 외부에 예외를 발생시킬 수 있는 논리를 작성할 수도 있습니다.


말하자면 이 솔루션은 정말 간단하지만 조금 더 나은 결과를 얻을 수 있다고 생각합니다.

비동기 EventHandler를 개선하기 위한 (약간) 더 멋진 접근 방식

처음에 제안된 솔루션을 통해 개선할 수 있다고 생각하는 한 가지 개선점은 예외 발생으로부터 안전해야 하는 비동기 EventHandler가 있음을 좀 더 명시적으로 만들 수 있다는 것입니다.


또한 이 접근 방식은 시간이 지남에 따라 코드 드리프트로 인해 문제가 있는 코드가 이벤트 핸들러 외부에서 실행되는 것을 방지합니다. 그러나 이를 수동으로 추가해야 한다는 사실은 다루지 않습니다.


코드를 확인해 봅시다:

 static class EventHandlers { public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Action<Exception> errorHandler) where TArgs : EventArgs => TryAsync<TArgs>( callback, ex => { errorHandler.Invoke(ex); return Task.CompletedTask; }); public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Func<Exception, Task> errorHandler) where TArgs : EventArgs { return new EventHandler<TArgs>(async (object s, TArgs e) => { try { await callback.Invoke(s, e); } catch (Exception ex) { await errorHandler.Invoke(ex); } }); } }


위의 코드는 예외가 비동기 무효 경계를 넘어가는 것을 방지하기 위해 말 그대로 정확히 동일한 접근 방식을 사용합니다. 우리는 단순히 이벤트 핸들러의 본문을 파악하려고 시도했지만 이제는 재사용할 수 있도록 명시적으로 전용 메서드로 묶었습니다.


이를 적용하면 다음과 같습니다.


 someEventRaisingObject.TheEvent += EventHandlers.TryAsync<EventArgs>( async (s, e) => { Console.WriteLine("Starting the event handler..."); await SomeTaskToAwait(); Console.WriteLine("Event handler completed."); }, ex => Console.WriteLine($"[TryAsync Error Callback] Our exception handler caught: {ex}"));


이제 작업할 비동기 작업 시그니처가 있는 대리자가 있음을 알 수 있으며, 그 안에 넣은 모든 항목은 앞서 본 도우미 메서드 내에서 시도/캐치할 수 있습니다.


다음은 예외를 올바르게 캡처하는 오류 핸들러 콜백을 보여주는 스크린샷입니다.


비동기 EventHandler에 대한 예제 프로그램의 출력

장점:


  • 여전히 매우 간단합니다. 래퍼 함수는 *약간* 더 복잡하지만 여전히 매우 기본적입니다.


  • 패키지가 필요하지 않습니다.


  • 이 작업을 수행하기 위해 이벤트를 발생시키는 클래스의 소유자일 필요는 없습니다. 이는 이 접근 방식이 WinForms 및 WPF UI 구성 요소를 포함하여 기존의 모든 이벤트 발생 개체에 대해 작동한다는 것을 의미합니다.


  • 핸들러를 이벤트에 연결할 때의 구문으로 인해 비동기 EventHandler를 사용하여 작업하려는 의도가 더 분명해졌습니다.


  • 결국 더 많은 예외를 발생시키는 코드 드리프트는 여전히 try/catch 내부에 래핑됩니다.


단점:


  • 당신은 아직도 이것을 연결하는 것을 기억해야 합니다!

비동기 EventHandler에 대한 결론

원래는 탐험을 시작했지만 비동기 무효를 처리하는 흥미로운 방법 , 예제가 비동기 EventHandler에 초점을 맞추고 있다는 점에서 독자 피드백은 유효했으며 확실히 더 간단한 방법이 있어야 합니다.


이 기사에서 우리는 비동기 EventHandler가 올바르게 작동하도록 하는 가장 간단한 방법이라고 주장할 수 있는 방법을 탐구했으며, 개선된 솔루션(제 생각에는)에는 사용 시 기억해야 할 단점만 있습니다.


한 논평자는 탐색할 수 있다고 제안했습니다. 관점 지향 프로그래밍 (AoP) 애플리케이션 전체에 이러한 종류의 동작을 주입하여 기억할 필요가 없도록 합니다.


몇 가지 컴파일 타임 AoP 프레임워크가 존재하지만 이를 독자 여러분을 위한 연습으로 남겨두겠습니다. 왜냐하면 이 프레임워크는 제가 후속 작업을 수행할 연습이기도 하기 때문입니다.