Silverlight를 설치하려면 여기를 클릭합니다.*
Korea 대한민국변경|Microsoft 전체 사이트
MSDN
|개발자 센터
MSDN Home   MSDN Home

Extreme ASP.NET
비동기 웹 파트


이 문서에 사용된 코드 다운로드: Onion2006_07.exe (176KB)

ASP.NET 2.0의 포털 인프라 덕분에 연결 가능한 웹 파트 컬렉션을 사용하여 사용자 지정 가능한 웹 사이트를 구축하는 일이 상당히 쉬워졌습니다. 이 모델은 매우 유연하므로 사용자는 웹 페이지 어디에나 웹 파트를 배치하고 자유롭게 웹 사이트를 사용자 지정할 수 있습니다. 그러나 사전에 어떤 구성 요소가 함께 사용되는지, 그리고 이에 따라 각 구성 요소에 어떠한 특정 데이터 검색 최적화를 사용해야 하는지를 알 수 없기 때문에 이러한 장점이 오히려 사용자 환경의 품질을 떨어뜨리는 비효율성으로 이어질 수 있습니다.

일반적인 포털 사이트에서 가장 흔히 볼 수 있는 비효율성은 여러 웹 파트가 네트워크에서 동시에 데이터 요청을 보낼 때 발생합니다. 이러한 요청은 일반적으로 서로 독립적이며 병렬로 처리할 수 있지만 웹 서비스에 대한 것인지 또는 원격 데이터베이스에 대한 것인지에 관계없이 각 요청은 페이지를 처리하는 데 필요한 전체 시간을 늘리게 됩니다.

다행히도 ASP.NET 2.0에는 비동기 웹 서비스 호출 및 비동기 데이터베이스 액세스를 조합한 비동기 페이지 모델이 도입되어 여러 독립적인 웹 파트가 병렬로 데이터를 수집하는 방법으로 포털 페이지의 응답 시간을 크게 향상시켜 줍니다. 여기에서는 비동기적으로 데이터 검색을 수행하여 이를 포함하고 있는 포털 페이지의 응답성 및 확장성을 향상시켜 주는 웹 파트를 만드는 기법을 살펴보겠습니다.


웹 파트 정체

먼저 그림 1에 있는 포털 페이지를 살펴보겠습니다. 이 샘플의 포털 페이지에는 각기 다른 원본에서 데이터를 검색하는 웹 파트 4개가 있습니다. 이 샘플 응용 프로그램의 전체 소스는 MSDNMagazine 웹 사이트에서 다운로드할 수 있으며 이 칼럼을 읽어가면서 응용 프로그램을 검토하기를 권장합니다. 이 샘플에서 웹 파트 3개는 웹 서비스에서 데이터를 검색하며 반환하기 전에 의도적으로 3초간 대기합니다. 4번째 웹 파트는 SQL Server 데이터베이스에 대한 ADO.NET 쿼리를 수행하며 역시 반환하기 전에 3초간 대기합니다. 이것은 다소 과장된 예이긴 하지만 전혀 현실성이 없는 것도 아닙니다.

그림 1 샘플 포털 페이지
그림 1 샘플 포털 페이지

샘플 응용 프로그램의 각 웹 파트는 사용자 컨트롤로 빌드되었으며 데이터 검색의 결과를 표시할 컨트롤에 바인딩합니다. 예를 단순하게 유지하고 웹 파트를 비동기화하는 데 초점을 맞추기 위해 각 컨트롤의 코드와 태그는 최소한으로만 사용했습니다.

다음은 NewsWebPart.ascx 사용자 컨트롤 파일입니다.

<%@ Control Language="C#" 
           AutoEventWireup="true" 
           CodeFile="NewsWebPart.ascx.cs"  
           Inherits="webparts_
           NewsWebPart" %>

   <asp:BulletedList ID="_newsHeadlines" 
           runat="server">
   </asp:BulletedList>

그리고 다음은 뉴스 헤드라인 샘플 웹 파트에 대한 코드 숨김 파일입니다.

public partial class webparts_NewsWebPart : UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        PortalServices ps = new PortalServices();
        _newsHeadlines.DataSource = ps.GetNewsHeadlines();
        _newsHeadlines.DataBind();
    }
}

그림 3 순차적 웹 파트 처리
그림 3 순차적 웹 파트 처리
샘플 뉴스 헤드라인을 검색하기 위해 웹 서비스와 어떻게 상호 작용하는지를 살펴보십시오. 주식 시세 웹 파트 및 일기 예보 웹 파트 역시 같은 웹 서비스를 사용하여 데이터를 검색하는 유사한 방법으로 구현됩니다. 비슷한 형태인 그림 2는 판매 보고서 샘플 웹 파트를 위한 SalesReportWebPart.ascx 사용자 컨트롤 파일 및 코드 숨김 파일입니다. 컨트롤에서 ADO.NET을 사용하여 데이터베이스에서 판매 데이터를 검색하는 방법과 이 데이터로 GridView 컨트롤을 채우는 방법을 살펴보십시오.

샘플 포털 페이지를 실행하면 문제가 분명하게 드러납니다. 요청을 처리하는 데 12초 이상이 소요되는데, 이 정도 대기 시간은 대부분의 사용자가 응용 프로그램 사용을 피하게 하는 데 충분합니다. 그림 3에서는 페이지가 실행될 때 요청의 실행 경로를 추적하여 이와 같은 긴 대기 시간이 발생하는 원인을 보여 주고 있습니다. 각 웹 파트는 페이지 컨트롤 계층의 다른 컨트롤과 마찬가지로 페이지의 컨트롤 계층에 정의된 순서에 따라 차례로 로드됩니다. 이러한 처리가 순차적이기 때문에 각 웹 파트는 데이터를 요청하고 응답을 준비하는 작업을 시작하기 전에 계층의 이전 파트가 완료될 때까지 대기해야 하는 것입니다. 데이터 검색을 수행하는 각 작업에 의도적으로 3초의 대기 시간을 추가했으므로 응답이 완료될 때까지 12초 이상이 걸릴 것임을 쉽게 알 수 있습니다. 각 웹 파트는 서로 완전히 독립적으로 데이터 검색을 수행합니다. 중요한 사실은 이러한 검색을 병렬로 수행한다면 응답 시간을 75%까지 단축할 수 있다는 것입니다. 바로 이것이 여기에서의 목표입니다.


비동기 웹 액세스

이 예에서 웹 파트 3개는 웹 서비스를 사용하여 데이터를 검색하며 하나는 ADO.NET을 사용하여 데이터베이스를 액세스합니다. 웹 서비스 설명 언어 도구인 WSDL.exe(또는 Visual Studio 2005 웹 서비스 참조 추가 도구)에서 비동기적으로 웹 메서드 호출을 수행하기 위해 생성하는 웹 서비스 프록시 클래스에서 매우 훌륭한 지원을 제공하므로 먼저 비동기적으로 웹 서비스 호출을 수행하는 방법부터 살펴보겠습니다.

ASP.NET 2.0에서 웹 서비스 프록시 클래스를 생성하면 실제로는 동기적인 방법 한 가지와 비동기적인 방법 두 가지로 특정 메서드를 실행하는 모두 세 가지의 다른 방법을 생성합니다. 예를 들어 웹 파트에서 사용하는 웹 서비스 프록시에서는 GetNewsHeadlines 웹 메서드를 호출하기 위해 다음과 같은 메서드를 사용할 수 있습니다.

   public string[] GetNewsHeadlines()
   
   public IAsyncResult BeginGetNewsHeadlines(
       AsyncCallback callback, object asyncState) 
   public string[] EndGetNewsHeadlines(       IAsyncResult asyncResult) 

   public void GetNewsHeadlinesAsync() 
   public void GetNewsHeadlinesAsync(       object userState)
   public event
       GetNewsHeadlinesCompletedEventHandler 
       GetNewsHeadlinesCompleted;

첫 번째 메서드인 GetNewsHeadlines는 표준 동기 메서드입니다. 다음 BeginGetNewsHeadlines 및 EndGetNewsHeadlines 두 메서드는 메서드를 비동기적으로 호출하는 데 사용할 수 있으며 표준 IAsyncResult 인터페이스를 통해 원하는 수만큼 .NET의 비동기 메커니즘으로 연결될 수 있습니다.

그러나 이 시나리오에서 사용할 가장 흥미로운 메서드는 마지막 메서드인 GetNewsHeadlinesAsync입니다. 이 메서드를 사용하기 위해서는 비동기 호출의 결과를 캡처하기 위해 특별히 생성된 프록시 클래스의 이벤트로 대리자를 등록해야 합니다. 이 예에서는 GetNewsHeadlinesCompleted 이벤트입니다. 메서드의 반환 값을 포함하여 메서드 구현에서 손쉽게 결과를 추출할 수 있도록 대리자 서명은 강력한 형식으로 되어 있습니다.

그림 4에서 보여 주고 있는 것처럼 이 이벤트 기반 비동기 메서드를 사용하여 헤드라인 뉴스 웹 파트의 웹 메서드 호출을 간단하게 비동기적으로 다시 작성할 수 있습니다. 먼저 프록시 클래스의 GetNewsHeadlinesCompleted 이벤트에 대해 대리자를 등록한 다음 GetNewsHeadlinesAsync 메서드를 호출했습니다. completed 이벤트에 대해 구독한 메서드 구현에서는 클라이언트에 표시를 위해 웹 메서드 호출의 결과를 BulletedList에 바인딩했습니다. 한 가지 추가 고려 사항은 웹 파트가 Async="true" 특성이 설정된 페이지에 있는 경우에만 이러한 비동기 메서드가 작동한다는 것인데 이러한 사항은 포함하는 페이지의 IsAsync 속성을 조사함으로써 프로그래밍 방식으로 확인할 수 있습니다. 웹 파트가 있는 페이지가 비동기가 아닌 경우 그림 4와 같이 표준 동기 바인딩에 의지해야 합니다.

비동기 웹 파트가 비동기적으로 데이터 검색을 수행하기 위해서는 Async 특성이 true로 설정된 웹 페이지에 배치되어야 합니다. 따라서 포털 페이지의 Page 지시문을 다음과 같이 수정했습니다.

<%@ Page Language="C#" AutoEventWireup="true"  Async="true" %>

웹 서비스를 사용하여 비동기적으로 데이터를 검색하도록 다른 두 웹 파트를 업데이트하자 포털 페이지의 반응 속도가 크게 개선되었습니다. 실제로 파트가 로드되는 순서에 따라서는 약 3초 정도에 클라이언트에 표시될 수도 있습니다. 판매 웹 파트가 가장 먼저 로드되는 경우에도 약 6초 정도가 소비됩니다. 판매 보고서 웹 파트는 아직 순차적으로 데이터베이스를 액세스하지만 다른 세 웹 파트는 비동기적으로 해당 웹 서비스 호출을 수행하므로 기본 요청 스레드는 이제 웹 파트가 완료되기를 기다릴 필요가 없습니다. 물론 근본적으로는 불필요하게 순차적으로 차단되는 일 없이 클라이언트가 웹 서비스 및 데이터베이스 기반 웹 파트를 사용할 수 있도록 모든 I/O 바인딩 작업을 비동기적으로 처리하기를 원할 것입니다.

I/O 바운드 작업을 비동기 I/O 요청으로 바꾸는 다른 이유는 기본 스레드를 스레드 풀로 양도하여 다른 요청에 서비스를 제공할 수 있도록 하기 위해서입니다. 현재는 판매 보고서 데이터베이스 쿼리를 완료한 다음에만 스레드를 양도하고 있는데 이것은 다른 요청에 서비스를 제공하는 데 사용할 수 있는 스레드 풀이 의미 없이 3초간 점유되어 있음을 의미합니다. 데이터에 대한 마지막 I/O 바운드 요청까지 비동기적으로 처리하도록 한다면 이 페이지는 모든 비동기 I/O 요청을 스풀링하는 동안만 요청 스레드를 사용하고 곧바로 풀을 반환할 수 있게 됩니다.


작동 원리

비동기 프로그래밍에 경험이 있다면 웹 서비스 호출에 대한 작은 변경만으로는 충분하지 않을 수 있다는 느낌이 있을 것입니다. IAsyncResult 인터페이스를 수정하지도 않았으며 포함하는 페이지에 작업을 등록하거나 다른 기법을 사용하여 비동기 작업을 수행하고 있음을 알린 것도 아니지만 이러한 모든 사항이 기대한 대로 작동되는 것으로 보입니다.

이러한 비밀은 비동기 메서드의 웹 서비스 프록시 클래스의 구현과 Microsoft .NET Framework 2.0에 도입된 AsyncOperationManager라고 하는 도우미 클래스에 숨겨져 있습니다. 프록시 클래스의 GetNewsHeadlinesAsync 메서드를 호출할 때 클래스는 프록시 클래스가 파생되는 SoapHttpClientProtocol 기본 클래스의 InvokeAsync라고 하는 내부 도우미 메서드에 대해 호출을 매핑합니다. InvokeAsync는 AsyncOperationManager의 정적 CreateOperation 메서드를 호출하여 비동기 작업을 등록하는 작업과 WebRequest 클래스의 BeginGetRequestStream 메서드를 호출하여 요청을 비동기적으로 시작하는 두 가지 중요한 임무를 수행합니다. 이 시점에 호출이 반환되고 페이지의 자체 주기 처리가 시작되지만 페이지가 Async="true" 특성으로 표시되어 있기 때문에 PreRender 이벤트를 통해서 계속 요청을 처리하며 그런 다음 요청 스레드를 스레드 풀에 반환합니다. 비동기 웹 요청이 완료되면 I/O 스레드 풀에서 가져온 별도의 스레드에서 프록시의 completed 이벤트에 대해 등록한 메서드를 호출합니다. 마지막 비동기 작업이 완료된 경우에는(AsyncOperationManager의 동기화 컨텍스트를 사용하여 추적) 페이지는 콜백되고 요청은 PreRenderComplete 이벤트부터 시작하여 남겨진 부분의 처리가 계속 진행됩니다. 그림 5는 비동기 페이지의 컨텍스트 내에서 비동기 웹 요청을 사용한 경우의 전체 수명 주기를 보여 줍니다.

그림 5 비동기 페이지 내의 비동기 웹 요청
그림 5 비동기 페이지 내의 비동기 웹 요청

AsyncOperationManager는 여러 다른 환경에서 비동기 메서드 호출을 관리하는 데 사용되도록 설계된 클래스입니다. 예를 들어 Windows Forms 응용 프로그램 내에서 웹 서비스를 비동기적으로 호출했다면 AsyncOperationManager 클래스에도 연결되었을 것입니다. 각 환경의 차이점은 AsyncOperationManager와 연결된 SynchronizationContext에 있습니다. ASP.NET 기반 응용 프로그램의 컨텍스트 내에서 실행되는 경우 SynchronizationContext는 AspNetSynchronizationContext 클래스의 인스턴스로 설정됩니다. 여기에서 주 목적은 언제 모든 요청이 완료되어 페이지 요청의 처리를 다시 시작할 수 있는지 알기 위해 얼마나 많은 수의 비동기 요청이 대기 중인지 추적하는 것입니다. 반면에 Windows Forms 기반 응용 프로그램의 컨텍스트 내에서 실행되는 경우 SynchronizationContext는 WindowsFormsSynchronizationContext 클래스의 인스턴스로 설정됩니다. 주 목적은 백그라운드 스레드에서 UI 스레드로 호출 마샬링을 수월하게 하기 위한 것입니다.


비동기 데이터 액세스

이제 마지막 웹 파트를 비동기적으로 만드는 것과 ADO.NET을 사용한 비동기적 데이터 검색 수행과 관련한 일반적인 문제로 돌아가겠습니다. 아쉽게도 비동기 데이터 검색을 수행하는 데는 웹 서비스 프록시에서 제공하는 것과 대응되는 간단한 비동기 메커니즘이 없습니다. 따라서 마지막 웹 파트가 비동기 작업에 동참하도록 하는 데는 좀 더 많은 작업이 필요합니다. SqlCommand 클래스의 새로운 비동기 메서드와 ASP.NET의 비동기 작업 기능을 활용하여 작업을 수행할 수 있으며 SqlCommand를 사용하면 다음과 같은 메서드 중 하나를 사용하여 명령을 비동기적으로 호출할 수 있습니다.

  • IAsyncResult BeginExecuteReader(AsyncCallback ac, object state)
  • IAsyncResult BeginExecuteNonQuery(AsyncCallback ac, object state)
  • IAsyncResult BeginExecuteXmlReader(AsyncCallback ac, object state)

그리고 데이터 스트림이 읽을 수 있는 상태로 준비되면 다음의 해당 완료 메서드를 호출할 수 있습니다.

  • SqlDataReader EndExecuteReader(IAsyncResult ar)
  • int EndExecuteNonQuery(IAsyncResult ar)
  • XmlReader EndExecuteXmlReader(IAsyncResult ar)

이러한 비동기 검색 메서드를 사용하려면 연결 문자열에 "async=true" 항목을 추가해야 합니다. 이 시나리오에서는 SqlDataReader에 바인딩하여 GridView를 채우기를 원하므로 비동기 호출을 시작하는 데 BeginExecuteReader 메서드를 사용할 것입니다.

ASP.NET 2.0에서는 이를 비동기 페이지에 연결할 수 있도록 페이지 렌더링이 완료되기 전에 실행되어야 하는 비동기 작업을 등록할 수 있도록 하고 있습니다. 이것은 웹 서비스 프록시에서 사용한 것보다는 더 명시적인 모델이지만 또한 추가적인 유연성을 제공합니다. 비동기 작업을 등록하기 위해 PageAsyncTask 클래스의 인스턴스를 만들고 시작 처리기, 종료 처리기, 시간 제한 처리기의 세 가지 대리자로 초기화하였습니다. 시작 처리기는 IAsyncResult 인터페이스를 반환해야 하므로 여기에서 BeginExecuteReader를 사용하여 비동기 데이터 요청을 시작할 것입니다. 종료 처리기는 작업이 완료되면(이 예에서는 데이터를 읽을 수 있는 상태로 준비되면) 호출되며 이 곳에서 결과를 사용할 수 있습니다. ASP.NET는 요청 스레드를 양도하기 직전에(PreRender 이벤트가 완료된 직후에) 시작 처리기를 호출하는 작업을 담당합니다. 그림 6은 비동기 작업과 SqlCommand 클래스의 비동기 BeginExecuteReader 메서드를 사용하여 비동기 데이터 액세스를 수행하는 업데이트된 판매 보고서 웹 파트의 구현을 보여 줍니다.

웹 서비스 요청에 대해서도 프록시 클래스에 대체 비동기 메서드(예: BeginGetNewsHeadlines)를 사용하는 동일한 기법을 사용할 수 있습니다. 이러한 기법을 사용했을 때 얻을 수 있는 장점은 시간 제한 처리기를 지정할 수 있다는 것입니다. 원격 호출이 제시간에 반환하지 않는 경우 연결된 시간 제한 처리기가 호출됩니다. 이러한 시간 제한은 Page 지시문에서 AsyncTimeout 특성을 사용하여 지정할 수 있으며 기본값은 20초입니다. 이벤트 기반 비동기 패턴을 사용할 때와는 달리 Page.RegisterAsyncTask를 사용할 때는 Page.IsAsync 결과에 따라 동기 호출을 분기할 필요가 없습니다. 웹 파트의 비동기 페이지 작업은 동기 페이지에서도 문제없이 작동하며 웹 파트의 병렬 실행까지 가능합니다. 핵심적인 차이는 동기 페이지(Async="true" 특성이 없는 페이지)에서 비동기 작업이 실행되는 동안 주 페이지 스레드가 스레드 풀로 해제되지 않는다는 것입니다.

이제 모든 웹 파트가 비동기적으로 데이터 검색을 수행하도록 했으므로 비동기로 표시된 어떤 페이지에도 이러한 파트를 사용할 수 있으며 응답 시간은 모든 웹 파트가 데이터를 검색하는 시간을 더한 것이 아니라 하나의 웹 파트가 완료되는 데 걸리는 최장 시간이 됩니다. 페이지를 비동기로 표시하고 비동기 I/O를 수행하는 웹 파트를 사용함으로써 데이터를 기다리는 동안 다른 클라이언트에 서비스를 제공할 수 있도록 페이지가 주 요청 스레드를 풀어줄 수 있게 되므로 사이트의 잠재적인 확장성 또한 향상되었습니다. 여기에서 기억할 교훈은 ASP.NET 2.0을 사용하여 포털 사이트를 구축한다면 이 릴리스에 도입된 모든 새 비동기 기능을 염두에 두고 응용 프로그램의 응답성과 확장성을 개선하는 데 이러한 기능을 활용하라는 것입니다. ASP.NET 2.0의 비동기 지원에 대한 자세한 내용은 MSDN Magazine 2005년 10월호에서 Jeff Prosise의 Wicked Code 칼럼을 참조하십시오.


Fritz에게 질문이나 의견이 있으면 xtrmasp@microsoft.com으로 보내시기 바랍니다.



Fritz Onion은 Microsoft .NET 교육 제공업체인 Pluralsight의 공동 창립자이며 이곳에서 웹 개발 커리큘럼을 이끌고 있습니다. Fritz는 Essential ASP.NET(Addison Wesley, 2003) 및 출간 예정인 Essential ASP.NET 2.0(Addison Wesley, 2006)의 저자입니다. 연락처는 pluralsight.com/fritz (영문)입니다.

페이지 맨 위로페이지 맨 위로QJ: 060711

Microsoft