본문 바로가기
ASP.NET Core

ASP.NET Core] Asynchronous Programming - Async, Await

by Fastlane 2023. 4. 26.
728x90
반응형

출처 : https://code-maze.com/asynchronous-programming-with-async-and-await-in-asp-net-core/

Asynchronous Programming과 장점

비동기 프로그래밍을 사용하면, 병목현상을 피하고, 앱의 반응성을 개선할 수 있다. thread pool의 고갈이나 앱 blocking 없이 시스템 흐름을 실행할 수 있수 있도록 하는 프로그래밍 기술이다. 

 

async, await keywords를 사용하므로 앱의 실행속도를 향상시킬 수 있다는 잘못된 개념이 있다. database로부터 데이터를 가져오는데 3초가 걸린는 synchronous code가 있다면, asynchronous code에서 그 시간이 빨라지지는 않는다. 하지만, 얼마나 많은 동시다발적 requests에 대해 server가 처리할 수 있는지에 대해서는 성능 향상을 얻을 수 있다. async, await keywords를 사용하므로 앱의 확장성을 증가시킬 수 있다. 

 

우리가 API를 server에 배포했을 때, server는 특정 수의 requests를 처리할 수 있다. 그 이상의 requests를 받았을때, 앱의 성능은 전체적으로 악화된다. 개선하기 위해서, server를 추가할 수 있다. 이것을 수평적 scaling이라고 한다. 또 다른 방법으로 memory나 CPU power를 증가시킬 수 있다 이것을 수직적 scaling이라고 한다. async, await를 적절히 사용하면, API를 server level에서 수직적 확장성을 증가시킬 수 있다. 

 

ASP.NET Core에서 Synchronous, Asynchronous Requests 동작

  • Synchronous Requests

client가 database로부터 회사리스트를 가져오도록 API에 request를 보내면, ASP.NET Core는 thread pool로부터 request를 처리하는 thread를 할당한다. thread pool에 2개의 threads가 있다고 가정해보자. 하나의 thread는 사용중이다. 이제 두번째 request가 도착하였고 thread pool로부터 두번째 thread가 할당되었다. 이제 thread pool에는 남은 threads가 없다. 세번째 request가 도착하였고, 앞의 두 requests가 완료되어 threads가 thread pool로 되돌아올때까지 기다려야 한다. thread pool로 돌아와야 새로운 request에 thread를 할당할 수 있다.  

이 결과로, client는 app의 속도저하를 경험허게 된다. 너무 오래 기다리면 503 error page를 받게된다. 게다가 database로부터 다량의 데이터를 받으려고 하면, 시간이 길어지고 thread는 task가 끝나기만을 기다릴 수 밖에 없다. 따라서 추가로 들어오는 request를 그동안 처리할 수 없게 된다. 

  • Asynchronous Requests

비동기 요청은 상황이 전혀 달라진다. 

API에 request가 도착 시, 여전히 thread pool로 부터 thread는 필요하다. 이제 하나의 thread가 남았다. 하지만 비동기이기 때문에, request가 db처리시간이 걸리는 I/O point에 도착하면 thread는 thread pool로 돌아간다. 이제 thread pool에는 2개의 thread가 있다. db처리시간 후 결과값을 API로 반환할 때 thread pool은 response를 처리하도록 다시 thread를 할당한다.  

이것은 우리가 처리할 수 있는 requests 수가 더 많다는 것을 의미한다. thread를 막거나, 대기하도록 강요하지 않는다. 앱의 확정성을 개선할 수 있다. 

 

ASP.NET Core 앱에서 Async, Await Keywords 사용

async, await은 비동기프로그래밍의 key role이다. await keyword를 사용하기 위해서는 함수 선언에 async keyword를 추가해야 한다. 또한, async keyword를 사용하는 것만으로 함수가 비동기되는 것은 아니다. 함수는 여전히 동기이다. 

 

await keyword는 argument에 대하 비동기적으로 wait하게 동작한다. 이 동작에는 몇가지 단계가 있다. operation이 이미 완료되었는지 확인한다. 완료되었다면 동기적으로 함수가 진행된다. 아니라면 await keyword는 async 함수 실행을 잠시 멈추고, 미완료 task를 반환한다. 몇초 후, 동작이 완료되면 async method는 실행을 이어나간다. 

 

비동기 프로그래밍에는 3가지 return types이 있다. 

  • Task<TResult> : return value가 있는 async method
  • Task : return value가 없는 async method
  • void : event handler

아래 함수를 비동기로 수정해보자. 

public IEnumerable<Company> GetAllCompanies() =>
    _repoContext.Companies
        .OrderBy(c => c.Name)
        .ToList();
using Microsoft.EntityFrameworkCore;

public async Task<IEnumerable<Company>> GetAllCompanies()
{
    var companies = await _repoContext.Companies
        .OrderBy(c => c.Name)
        .ToListAsync();
    return companies;
}

async keyword를 추가했고, return type을 Task로 감쌌다. ToList 함수를ToListAsync함수로 변경해야 하며, ToListAsync함수는 Microsoft.EntityFrameworkCore namespace에서 나온다. 쿼리를 비동기적으로 실행한다. 

 

또는 아래와 같이 lambda expression을 사용할 수도 있다. 

using Microsoft.EntityFrameworkCore;

public async Task<IEnumerable<Company>> GetAllCompanies() => 
    await _repoContext.Companies
        .OrderBy(c => c.Name)
        .ToListAsync();

코드에서 비동기 프로그래밍을 사용한 경우, 전체 flow에 비동기 프로그래밍을 적용해야한다. GetAllCompanies함수가 CompaniesController에서 호출하면, action함수 또한 비동기 프로그래밍으로 수정해야 한다. 

[HttpGet]
public async Task<IActionResult> GetCompanies()
{
    var companies = await _repository.GetAllCompanies();

    var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);

    _logger.LogInfo("All companies fetched from the database");

    return Ok(companiesDto);
}

GetCompanies action에서 async operation 이후의 코드는 async 동작이 성공한 이후에 실행된다. 

 

하나의 method에서 여러개의 await keywords를 사용할 수 있다. 

private async Task GetCompaniesWithHttpClientFactory()
{
    var httpClient = _httpClientFactory.CreateClient();
    using (var response = await httpClient.GetAsync("https://localhost:5001/api/companies", HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();
        var stream = await response.Content.ReadAsStreamAsync();
        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
    }
}
반응형

Exception Handling with Asynchronous Operations

await 키워드는 비동기 동작의 성공여부를 확인한다. 따라서 우리가 해야할 것은 에러를 catch할 수 있도록 try catch안에 code를 감싸는 것이다. 

public async Task<IEnumerable<Company>> GetAllCompanies()
{
    throw new Exception("Custom exception for testing purposes");
    return await _repoContext.Companies
        .OrderBy(c => c.Name)
        .ToListAsync();
}
[HttpGet]
public async Task<IActionResult> GetCompanies()
{
    try
    {
        var companies = await _repository.GetAllCompanies();

        var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);

        _logger.LogInfo("All companies fetched from the database");

        return Ok(companiesDto);
    }
    catch (Exception ex)
    {
        _logger.LogError($"Exception occurred with a message: {ex.Message}");
        return StatusCode(500, ex.Message);
    }
}

500 error 코드와 함께 "Custom exception for testing purposes" 메시지를 반환한다. 

만약에 await 키워드를 빼고 실행한다면, task는 exception을 넘겨버린다. 그리고 함수가 진행되어, mapping 소스에서 에러가 발생한다. 

 

project의 모든 action에 try/catch block을 작성하길 원하지 않으면, Global Exception Handling을 적용할 수 있다. 

 

결론

  • async, await 키워드는 같이 사용한다. async 키워드만 사용한다고 method가 비동기화 되는 것이 아니다. 
  • return value가 없으면 Task를 return한다. 
  • 비동기 동작 검증을 위해, await keyword를 사용해야 한다. 
  • event handler를 사용하지 않는 이상 void return type은 사용하지 않는다. 
  • 비동기 동작의 결과를 얻기 위해, await 키워드를 사용해야 한다. Result property, Wait method는 사용하지 않는다. 
728x90
반응형

댓글