Laboratory/Develop

ADO.NET 2.0에서 비동기 명령 실행

theking 2008. 4. 21. 08:04
 

Pablo Castro
Microsoft Corporation

2005년 4월
2005년 6월 업데이트

적용 대상:
   ADO.NET 2.0

Summary:ADO.NET 2.0의 새로운 비동기 실행 기능과 이 기능으로 가능해진 시나리오, 그리고 이 기능 사용 시 염두에 두어야 할 몇 가지 문제점에 대해 개략적으로 다룹니다.

목차

서론
진정한 비동기 I/O
새로운 API 요소
응용 시나리오
기억해야 할 사항
결론

서론

ADO.NET 2.0을 릴리스하면서 우리는 기존 시나리오를 더 쉽게 만들 뿐만 아니라 이전에는 불가능했거나 아니면 현실적이지 못했던 새 시나리오를 가능하도록 할 수 있기를 원했습니다.

비동기 명령 실행이 그 좋은 예입니다. ADO.NET 2.0이 릴리스되기 전에는 한 명령을 실행하고 그 실행이 완료된 후에야 다음 실행으로 넘어갈 수 있었습니다. 하지만 비동기 API가 추가되자 데이터베이스가 작업을 완료하기를 기다리지 않고 응용 프로그램이 계속해서 실행되어야 하는 시나리오가 가능해 졌습니다.

본 자료에서는 비동기 데이터베이스 API의 기본과 함께 이러한 API가 유용하게 사용되는 몇 가지 시나리오를 다루겠습니다. 어떤 데이터 액세스 공급자와 사용할 수 있도록 API를 디자인했지만 SqlClient(SQL Server 용 .NET 데이터 액세스 공급자)만이 .NET에 포함된 네 공급자 중 실제로 API를 지원하는 유일한 공급자입니다. 그러한 이유로 본 자료의 샘플과 메서드 및 클래스 설명에 SqlClient를 사용하겠습니다. 하지만 타사 공급자 기록기도 이 비동기 API를 잘 구현할 수 있으므로 비동기식으로 액세스할 수 있는 더 많은 데이터베이스를 볼 수 있다는 사실을 기억하시기 바랍니다. 클래스와 메서드 이름만 적합하게 변경하면 이러한 샘플을 다른 데이터베이스에서도 사용할 수 있습니다.

진정한 비동기 I/O

.NET Framework 이전 버전에서는 비동기 위임 또는ThreadPool클래스를 사용하여 차단 없는 실행을 시뮬레이션하는 것이 가능했습니다. 하지만 그러한 솔루션은 백그라운드의 다른 스레드를 차단하였으므로, 아래 응용 시나리오 부분에서 확인하게 되겠지만 스레드 차단을 피해야 하는 경우에는 적합하지 않았습니다.

당시에는 “비블로킹” 문 실행을 노출한 더 이전의 데이터베이스 액세스 API인 ADO가 있었습니다. 하지만 ADO와 ADO/NET/SqlClient 구현은 근본적으로 다릅니다. ADO는 백그라운드 스레드를 만들며 데이터베이스 작업이 끝날 때까지 호출 스레드 대신 그 스레드를 차단합니다. 이 방법은 클라이언트쪽 응용 프로그램에는 효과적이지만 본 자료 뒷부분에서 다룰 중간 계층 및 서버쪽 시나리오에는 적합하지 않습니다.

ADO.NET/SqlClient 비동기 명령 실행 지원은 진정한 네트워크 I/O(또는 공유 메모리의 경우 비블로킹 신호)를 기초로 합니다. 필요해 진다면 언젠가 이 내부 구현에 대해서도 쓸 것입니다. 하지만 지금은, 특정 I/O 작업이 끝나기를 기다리면서 백그라운드 스레드를 차단하는 일이 없는 “진정한 비동기”를 실행하며, Windows 2000/XP/2003 운영 체제의 겹친 I/O 및 입/출력 완료 포트 기능을 사용하면 단일 스레드(또는 소수의 스레드)를 사용하여 해당 프로세스에 대해 처리되지 않은 요청을 모두 처리할 수 있도록 만들 수 있다는 것까지만 언급하겠습니다.

새로운 API 요소

.NET Framework의 기존 API를 모델로 새 ADO.NET 비동기 API를 만들었으며 기능도 비슷합니다. 대형 프레임워크에서는 일관성이 중요하기 때문입니다.

비동기 메서드

ExecuteReader, ExecuteNonQuery, ExecuteXmlReader, ExecuteScalar를 포함한 모든 명령 실행 API는 ADO.NET의 Command 개채 안에 있습니다. 우리는 이 기능을 위해 추가하는 API 표면을 최소화하기로 결정하였으므로 다른 메서드에서 채택할 수 없는 메서드인ExecuteReader, ExecuteNonQuery, ExecuteXmlReader에 대해서만 비동기 버전을 추가했습니다.ExecuteScalarExecuteReader +1열/1행 반입 + 읽기 프로그램 닫기의 짧은 형태에 불과하므로 이 메서드에 대한 비동기 버전은 포함시키지 않았습니다.

이미 .NET Framework에서 사용되고 있는 비동기 API 패턴에 따라 기존의 개별 동기 메서드에는 작업을 시작하는begin부분과 작업을 완료하는end부분의 두 메서드로 나눠진 비동기 대응물이 있습니다. 아래 테이블은 명령 개체의 새 메서드를 요약한 것입니다.

표 1. ADO.NET 2.0에서 사용할 수 있는 새로운 비동기 메서드

동기 메서드비동기 메서드
"Begin" 부분"End" 부분
ExecuteNonQueryBeginExecuteNonQueryEndExecuteNonQuery
ExecuteReaderBeginExecuteReaderEndExecuteReader
ExecuteXmlReaderBeginExecuteXmlReaderEndExecuteXmlReader
비동기 패턴은 메서드를 모델로 하므로begin메서드는 모든 입력 매개 변수를 받으며end메서드는 반환값뿐만 아니라 모든 출력 매개 변수를 제공합니다. 다음은ExecuteReader비동기 호출의 예입니다.
IAsyncResult ar = command.BeginExecuteReader();// ...// do other processing// ...SqlDataReader r = command.EndExecuteReader(ar);// use the reader and then close the reader and the connection

위 예에서BeginExecuteReader는 어떤 매개 변수도 취하지 않으며 (어떤 매개 변수도 취하지 않는ExecuteReader오버로드로 매핑됨)EndExecuteReaderExecuteReader처럼SqlDataReader를 반환하는 것을 볼 수 있습니다.

기본 클래스 라이브러리에 있는 다른 비동기 API와 함께begin메서드는 작동 상태 추적에 사용될 수 있는IAsyncResult레퍼런스를 반환합니다. 이에 대해서는 아래 “완료 신호”에서 보다 자세히 다루겠습니다.

“비동기” 연결 문자열 키워드

비동기 명령을 사용하기 위해서는 연결 문자열에async=true를 사용하여 이 명령이 실행될 연결을 초기화해야 합니다. 연결 문자열에async=true가 없는 연결에서 비동기 메서드가 호출되면 예외가 발생합니다.

동기 명령만을 사용하여 주어진 연결 개체를 사용할 것이라는 사실을 아는 경우에는 연결 문자열에async키워드를 포함시키지 않는 것이 좋으며 아니면 포함시키더라도false로 설정하는 것이 좋습니다. 비동기 작업이 가능해진 연결에서 동기 작업을 실행하면 리소스 사용이 눈에 띄게 증가합니다.

동기 및 비동기 API가 모두 필요한 경우에는 가능하면 서로 다른 연결을 사용해야 합니다. 그럴 수 없는 경우에는async=true를 사용해 열린 연결에서 여전히 동기 메서드를 사용할 수 있으며 이 경우 동기 메서드는 약간의 성능 약화가 발생하긴 하지만 평소처럼 실행됩니다.

완료 신호

비동기 API의 기본 요소 중 하나는 완료 신호 메커니즘입니다. 동기 API에서는 작업이 끝나기 전까지는 메서드 호출이 반환을 하지 않으므로 문제가 되지 않습니다. 하지만 비동기 호출의 경우에는begin호출이 즉시 반환을 하므로 작업이 실제로 완료되는 때를 감지하는 방법이 필요합니다.

.NET Framework 비동기 API 에서와 마찬가지로 ADO.NET에도 비동기 명령이 실행을 완료한 때를 감지하는 옵션이 많이 있습니다.

  • 콜백:모든begin메서드에는 사용자 지정state개체와 함께 위임을 매개 변수로 취하는 오버로드가 있습니다. 이 오버로드가 사용되면 ADO.NET은 전달된 그 위임을 호출하고 (그 위임에 대한 매개 변수로 전달된)IAsyncResult개체를 통해 그state개체를 사용 가능하도록 만듭니다. 콜백은 스레드-풀 스레드에서 호출되므로 이 작업을 시작한 스레드와는 다를 가능성이 높습니다. 응용 프로그램에 따라 적절한 동기화가 필요할 수 있습니다.
  • 동기화 개체:begin메서드에 의해 반환된IAsyncResult개체에는 이벤트 개체가 들어 있는 WaitHandle 속성이 있습니다. 이 이벤트 개체는WaitHandle.WaitAnyWaitHandle.WaitAll와 같은 동기화 기본에서 사용될 수 있습니다. 따라서 호출 코드는 보류 중인 다수의 작업을 기다릴 수 있으며 한 작업이 완료되거나 모든 작업이 완료될 때 통지를 받게 됩니다. 그리고 클라이언트 코드가 데이터베이스 작업과 이벤트를 사용하는 다른 작업을 모두 기다려야 하는 시나리오도 가능하며 동기화 용으로 사용되는 다른 OS 대기 가능 핸들도 사용 가능해 집니다.
  • 폴링:IAsyncResult 개체에는IsCompleted부울 속성도 있습니다. 작업이 완료되면 이 속성은true로 변하므로 일부 연속 작업을 실행해야하는 코드에 사용할 수 있습니다. 이 코드는 정기적으로 그 속성을 확인하여 속성이 변경되면 그 결과를 처리합니다.

이 세 경우 모두 일단 작업이 완료된 것으로 신호가 되면 호출자는 그 비동기 명령을 시작한begin메서드에 대한 해당end메서드를 반드시 호출해야 합니다. 그begin메서드에 일치하는end메서드를 호출하지 않으면 ADO.NET이 시스템 리소스를 누출할 수 있습니다. 이 외에도end메서드를 호출하면 그 작업의 결과를 호출자가 이용할 수 있게 됩니다. 이 메서드는SqlDataReader(EndExecuteReader용), 영향을 받는 레코드 수(EndExecuteNonQuery용) 또는XmlReader(EndExecuteXmlReader용)입니다.

작업이 완료되기를 기다리지 않고end메서드를 호출하더라도 전혀 잘못된 것은 없습니다. 그러한 경우에는 작업이 완료될 때까지 그 메서드가 차단되므로 동기 작업으로 전환됩니다(기능상으로는 비동기 상태로 남아 있습니다).

응용 시나리오

이 “멋진 기능”을 바탕으로 한 기능 디자인은 너무 쉽고 매력적이므로 여기서는 우리가 제공하는 각각의 새 기능과 관련된 응용 시나리오에 대해서만 살펴 보겠습니다. 다음은 우리가 비동기 API를 디자인할 때 염두에 두었던 몇몇 주요 시나리오입니다. 그리고 비동기 명령이 좋은 방법처럼 보이지만 좀 더 신중하게 고려할 경우 그렇지 않은 시나리오도 포함시켰습니다.

문을 병렬로 실행

한 가지 재미있는 비동기 명령 실행 시나리오는 동일하거나 서로 다른 데이터베이스 서버에 대해 다수의 SQL 문을 병렬로 실행하는 것입니다.

특정 직원에 대한 정보를 응용 프로그램에서 표시해야 하는데 그 정보 중 일부는 인적 자원 관리 데이터베이스에 있고 임금 관련 정보는 회계 데이터베이스에 있다고 합시다. 첫 번째 문이 완료되도록 기다린 다음 두 번째 문을 시작하는 대신 두 데이터베이스에 동시에 쿼리를 전송하여 병렬로 실행할 수 있다면 좋을 것입니다.

예:

// obtain connection strings from configuration files or// similar facility// NOTE: these connection strings have to include "async=true", for// example: // "server=myserver;database=mydb;integrated security=true;async=true"string connstrAccouting = GetConnString("accounting");string connstrHR = GetConnString("humanresources");// define two connection objects, one for each databaseusing(SqlConnection connAcc = new SqlConnection(connstrAccounting))using(SqlConnection connHumanRes  = new SqlConnection(connstrHR)) {  // open the first connection  connAcc.Open();  // start the execution of the first query contained in the  // "employee_info" stored-procedure  SqlCommand cmdAcc = new SqlCommand("employee_info", connAcc);  cmdAcc.CommandType = CommandType.StoredProcedure;  cmdAcc.Parameters.AddWithValue("@empl_id", employee_id);  IAsyncResult arAcc = cmdAcc.BeginExecuteReader();  // at this point, the "employee_info" stored-proc is executing on  // the server, and this thread is running at the same time  // now open the second connection  connHumanRes.Open();  // start the execution of the second stored-proc against  // the human-resources server  SqlCommand cmdHumanRes = new SqlCommand("employee_hrinfo",                                           connHumanRes);  cmdHumanRes.Parameters.AddWithValue("@empl_id", employee_id);  IAsyncResult arHumanRes = cmdHumanRes.BeginExecuteReader();  // now both queries are running at the same time  // at this point; more work can be done from this thread, or we  // can simply wait until both commands finish - in our case we'll  // wait  SqlDataReader drAcc = cmdAcc.EndExecuteReader(arAcc);  SqlDataReader drHumanRes = cmdHumanRes.EndExecuteReader(arHumanRes);  // now we can render the results, for example, bind the readers to an ASP.NET  // web control, or scan the reader and draw the information in a   // WebForms form.}

여기서는EndExecuteReader를 한 번만 호출했다는 점에 주의하십시오. 데이터베이스 작업이 완료되기 전까지는 다른 작업이 필요하지 않습니다.EndExecuteReader는 이 작업이 완료될 때까지 차단되며 이 작업이 완료되면SqlDataReader개체를 반환합니다.

다수의 작업을 기다려야 하며 모든 작업이 비동기 ADO.NET 작업은 아닌 보다 정교한 시나리오의 경우에는WaitHandle.WaitAll또는WaitHandle.WaitAny를 사용할 수 있습니다.IAsyncResult.WaitHandle에는 동기화에 사용할 수 있는 이벤트 개체가 들어 있습니다.

보다 정교한 이 방법이 적용된 것이 바로 순서에 맞지 않는 렌더링입니다. 데이터 소스가 여럿인 ASP.NET 페이지가 있다고 합시다. 다수의 명령을 실행할 수 있으며 그 명령이 완료되면 다른 데이터베이스가 다른 작업을 처리하는 동안 사용자는 그 페이지의 해당 부분을 렌더링할 수 있습니다. 이렇게 되면 어떤 작업이 먼저 끝나는지에 관계 없이 사용 가능한 데이터가 있기만 하면 작업을 진행할 수 있습니다.

Northwind 데이터베이스(본 자료와 함께 제공되는 zip 파일에 이 예의 원본이 포함되어 있음)를 사용하는 예에서 재미있는 점이 바로 여기에 있습니다. 이것은 작업이 서로 다른 데이터베이스에서 이루어지거나 데이터베이스 서버가 모든 쿼리를 동시에 처리할 만큼 강력하다면 더 유용합니다.

// NOTE: connection strings denoted by "connstring" have to include  // "async=true", for example:  // "server=myserver;database=mydb;integrated security=true;async=true" // we'll use three connections for this using(SqlConnection c1 = new SqlConnection(connstring)) using(SqlConnection c2 = new SqlConnection(connstring)) using(SqlConnection c3 = new SqlConnection(connstring)) {  // get customer info  c1.Open();  SqlCommand cmd1 = new SqlCommand(    "SELECT CustomerID, CompanyName, ContactName FROM Customers " +    "WHERE CustomerID=@id", c1);  cmd1.Parameters.Add("@id", SqlDbType.Char, 5).Value = custid;  IAsyncResult arCustomer = cmd1.BeginExecuteReader();  // get orders  c2.Open();  SqlCommand cmd2 = new SqlCommand(    "SELECT * FROM Orders WHERE CustomerID=@id", c2);  cmd2.Parameters.Add("@id", SqlDbType.Char, 5).Value = custid;  IAsyncResult arOrders = cmd2.BeginExecuteReader();  // get order detail if user picked an order  IAsyncResult arDetails = null;  SqlCommand cmd3 = null;  if(null != orderid) {   c3.Open();   cmd3 = new SqlCommand(      "SELECT * FROM [Order Details] WHERE OrderID=@id", c3);   cmd3.Parameters.Add("@id", SqlDbType.Int).Value =                                                             int.Parse(orderid);   arDetails = cmd3.BeginExecuteReader();  }  // build the wait handle array for WaitForMultipleObjects  WaitHandle[] handles = new WaitHandle[null == arDetails ? 2 : 3];  handles[0] = arCustomer.AsyncWaitHandle;  handles[1] = arOrders.AsyncWaitHandle;  if(null != arDetails)   handles[2] = arDetails.AsyncWaitHandle;  // wait for commands to complete and render page controls as we   // get data back  SqlDataReader r;  for(int results = (null==arDetails) ? 1 : 0; results < 3;results++) {   // wait for any handle, then process results as they come   int index = WaitHandle.WaitAny(handles, 5000, false); // 5 secs   if(WaitHandle.WaitTimeout == index)    throw new Exception("Timeout");   switch(index) {    case 0: // customer query is ready     r = cmd1.EndExecuteReader(arCustomer);     if (!r.Read())      continue;     lblCustomerID.Text = r.GetString(0);     lblCompanyName.Text = r.GetString(1);     lblContact.Text = r.GetString(2);     r.Close();     break;    case 1: // orders query is ready     r = cmd2.EndExecuteReader(arOrders);     dgOrders.DataSource = r; // data-bind to the orders grid     dgOrders.DataBind();     r.Close();     break;    case 2: // details query is ready     r = cmd3.EndExecuteReader(arDetails);     dgDetails.DataSource = r; // data-bind to the details grid     dgDetails.DataBind();     r.Close();     break;    }   }  } }}

위의 두 예에서 기존의 동기 ADO.NET과 비동기 위임,QueueUserWorkItem같은 스레드-풀 API 또는 사용자가 작성한 스레드를 사용해도 비슷한 효과를 얻을 수 있습니다. 하지만 어떤 경우에나 개별 명령은 스레드를 차단할 것입니다. 스레드 차단은 클라이언트쪽 응용 프로그램과 같은 일부 경우에는 문제가 되지 않지만 중간 계층 및 서버쪽 응용 프로그램의 경우에는 확장성을 손상시킬 수 있습니다. 다음 섹션에서는 이에 대해 살펴 봅시다.

비블로킹 ASP.NET 처리기 및 페이지

웹 서버는 일반적으로 다양한 스레드 풀링을 사용하여 웹 페이지 요청(또는 웹 서비스나 HttpHandler 호출과 같은 다른 종류의 요청) 처리에 사용되는 스레드를 관리합니다.

로드가 높은 데이터베이스 구동 웹 사이트에서 동기 데이터베이스 API를 사용하면 데이터베이스 서버가 결과를 반환하기를 기다리는 동안 스레드-풀 스레드의 대부분이 차단될 수 있습니다. 그렇게 되면 웹 서버는 CPU와 네트워크를 거의 사용하지 못하게 되며 새 요청을 받아들이지 못하거나 받아 들인다 하더라도 그럴 만한 스레드가 거의 없게 됩니다.

ASP.NET에는IHttpAsyncHandler를 구현하는 클래스인 “비동기 HTTP 처리기”가 있는데 이 클래스는 “ashx”라는 확장명으로 ASP.NET 파일과 연결되어 있습니다. 이러한 클래스는 비동기식으로 요청을 처리하고 응답을 생성할 수 있습니다. 이 기능은 비동기 ADO.NET 명령과 아주 잘 통합됩니다.

IHttpAsyncHandler에 대한 보다 자세한 내용은 MSDN에 대한 IHttpAsyncHandler 인터페이스 항목을 참조하십시오. 이 외에도 MSDN Magazine에도 이 인터페이스 사용 방법에 관한 “Use Threads and Build Asynchronous Handlers in Your Server-Side Web Code(스레드를 사용하여 서버쪽 웹 코드에 비동기 처리기 만들기)”라는 좋은 자료가 있습니다(이 자료에는 보이지 않는 곳에서 모든 작업이 어떻게 이루어지는지를 보여 주는 훌륭한 그림이 포함되어 있음).

위 자료의 샘플에 포함되어 있는 asyncorders.cs와 asyncorders.ashx inc 파일 역시 단순하지만 이 테크닉에 적합한 샘플입니다. 다음은 우리가 관심을 갖고 있는 부분입니다.

public class AsyncOrders : IHttpAsyncHandler{ protected SqlCommand _cmd; protected HttpContext _context; // asynchronous execution support is split between  // BeginProcessRequest and EndProcessRequest public IAsyncResult BeginProcessRequest(HttpContext context,                                                           AsyncCallback cb,                                          object extraData) {  // get the ID of the customers we need to list the orders for   // (it's in the query string)  string customerId = context.Request["customerId"];  if(null == customerId)   throw new Exception("No customer ID specified");  // obtain the connection string from the configuration file  string connstring =                 ConfigurationSettings.AppSettings["ConnectionString"];  // connect to the database and kick-off the query  SqlConnection conn = new SqlConnection(connstring);  try {   conn.Open();   // we use an stored-procedure here, but this could be any statement   _cmd = new SqlCommand("get_orders", conn);   _cmd.CommandType = CommandType.StoredProcedure;   _cmd.Parameters.AddWithValue("@ID", customerId);   // begin execution of the command. This method will return post    // the query   // to the database and return without waiting for the results   // NOTE: we are passing to BeginExecuteReader the callback    // that ASP.NET passed to us; so ADO.NET will call cb directly    // once the first database results are ready. You can also use    // your own callback and invoke the ASP.NET one as appropiate   IAsyncResult ar = _cmd.BeginExecuteReader(cb, extraData);   // save the HttpContext to use it in EndProcessRequest   _context = context;   // we're returning ADO.NET's IAsyncResult directly. a more    // sophisticated application might need its own IAsyncResult    // implementation   return ar;  }  catch {   // only close the connection if we find a problem; otherwise, we'll   // close it once we're done with the async handler   conn.Close();   throw;  } } // ASP.NET will invoke this method when it detects that the async  // operation finished public void EndProcessRequest(IAsyncResult result) {  try {   // obtain the results from the database   SqlDataReader reader = _cmd.EndExecuteReader(result);   // render the page   RenderResultsTable(_context, "Orders (async mode)", reader);  }  finally {   // make sure we close the connection before returning from    // this method   _cmd.Connection.Close();   _cmd = null;  } } // rest of AsyncOrders members // ...}

위 예에서 우리는 HTTP 처리기가 어떻게 입력 매개 변수를 처리하고 데이터베이스 쿼리를 시작한 다음BeginProcessRequest메서드에서 반환되는지를 볼 수 있습니다. 이 메서드에서 반환됨에 따라 우리는 컨트롤을 다시 ASP.NET으로 보낼 수 있으며 이제 데이터베이스 서버가 쿼리를 처리하는 동안 ASP.NET은 이 스레드를 다시 사용하여 다른 요청을 처리할 수 있게 됩니다. 작업이 완료되면 신호 메커니즘이EndProcessRequest호출을 유발하고 그러면 페이지 렌더링이 완료됩니다.EndProcessRequest는 동일한 스레드 또는 다른 스레드에서 호출될 수 있습니다.

한 페이지를 렌더링하는데 다수의 쿼리가 필요한 시나리오에서는 쿼리를 묶어 단일한 일괄 처리로 전송(모든 커리가 동일한 데이터베이스를 대상으로 할 경우)하거나 다수의 연결을 사용하여 다수의 비동기 명령 실행을 시작할 수 있습니다. 후자의 경우 명령 완료를 종합하고 모든 명령이 완료될 경우 ASP.NET에 이를 알리려면 추가 코드가 필요합니다.

WinForms 응용 프로그램을 응답 상태로 유지

WinForms응용 프로그램에서 장기 실행 데이터베이스 작업을 실행하고 있다면 그러한 작업을 실행할 때 응용 프로그램이 고정되는 것을 눈치챘을 것입니다. 이는, 데이터베이스 호출 시 이벤트 처리기가 차단되며 그 동안에는 다른 Windows 메시지를 처리할 수 없기 때문입니다.

비동기 명령 실행을 사용하여 이 문제를 처리하자는 생각이 들 수 있습니다. 사실, 그것이 바로 우리가 필요로 하는 것입니다. 명령을 실행하고 이벤트 처리기에서 즉시 반환되는 방법 말입니다. 이 경우에는WinForms응용 프로그램이 좀더 복잡하지만WinForms컨트롤이 만들어진 스레드를 제외한 다른 스레드에서는 이 컨트롤을 건드릴 수가 없습니다. 이는 ADO.NET 비동기 작업을 시작할 수 없으며 콜백에서 새 데이터를 사용해 컨트롤을 새로 고치거나 데이터 바인딩 작업을 실행할 수 없다는 것을 뜻합니다. 따라서 데이터를 UI 스레드로 다시 마샬링하고 거기서 업데이트를 해야 합니다. 게다가 많은 경우 UI를 채울 다수의 쿼리가 필요하며 그 중 일부는 이전 쿼리의 결과에 의존하기도 하므로 문제가 더욱 복잡합니다. 따라서 다수의 비동기 쿼리를 조정한 다음 모든 결과를 UI 스레드로 마샬링하고 UI 컨트롤을 다시 채워야 합니다.

훨씬 간단한 방법은 .NET 2.0에 포함될 새BackgroundWorker클래스를 사용하는 방법입니다. 이 클래스를 사용하면 UI 스레드를 차단하지 않고도 전통적인 동기식 데이터베이스 작업을 할 수 있습니다.BackgroundWorker클래스에 대한 자세한 내용은 .NET Framework 2.0 설명서가 발표된 후에 확인하십시오.

옵션을 분석한 결과 그럼에도 불구하고 여전히 비동기 명령을 사용해야 한다고 생각되는 경우에는 이 명령을 사용할 수 있습니다. 하지만 이 경우 필수 사항을 모두 고려해야 합니다. 우리는 스레드 차단을 피하는 것이 중요한 시나리오에 그 초점을 맞추고 이 기능을 디자인했습니다. 일반적으로 클라이언트쪽 응용 프로그램의 경우에는 스레드 차단이 별 문제가 되지 않으므로 단순한BackgroundWorker를 시도해 볼 만한 가치가 있습니다.

기억해야 할 사항

다음은 비동기 명령 실행을 사용할 때 명심해야 할 몇 가지 고려 사항입니다.

오류 처리

오류는 명령 실행 중 언제라도 발생할 수 있습니다. ADO.NET은 실제 데이터베이스 작업을 시작하기 전에 오류를 감지할 수 있으므로begin메서드에서 예외를 발생시킵니다. 이는ExecuteReader또는 유사한 메서드 호출에서 직접 예외가 발생하는 동기식 사례와 아주 비슷합니다. 부적절한 매개 변수, 관련 개체의 나쁜 상태(예: 해당 명령에 대한 연결 집합 없음), 또는 일부 연결 문제(예: 서버 또는 네트워크 다운) 등이 여기에 포함됩니다.

이제, 서버에 작업을 전송하고 그 작업을 사용자에게 반환하고 나면 문제가 발생했다는 것을 감지하더라도 그 즉시 그러한 사실을 알릴 방법이 없습니다. 그냥 예외를 발생시킬 수는 없습니다. 중간 처리를 할 경우 이 스택에서 우리 위에는 사용자 코드가 없으므로 예외를 발생시킨다 하더라도 사용자는 그러한 예외를 알 방법이 없습니다. 따라서 우리는 그 오류 정보를 그대로 두고 그 작업이 완료되었다고 신호합니다. 따라서 나중에 사용자의 코드가end메서드를 호출하면 우리는 처리 중 오류가 발생하였음을 감지하고 예외를 발생시킵니다.

결론적으로beginend메서드에서 모두 오류를 처리할 준비가 되어 있어야 한다는 것입니다.

작업 차단

비동기 실행을 사용할 때에도 I/O를 위해 차단을 해야할 경우가 많이 있습니다. 다음은SqlClient에 고유한 차단 호출의 개략적인 목록입니다.

  • Begin 메서드: 다수의 매개 변수 또는 아주 긴 SQL 문을 전송해야 할 경우 네트워크 쓰기가 차단될 수 있습니다.
  • SqlDataReader.Read: 응용 프로그램이 서버가 데이터를 만드는 속도보다 더 빠르게 그 데이터를 읽으며 네트워크가 그 데이터를 클라이언트로 전송할 수 있다면Read()가 네트워크 읽기를 차단할 수도 있습니다.
  • SqlDataReader.Get*: 이 데이터 판독기는 CLR 및 SQL 형식 시스템(예: 각각GetStringGetSqlString)에 개별 데이터 형식에 대한Get메서드를 갖고 있습니다.Begin호출에CommandBehavior.SequentialAccess가 사용되었다면 이Get메서드가 네트워크 읽기를 차단할 수도 있습니다.
  • SqlDataReader.Close()SqlDataReader.Dispose(): 소비되지 않은 보류 중인 행이 있거나 네트워크에서 출력 매개 변수가 아직 반입되지 않은 경우에는 이러한 매개 변수가 차단을 유발할 수 있습니다.

보류 중인 명령 취소

명령 개체에는 실행 명령을 취소하는데 사용할 수 있는Cancel()메서드가 있습니다.

버전

제가 이 기능에서 정말 좋아하는 점 중 하나는 SQL Server 7.0, SQL Server 2000, 그리고 새 SQL Server 2005을 포함해SqlClient가 지원하는 모든 SQL Server 버전에서 실행이 된다는 것입니다. 물론 예외가 있습니다. SQL Server 2005 이전 서버에서는 공유 메모리에 대한 비동기 실행을 지원하지 않습니다. 따라서 동일한 컴퓨터에 이 서버와 클라이언트가 있는데 비동기 명령을 사용하고 싶다면localhost를 서버 이름으로 사용하거나 서버 이름에tcp:를 추가하여 공급자가 공유 메모리 대신 TCP/IP를 사용하도록 만들어야 합니다.

이 외에도 이 기능은 Windows 2000, Windows XP, Windows 2003 Server 및 Windows 이후 버전을 포함한 Windows NT 기반 운영 체제에만 있는 특정 기능에 크게 의존합니다. 비동기 실행은 Windows 9x와 Windows Me에서는 지원되지 않습니다.

참고   예를 시험해 보려면 .NET Framework 2.0이 필요합니다.

결론

비동기 명령 실행은 ADO.NET의 강력한 확장 기능으로, 약간의 복잡성이 추가되지만 확장성 높은 새로운 시나리오를 가능하게 만들어 줍니다.

다른 복잡한 기능과 마찬가지로 비동기 명령 실행 역시 반드시 필요한 경우에만 사용하는 것이 좋습니다. 이 기능은 응용 프로그램에 코드를 추가하게 되며 소유자는 그러한 코드를 이해하고 유지 관리해야 하기 때문입니다.

독자분들이 이러한 새로운 기능이 유익하다고 느끼길 바라며, 질문이나 의견이 있는 경우에는 ADO.NET 뉴스 그룹으로 알려 주시기 바랍니다. 저를 포함한 ADO.NET 팀원들이 거의 매일 뉴스 그룹을 확인하고 있습니다. 뉴스 그룹 서버는 msnews.microsoft.com이며 ADO.NET 뉴스 그룹은 microsoft.public.dotnet.framework.adonet입니다.