Microsoft MVP성태의 닷넷 이야기
닷넷: 2138. C# - async 메서드 호출 원칙 [링크 복사], [링크+제목 복사],
조회: 12876
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 12개 있습니다.)
.NET Framework: 698. C# 컴파일러 대신 직접 구현하는 비동기(async/await) 코드
; https://www.sysnet.pe.kr/2/0/11351

.NET Framework: 716. async 메서드의 void 반환 타입 사용에 대하여
; https://www.sysnet.pe.kr/2/0/11414

.NET Framework: 717. Task를 포함하지 않는 async 메서드의 동작 방식
; https://www.sysnet.pe.kr/2/0/11415

.NET Framework: 719. Task를 포함하는 async 메서드의 동작 방식
; https://www.sysnet.pe.kr/2/0/11417

.NET Framework: 731. C# - await을 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법
; https://www.sysnet.pe.kr/2/0/11456

.NET Framework: 737. C# - async를 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법
; https://www.sysnet.pe.kr/2/0/11484

.NET Framework: 813. C# async 메서드에서 out/ref/in 유형의 인자를 사용하지 못하는 이유
; https://www.sysnet.pe.kr/2/0/11850

닷넷: 2138. C# - async 메서드 호출 원칙
; https://www.sysnet.pe.kr/2/0/13405

닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
; https://www.sysnet.pe.kr/2/0/13421

닷넷: 2318. C# - (async Task가 아닌) async void 사용 시의 부작용
; https://www.sysnet.pe.kr/2/0/13884

닷넷: 2319. ASP.NET Core Web API / Razor 페이지에서 발생할 수 있는 async void 메서드의 부작용
; https://www.sysnet.pe.kr/2/0/13885

닷넷: 2321. Blazor에서 발생할 수 있는 async void 메서드의 부작용
; https://www.sysnet.pe.kr/2/0/13888




C# - async 메서드의 호출 원칙

async 메서드에 대해 지켜야 할 가장 기본적인 원칙은, "async all the way"라는 것입니다.

즉, async 메서드인 경우 "특별한 예외"가 없는 한 "await" 호출을 하면 됩니다. 다른 말로 하면, async 메서드를 일반 메서드처럼 호출하지는 말라는 의미입니다.

예를 들어, 대상 메서드가 async로 되어 있다면 (거의 무조건) await으로 호출합니다.

await Do();            

private async Task Do()
{
    Console.WriteLine("Do");
}

Visual Studio의 인텔리센스로 본다면 아래와 같이 "(awaitable)"이라고 붙은 메서드가 이에 해당합니다.

call_awatiable_1.png

저런 async 메서드를 "await" 없이 호출하는 것은 의미가 없습니다. 왜냐하면 await 호출을 가정하고 만든 것이기 때문에 단순히 동기 방식으로 호출하는 것은 자칫 프로그램의 흐름을 쉽게 이해하지 못하도록 만들어버립니다.




async 메서드를, 혹은 단순히 Task만 반환해도 awaitable을 만족하는데, 그런 메서드에 대해 동기식으로 호출해야 하는 상황이 분명히 있긴 합니다.

예를 들어, 네트워크로 로깅을 하는 코드를 가정했을 때, 로깅 자체의 지연을 프로그램의 성능에 넣고 싶지 않아 비동기로 만들었다고 가정해 보면 이렇게 호출하고 싶을 것입니다.

NLogAsync("test");

async Task NLogAsync(string text)
{
    // ...네트워크 비동기 쓰기...
}

하지만 직관성을 염두에 둔다면, 개발자들은 NLogAsync에 대해 "await ..." 호출을 하려고 들 것이므로 애당초 저 메서드를 async로 표현하지 않는 것이 더 좋습니다.

NLogAsync("test");

void NLogAsync(string text) // 자연스럽게 개발자는 이 메서드에 대해 await 호출 대상이라고 여기지 않음
{
    // ...네트워크 비동기 쓰기...
}




현재, (개인적으로) 유일하게 async 메서드를 await으로 직접 호출하지 않을 실용적인 사례는 "병렬" 처리일 때입니다. 예를 들어, DB 쓰기를 2개의 데이터베이스에 수행해야 한다고 가정해 보겠습니다.

await DBWriteAsync("[db1 연결문자열]");
await DBWriteAsync("[db2 연결문자열]");

async Task DBWriteAsync(string connectionString)
{
    // DB 비동기 쓰기 (1초 소요)
}

위와 같이 DBWriteAsync를 2번 수행하면 총 수행 시간은 2초가 걸립니다. 바로 이런 경우, (의존성이 없어 병렬로 수행할 수 있다면) 직접 호출한 후 Task.WhenAll 메서드와 곁들여 처리하면 됩니다.

Task task1 = DBWriteAsync("[db1 연결문자열]");
Task task2 = DBWriteAsync("[db2 연결문자열]");

Task.WhenAll(task1, task2);

위와 같이 해주면 DBWriteAsync 2번의 호출이 연이어 호출되므로 총 작업 시간은 1초에 끝나게 됩니다.

하지만, 저것 역시 엄밀히는 "async all the way" 방식에 부합하지 않습니다. 왜냐하면 "Task.WhenAll(...)" 호출은 그 내부에서 이뤄지는 비동기를 무시하고 바로 반환하기 때문에 역시 이후의 처리에서 코드 수행 순서가 복잡해집니다.

따라서 WhenAll까지 await 호출을 해줘야 비로소 진정한 비동기 처리의 완성이 되는 것입니다.

await Task.WhenAll(task1, task2); // task1, task2 완료 후에 다음 코드를 수행하도록 비동기 호출




실제로 제가 지금까지 종종 받아온 async/await 질문 중에는 async 메서드에 대해 그냥 (await 없이) 호출하면서 프로그램의 흐름이 잘 이해되지 않는다는 글들이 있었는데요, 올바른 사용법이 아니므로 엄밀히는 그 흐름을 굳이 이해하려고 애쓸 필요가 없습니다.

마침 아래의 질문도 이와 유사합니다.

Thread.Sleep(500), await Task.Delay(500), Task.Delay(500) 차이점이 궁금합니다.
; https://www.sysnet.pe.kr/3/0/5916

본문의 코드에 동기 호출과 비동기 호출을 함께 담아 예제를 구성하고 있는데요, 엄밀히는 이런 예제는 현실성이 없습니다.

즉, 동기/비동기로 나누는 경우 예제 코드가 아래와 같이 분명한 차이를 보여야 하는 것입니다.

// countDown을 동기로 만든 경우,

private void button1_Click(object sender, EventArgs e)
{
    countDown();
    countDown();
    MessageBox.Show("Done");
}

private void countDown()
{
    for (int i = 9; i >= 0; i--)
    {
        textBox1.Text += i.ToString();
        Thread.Sleep(500);
    }
}

// countDown을 비동기 및 병렬 처리로 수행하는 경우

private async void button1_Click(object sender, EventArgs e)
{
    var x = countDownAsync();
    var y = countDownAsync();
    await Task.WhenAll(x, y);
    MessageBox.Show("Done");
}

private async Task countDownAsync()
{
    for (int i = 9; i >= 0; i--)
    {
        textBox1.Text += i.ToString();
               
        await Task.Delay(500);
    }
}

결국, "3번" 상황은 그냥 잊어버리셔도 됩니다.

// "async all the way" 원칙에 맞지 않게 코딩한 경우

private async Task countDown()
{
    for (int i = 9; i >= 0; i--)
    {
        textBox1.Text += i.ToString();
        Task.Delay(500);
    }
}

현실적으로 저렇게 사용하는 경우는 없어야 합니다. 물론, 의도적으로 그렇게 하는 경우도 있겠지만... 다른 사람들의 코드 리딩을 돕기 위해서라면 저런 경우는 그냥 다음과 같이 만드는 것이 더 직관적입니다.

private void countDown()
{
    for (int i = 9; i >= 0; i--)
    {
        textBox1.Text += i.ToString();
        // Task.Delay(500);
        Thread.Sleep(500);
    }
}

비록 공부하는 단계에서 저 3가지 경우를 엮어서 1개의 코드베이스로 해석하고 싶겠지만, 현실적으로는 그다지 권장하지 않는 접근 방식입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/26/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12560정성태3/11/202117721개발 환경 구성: 549. ssh-keygen으로 생성한 PKCS#1 개인키/공개키 파일을 각각 PKCS8/PEM 형식으로 변환하는 방법
12559정성태3/11/202117969.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용 [2]파일 다운로드1
12558정성태3/10/202117040Windows: 192. Power Automate Desktop (Preview) 소개 - Bitvise SSH Client 제어 [1]
12557정성태3/10/202115257Windows: 191. 탐색기의 보안 탭에 있는 "Object name" 경로에 LEFT-TO-RIGHT EMBEDDING 제어 문자가 포함되는 문제
12556정성태3/9/202113543오류 유형: 703. PowerShell ISE의 Debug / Toggle Breakpoint 메뉴가 비활성 상태인 경우
12555정성태3/8/202116839Windows: 190. C# - 레지스트리에 등록된 DigitalProductId로부터 라이선스 키(Product Key)를 알아내는 방법파일 다운로드2
12554정성태3/8/202116399.NET Framework: 1027. 닷넷 응용 프로그램을 위한 PDB 옵션 - full, pdbonly, portable, embedded
12553정성태3/5/202116393개발 환경 구성: 548. 기존 .NET Framework 프로젝트를 .NET Core/5+ 용으로 변환해 주는 upgrade-assistant, try-convert 도구 소개 [4]
12552정성태3/5/202115850개발 환경 구성: 547. github workflow/actions에서 Visual Studio Marketplace 패키지 등록하는 방법
12551정성태3/5/202114212오류 유형: 702. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly. (2)
12550정성태3/5/202113962오류 유형: 701. Live Share 1.0.3713.0 버전을 1.0.3884.0으로 업데이트 이후 ContactServiceModelPackage 오류 발생하는 문제
12549정성태3/4/202115233오류 유형: 700. VsixPublisher를 이용한 등록 시 다양한 오류 유형 해결책
12548정성태3/4/202116366개발 환경 구성: 546. github workflow/actions에서 nuget 패키지 등록하는 방법
12547정성태3/3/202117027오류 유형: 699. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly.
12546정성태3/3/202116866개발 환경 구성: 545. github workflow/actions에서 빌드시 snk 파일 다루는 방법 - Encrypted secrets
12545정성태3/2/202119725.NET Framework: 1026. 닷넷 5에 추가된 POH (Pinned Object Heap) [10]
12544정성태2/26/202119935.NET Framework: 1025. C# - Control의 Invalidate, Update, Refresh 차이점 [2]
12543정성태2/26/202117912VS.NET IDE: 158. C# - 디자인 타임(design-time)과 런타임(runtime)의 코드 실행 구분
12542정성태2/20/202119581개발 환경 구성: 544. github repo의 Release 활성화 및 Actions를 이용한 자동화 방법 [1]
12541정성태2/18/202117147개발 환경 구성: 543. 애저듣보잡 - Github Workflow/Actions 소개
12540정성태2/17/202118261.NET Framework: 1024. C# - Win32 API에 대한 P/Invoke를 대신하는 Microsoft.Windows.CsWin32 패키지
12539정성태2/16/202118186Windows: 189. WM_TIMER의 동작 방식 개요파일 다운로드1
12538정성태2/15/202118649.NET Framework: 1023. C# - GC 힙이 아닌 Native 힙에 인스턴스 생성 - 0SuperComicLib.LowLevel 라이브러리 소개 [2]
12537정성태2/11/202119351.NET Framework: 1022. UI 요소의 접근은 반드시 그 UI를 만든 스레드에서! - 두 번째 이야기 [2]
12536정성태2/9/202118194개발 환경 구성: 542. BDP(Bandwidth-delay product)와 TCP Receive Window
12535정성태2/9/202117270개발 환경 구성: 541. Wireshark로 확인하는 LSO(Large Send Offload), RSC(Receive Segment Coalescing) 옵션
... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...