Microsoft MVP성태의 닷넷 이야기
닷넷: 2138. C# - async 메서드 호출 원칙 [링크 복사], [링크+제목 복사],
조회: 4041
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 9개 있습니다.)
.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




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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13414정성태9/16/20233610디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법
13413정성태9/14/20233832닷넷: 2143. C# - 시스템 Time Zone 변경 시 이벤트 알림을 받는 방법
13412정성태9/14/20237140닷넷: 2142. C# 12 - 인라인 배열(Inline Arrays) [1]
13411정성태9/12/20233612Windows: 252. 권한 상승 전/후 따로 관리되는 공유 네트워크 드라이브 정보
13410정성태9/11/20235152닷넷: 2141. C# 12 - Interceptor (컴파일 시에 메서드 호출 재작성) [1]
13409정성태9/8/20233968닷넷: 2140. C# - Win32 API를 이용한 모니터 전원 끄기
13408정성태9/5/20233953Windows: 251. 임의로 만든 EXE 파일을 포함한 ZIP 파일의 압축을 해제할 때 Windows Defender에 의해 삭제되는 경우
13407정성태9/4/20233656닷넷: 2139. C# - ParallelEnumerable을 이용한 IEnumerable에 대한 병렬 처리
13406정성태9/4/20233648VS.NET IDE: 186. Visual Studio Community 버전의 라이선스
13405정성태9/3/20234041닷넷: 2138. C# - async 메서드 호출 원칙
13404정성태8/29/20233614오류 유형: 876. Windows - 키보드의 등호(=, Equals sign) 키가 눌리지 않는 경우
13403정성태8/21/20233434오류 유형: 875. The following signatures couldn't be verified because the public key is not available: NO_PUBKEY EB3E94ADBE1229CF
13402정성태8/20/20233517닷넷: 2137. ILSpy의 nuget 라이브러리 버전 - ICSharpCode.Decompiler
13401정성태8/19/20233780닷넷: 2136. .NET 5+ 환경에서 P/Invoke의 성능을 높이기 위한 SuppressGCTransition 특성 [1]
13400정성태8/10/20233614오류 유형: 874. 파이썬 - pymssql을 윈도우 환경에서 설치 불가
13399정성태8/9/20233523닷넷: 2135. C# - 지역 변수로 이해하는 메서드 매개변수의 값/참조 전달
13398정성태8/3/20234385스크립트: 55. 파이썬 - pyodbc를 이용한 SQL Server 연결 사용법
13397정성태7/23/20233874닷넷: 2134. C# - 문자열 연결 시 string.Create를 이용한 GC 할당 최소화
13396정성태7/22/20233660스크립트: 54. 파이썬 pystack 소개 - 메모리 덤프로부터 콜 스택 열거
13395정성태7/20/20233515개발 환경 구성: 685. 로컬에서 개발 중인 ASP.NET Core/5+ 웹 사이트에 대해 localhost 이외의 호스트 이름으로 접근하는 방법
13394정성태7/16/20233478오류 유형: 873. Oracle.ManagedDataAccess.Client - 쿼리 수행 시 System.InvalidOperationException
13393정성태7/16/20233667닷넷: 2133. C# - Oracle 데이터베이스의 Sleep 쿼리 실행하는 방법
13392정성태7/16/20233566오류 유형: 872. Oracle - ORA-01031: insufficient privileges
13391정성태7/14/20233587닷넷: 2132. C# - sealed 클래스의 메서드를 callback 호출했을 때 인라인 처리가 될까요?
13390정성태7/12/20233515스크립트: 53. 파이썬 - localhost 호출 시의 hang 현상
13389정성태7/5/20233561개발 환경 구성: 684. IIS Express로 호스팅하는 웹을 WSL 환경에서 접근하는 방법
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...