[ASP.NET MVC] AsyncController의 사용

2014. 3. 26. 19:47WEB/ASP.NET MVC

AsyncController는 비동기 Action Method를 작성할 수 있게 해줍니다. Action Method 내에서 실행되는 작업이 처리시간은 길지만 CPU사용시간은 적은 유형의 작업이라면 AsyncController를 활용하여 WebSite의 성능을 향상시킬 수 있습니다.

Thread Pool에서 Request가 처리되는 순서

ASP.NET은 Request를 처리하기 위해 사용되는 Thead들을 Thread Pool에서 관리하고 있습니다. 요청이 도착하면 Thread Pool에서 하나의 Thread가 추출되고 요청이 처리되게 됩니다. 요청처리를 동기방식으로 진행한다면 해당 Therad는 요청이 처리 완료될 때 까지 작업에 전념해야 합니다.

만약 Thread Pool이 충분히 크고 Request 수가 적다면 별 문제가 없을 것입니다. 하지만 동시요청의 수가 늘어나고 해당 요청들이 오랫동안 수행되어야 하는 작업들이라면 금방 Thread Pool의 Thread는 고갈 될 것입니다. 이러한 고갈현상을 Thread Starvation이라 합니다. 그 이후부터 요청들은 Request Queue에 쌓이게 됩니다. Request Queue도 모두 차면 이후 요청은 HTTP 503 (Server Too Busy)을 반환하게 됩니다.

동기 요청 처리

AsyncConttroller를 사용하면 위와 같은 Thread Starvation과 같은 문제를 해결할 수도 있습니다. 하지만, AsyncController를 이용해 비동기로 Action Method를 실행한다고 해서 처리에 소요되는 시간이 줄어드는 것은 아닙니다. 예를 들어서 동기방식으로 요청을 처리하는데 2초가 걸렸다면, 이 것을 비동기로 변경해도 여전히 2초가 소요될 수 있습니다. 하지만, 동기방식은 요청이 처리완료 될 때까지 Thread를 붙잡고 있는 반면, 비동기 방식은 Thread를 풀어줄 수 있기 때문에 Thread를 더 효율적으로 사용하며 결과적으로 더 많은 요청을 처리할 수 있게 됩니다.

AsyncController를 이용한 비동기 Action Method는 다음과 같이 처리됩니다.

  1. Request가 도착하면 웹서버는 Thread Pool에서 하나의 Thread를 가져옵니다. (Worker Thread라 합니다) 그리고 해당 Thread가 Request를 수행하도록 스케줄링 합니다.
  2. Worker Thread는 Request를 처리하게 되며, 비동기 작업이 시작하게 되면 Thread는 다시 Thread Pool로 되돌아가게 됩니다.
  3. 비동기 작업이 끝나면 ASP.NET는 다시 Thread Pool에서 아무 유휴 Thread를 하나 선택하여 남은 Request 작업을 진행하게 합니다.
  4. Request 처리가 끝나고 결과를 반환하면 Thread는 작업을 종료하고 다시 Thread Pool로 되돌아 갑니다.

비동기 vs 동기 Action Method 선택하기

동기와 비동기 Action Method 중 적합한 방법이 무엇인지 판단하기 위한 몇 가지 가이드라인은 다음과 같습니다.

일반적으로 다음과 같은 경우 동기방식이 적절합니다.

  • 작업이 짧거나 단순한 경우
  • 효율성 보다 단순함이 보다 중요한 경우
  • 작업이 주로 CPU의 자원을 소모하는 것이 아니라 Network 또는 HDD 등 다른 자원을 소모하는 경우 (CPU 자원이 필요한 작업을 비동기로 전환할 경우 오히려 Overhead 증가로 효율이 떨어지게 됩니다.)

일반적으로 다음과 같은 경우 비동기 방식이 적절합니다.

  • 작업이 Network-bound, I/O-bound 등의 CPU 자원이 불필요한 유형인 경우
  • 테스트 결과 비동기 방식으로 전환함으로써 병목을 해소하고 더 많은 요청의 처리가 가능하다고 판단될 경우
  • 병렬처리가 단순함 보다 중요할 경우
  • 오랜 시간이 걸리는 요청을 사용자가 중지할 수 있도록 기능을 제공하고 싶을 경우

위와 같은 기준은 참고 사항일 뿐 실제로 Async Action Method로 전환 시 성능상의 이점이 있는지는 실제로 테스트 해보고 판단해야 합니다. 일반적으로 대부분의 경우 동기 방식이 적절합니다. 왜냐하면, 비동기방식은 Context Change에 소모되는 Overhaed가 추가적으로 발생하므로 대부분의 경우 동기방식이 효율이 더 뛰어납니다.

하지만 몇몇 경우에 Async Action Method로 전환함으로써 IIS가 더 많은 동시 요청을 처리하게 할 수 있습니다.

Thread 설정에 대한 더 많은 정보를 원하시면 다음 문서를 확인해주세요. (http://blogs.msdn.com/b/tmarq/archive/2007/07/21/asp-net-thread-usage-on-iis-7-0-and-6-0.aspx)

비동기 Database 요청에 대해서 더 자세한 정보는 다음 문서에서 확인할 수 있습니다. (http://blogs.msdn.com/b/rickandy/archive/2009/11/14/should-my-database-calls-be-asynchronous.aspx)

Action Method를 Async Action Method로 전환하기

다음 메서드는 동기방식으로 동작하는 일반적인 Action Method 입니다.

public class PortalController: Controller
{
    public ActionResult News(string city) 
    {
        NewsService newsService = new NewsService();
        ViewStringModel headlines = newsService.GetHeadlines(city);
        return View(headlines);
    }
}

이 Action Method를 비동기 방식으로 전환하면 다음과 같습니다.

public class PortalController : AsyncController 
{
    public void NewsAsync(string city) 
    {
        AsyncManager.OutstandingOperations.Increment();

        NewsService newsService = new NewsService();
        newsService.GetHeadlinesAsync(city);
        newsService.GetHeadlinesCompleted += (sender, e) =>
        {
            AsyncManager.Parameters["headlines"] = e.Value;
            AsyncManager.OutstandingOperations.Decrement();
        };
    }

    public ActionResult NewsCompleted(string[] headlines) 
    {
        return View("News", new ViewStringModel
        {
            NewsHeadlines = headlines
        });
    }
}
  1. Controller Class가 아닌 AsyncController로 부터 상속하도록 합니다. AsyncController는 비동기 방식뿐만 아니라 기존 동기방식의 Action Method도 처리할 수 있습니다.
  2. 기존 Action이름에 접미사 Async를 붙인 메서드를 생성합니다. 반환형은 Void로 합니다. Async가 붙은 메서드는 요청이 도달했을 때 호출되는 메서드 입니다. 따라서 매개변수도 동기방식의 Action Method와 동일하게 작성하시면 됩니다.
    public void NewsAsync(string city) 
    {
        //...
    }
  3. Action이름에 접미사 Completed를 붙인 메서드를 생성합니다. 반환형은 ActionResult로 합니다. Completed로 끝나는 Method는 비동기 요청이 만료되었을 때 호출되는 CallBack Method로서 동작합니다. AsyncManager.Parameters Dictionary의 값이 매개변수로 전달됩니다.
    //AsyncManager.Parameters Dictionary의 값이 매개변수로 전달됩니다.
    public ActionResult NewsCompleted(string[] headlines) 
    {
        //...
    }
    위에서 선언한 2개의 Method는 하나의 Action을 처리하기 위해 사용됩니다. 즉 /Portal/News?city=Seattle로 요청할 경우 NewsAsync Action Method가 호출되며 NewsComplited Method를 통해서 결과가 반환됩니다. RedirectToAction(“Seattle”), RenderAction(“Seattle”) 등의 호출도 동일하게 처리됩니다.
  4. Async Action Method 내부의 작업 처리 방식을 비동기 방식으로 변경합니다. 위 예제에서는 동기방식 메서드 GetHeadlines()를 비동기 방식 메서드인 GetHeadlinesAsync()로 변경하였습니다.

    NewsService newsService = new NewsService();
    newsService.GetHeadlinesCompleted += (sender, e) => { /* ... */ };
    newsService.GetHeadlinesAsync(city);

    위에서 사용한 NewsService Class는 Event-based Asynchronous Pattern을 사용하여 구현된 비동기 Class입니다. Event-based Asynchronous Pattern에 대해 더 자세히 알고 싶으시면 다음 링크를 확인해주세요. (http://msdn.microsoft.com/en-us/library/wewwczdw%28v=vs.100%29.aspx)

  5. AsyncManager는 비동기 처리를 위해 사용하는 Class입니다. OutstandingOperations Property는 얼마나 많은 비동기 작업이 진행 중인지, 언제 비동기 작업들이 모두 끝날지 ASP.NET에게 알려주기 위해 사용하는 Property입니다. OustandingOperations.Increment()에 의해 1증가하며 OustandingOperations.Decrement()에 의해 1감소하게 됩니다. OutstandingOperations Property의 값이 0이 되면 ASP.NET는 NewsCompleted Method를 호출하게 됩니다.
    AsyncManager.OutstandingOperations.Increment();//1 증가
    AsyncManager.OutstandingOperations.Increment(3);//3 증가
    
    AsyncManager.OutstandingOperations.Decrement();//1 감소
    AsyncManager.OutstandingOperations.Decrement(3);//3 감소
  6. Completed Method에 넘겨줄 매개변수를 지정하기 위해서 AsyncManager.Parameters Dictionary를 이용합니다.
    AsyncManager.Parameters["headlines"] = e.Value;

비동기 Action Method의 특징에 대해 요약하면 다음과 같습니다.

  • 하나의 Action에 대해 Async, Completed 접미사를 가진 2개의 메서드를 작성해야 합니다.
  • View는 기존의 동기방식과 동일하게 작성하시면 됩니다. View에는 Async나 Completed 등의 접미사를 붙이지 않습니다.
  • 하나의 Controller안에 Async 접미사가 붙은 Action과 기존의 Action이 동시에 위치하면 AmbiguousMatchException이 발생하게 됩니다. 따라서 NewsAsync()와 News() 중 하나만 Controller에 정의해야 합니다.

Async Action Method에 Attribute 사용

Attribute는 Async Method에 Method에 적용해주세요. Completed 접미사가 붙은 Method에 적용할 경우 무시됩니다. AsyncController 또는 Async Action Method에 AsynctimeoutAttribute와 NoAsyncTimeoutAttribute를 사용할 수 있습니다.

  • AsyncTimeoutAttribute: Timeout 시간을 정합니다. 단위는 Milliesecond 입니다.
    [AsyncTimeout(5000)]
    [HandleError(ExceptionType=typeof(System.TimeoutException), View="Timeout")]
    public void NewsAsync(string city) 
    {
        //...
    }
    AsyncTimoueoutAttribute에서 지정한 시간이 초과할 경우 TimeoutException이 발생합니다. 따라서 Timeout Excpetion을 같이 처리해 주어야 합니다.
  • NoAsyncTimeutAttribute: Timeout 시간을 지정하지 않고자 할 때 사용할 수 있습니다.
    [NoAsyncTimeout]
    public class PortalController : AsyncController
    {
        //...
    }

BeginMethd/EndMethod Pattern 사용 시 주의사항

비동기 프로그래밍 패턴은 여러 가지가 있습니다. 위에서는 Event-based Asynchronous Pattern을 따른 비동기 프로그래밍에 대한 Async Action Method 구성방법을 다루었습니다. 만약 BeginMethod/EndMethod Pattern을 사용한 비동기 프로그래밍에 대해 Async Action Method를 구성하기 위해서는 몇 가지 주의할 부분이 있습니다. (BeginMethod/EndMethod Pattern에 대한 자세한 정보: http://msdn.microsoft.com/en-us/library/ms228963.aspx)

BeginMethod/EndMethod Pattern사용시 Callback Method는 ASP.NET MVC가 사용하는 Thread가 아닌 다른 Thread에서 실행될 수 있습니다. 이 경우 HttpContext.Current는 Null을 반환할 것이며 따라서 AsyncManager.Parameters나 AsyncManager.OutstandingOperations.Decrement()에 접근할 경우 경쟁상태(서로 하나의 자원을 동시에 차지하려고 하려는 상태)를 발생시키게 됩니다.

//BeginMethod/EndMethod Pattern을 통한 비동기 Class 사용 시
//Asnyc Action Method의 구성.
public void NewsAsync(string city) 
{
   AsyncManager.OutstandingOperations.Increment();
   NewsService newsService = new NewsService();
   newsService.BeginGetNews(city, t =>
   {
      //이렇게 구현할 경우 수행되는 Thread가 달라짐으로써 경쟁상태를 야기할 수 있다.
      AsyncManager.Parameters["news"] =
        newsService.EndGetNews(t);
      AsyncManager.OutstandingOperations.Decrement();
   }, null);
}

이러한 경쟁상태를 피하기 위해서는 Callback Method가 ASP.NET Thread에서 실행되도록 해야합니다. 이를 위해서 AsyncManager.Sync()가 제공되고 있습니다. 다음 예제와 같이 Callback Method를 Sync()를 통해 호출할 경우 Callback Method는 ASP.NET과 동일한 Thread에서 실행되게 됩니다.

하지만 Callback Method가 이미 ASP.NET Thread에서 실행되고 있는 경우에는 AsyncManager.Sync()를 호출해서는 안됩니다. Callback Method를 실행하는 Thread가 달라지는 경우는 비동기 작업이 수행되었을 때 발생하므로, 이를 해결하기 위해서는 다음과 같이 CompletedSynchronously 값이 false일 때만 Sync()를 통해 호출해야 합니다.

public void NewsAsync(string city)
{
    AsyncManager.OutstandingOperations.Increment();
    NewsService newsService = new NewsService();
    newsService.BeginGetNews(city, t =>
    {
        if(!t.CompletedSynchronously)
        {
            //Sync Method를 통해 Thread의 경쟁상태를 방지한다
            AsyncManager.Sync(() =>
            {
                AsyncManager.Parameters["news"] =
                    newsService.EndGetNews(t);
                AsyncManager.OutstandingOperations.Decrement();
            });
        }
        else
        {
            //만약 동기방식으로 작업이 처리되었다면 이미 ASP.NET Thread에서 수행되므로
            //AsyncManager.Sync()를 통하지 않고 작업을 진행한다.
            AsyncManager.Parameters["news"] =
                newsService.EndGetNews(t);
            AsyncManager.OutstandingOperations.Decrement();
        }
    }, null);
}

async, await를 통한 비동기 프로그래밍에 대한 AsyncController 구성

.NET Framework에서는 async, await를 통해 비동기 프로그래밍을 매우 간편하게 진행할 수 있습니다. 만약 async, await를 통해 비동기 프로그래밍을 구현하고 있다면 AsyncController를 구현하는 방법은 더 간단해집니다.

public class PortalController : AsyncController
{
    //비동기 Action Method
    public async Task<ActionResult> News()
    {
        ViewBag.Result = await GetHeadlinesAsync();
        return View();
    }

    //async, awiat를 통해 구현된 비동기 메서드
    public async Task<string> GetHeadlinesAsync()
    {
        HttpClient client = new HttpClient();
        return await client.GetStringAsync("http://sample.com/headlines");
    }
}

위와 같이 Action Method를 async keyword로 수식하고 반환형은 Task<ActionResult>로 변경하시면 간단히 비동기 Action Method로 실행되게 됩니다. 만약 2개 이상의 비동기 메서드를 처리해야 한다면 다음과 같이 Task.WhenAll()을 이용하시면 됩니다.

public class PortalController : AsyncController
{
    public async Task<ActionResult> News()
    {
        Task<string> headlineTask = GetHeadlinesAsync();
        Task<string> forecastTask = GetForecastAsync();
        
        await Task.WhenAll(headlineTask, forecastTask);

        ViewBag.Headlines = headlineTask.Result;
        ViewBag.Forecast = headlineTask.Result;
        return View();
    }

    public async Task<string> GetHeadlinesAsync()
    {
        HttpClient client = new HttpClient();
        return await client.GetStringAsync("http://sample.com/headlines");
    }

    public async Task<string> GetForecastAsync()
    {
        HttpClient client = new HttpClient();
        return await client.GetStringAsync("http://sample.com/forecast");
    }
}

References