Thinking Different





출처 : http://andromedarabbit.net/wp/myth_of_ado_connection_pool/

우선 퀴즈 하나! 이 VB.NET 코드는 연결 풀을 사용할까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Module Module1    
    Sub Main()       
        Dim cn(20)       
        Dim rs     
        Dim n     
        
        For n = 0 To 2    
            cn(n) = CreateObject("ADODB.Connection")       
            
            cn(n).Open("Provider=SQLNCLI; DataTypeCompatibility=80; MARS Connection=True; Server=localhost\SQLEXPRESS; 
Trusted_Connection=yes; Database=SpaceDB;")         
            
            cn(n).Execute("exec sp_help sp_help")          
            
            cn(n).Close()      
            cn(n) = Nothing   
        Next  
    End 
SubEnd Module

우선 MSDN 등의 문서에 따르면 ADO는 따로 설정하지 않는 한 알아서 연결 풀을 활성화한다고 한다. 그렇다면 이 코드는 연결 풀을 이용할 거라 생각할 수 있다. SQL Server Profiler를 사용해 실제로 연결 풀이 활성화됐는지 확인해보자.

그림 1. Visual Basic .NET

21

[그림 1]에서 주목할 부분은 Audit Logout 이벤트 클래스와 exec sp_reset_connection이다. Audit Logout을 보고 앗! 연결을 끊었네. 연결 풀을 안 쓰나 보다.라고 생각할 수도 있지만(내가 그러는 바람에 고생 꽤나 했다), MSDN을 보면 이렇게 쓰여 있다3.

Events in this class are fired by new connections or by connections that are reused from a connection pool.

이 이벤트 클래스는 새 연결이 붙었을 때나 연결 풀이 연결을 다시 사용할 때 발생한다3

그러므로 이 이벤트 클래스만 봐서는 연결 풀을 사용하는지 사용하지 않는지 알 수 없다. 주목할 것은 Audit Logout이 아닌 sp_reset_connection이다. SQL 자습서에 문서화되어 있지 않은 이 저장 프로시저는 연결 풀을 구현할 때 쓴다. 이 저장 프로시저는 SET ANSI NULL ON 등의 연결 설정 값을 초기화함으로써, 재사용하는 연결을 새 연결처럼 다시 초기화시키는 역할을 한다. 이 저장 프로시저가 보인다면 필시 연결 풀이 활성화된 것이다. 다시 말해 [그림 1]은 앞선 VB.NET 코드가 연결 풀을 사용한다는 걸 보여준다.

이제 Visual C++ 코드를 보자. 얼핏 보기에 이 코드는 앞선 VB.NET 코드와 동일해 보이며, 당연히 연결 풀을 사용할 것 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void main()
{   
    if(FAILED(::CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE | COINIT_SPEED_OVER_MEMORY)))      
        return;     
    
    const TCHAR* const  connectionString = _T("Provider=SQLNCLI; Data Source=localhost\\SQLEXPRESS; 
Trusted_Connection=yes; Database=SpaceDB;"); 
    
    for(int i=0; i<3; i++)    
    {       
        _ConnectionPtr pConnection1 = NULL;       
        
        // Open a connection using OLE DB syntax.     
        TESTHR(pConnection1.CreateInstance(_T("ADODB.Connection")));  
 
        pConnection1->Open(connectionString,"","",adConnectUnspecified);       
        pConnection1->Execute(_T("exec sp_help sp_help"), NULL, adCmdText);     
        
        if (pConnection1)          
            if (pConnection1->State == adStateOpen)           
                pConnection1->Close();   
    }    
    
    ::CoUninitialize();
}

하지만 프로파일링 결과를 보면 아주 황당하다.

그림 2. Visual C++

54

보다시피 sp_reset_connection 호출이 전혀 없다. 연결 풀을 전혀 사용 안하고 있는 것이다. 이 문제를 반나절이 넘게 추적하고 나서야 뭐가 문제인지 알 수 있었다.

Pooling in the Microsoft Data Access Components6은 ODBC, OLEDB, ADO의 연결 풀에 대해 설명하는 문서인데, 여기에 ADO 사용자를 위한 팁이 조금 나와있다. 이걸 J씨가 찾아주지 않았으면 반나절이 아니라 하루를 넘게 고생했을지 모를 일이다.

The ADO Connection object implicitly uses IDataInitialize. However, this means your application needs to keep at least one instance of a Connection object instantiated for each unique user-at all times. Otherwise, the pool will be destroyed when the last Connection object for that string is closed. (The exception to this is if you use Microsoft Transaction Server. In this case, the pool is destroyed only if all of the connections in the pool have been closed by client applications and are allowed to time out.)

ADO Connection 객체는 암시적으로 IDataInitialize를 사용합니다. 하지만 이는 애플리케이션이 적어도 사용자마다 인스턴스화된 Connection 객체를 적어도 하나씩은 유지해야 한다는 뜻입니다. 그렇지 않으면 해당 문자열에 대한 마지막 Connection 객체가 닫힐 때 풀도 닫힙니다.7

그러니 C++로 ADO를 쓸 때는 다음과 같이 최초의 연결 객체를 닫지 않고 놔둬야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void main()
{
    if(FAILED(::CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE | COINIT_SPEED_OVER_MEMORY)))
        return;
    
    const TCHAR* const  connectionString = _T("Provider=SQLNCLI; Data Source=localhost\\SQLEXPRESS; 
Trusted_Connection=yes; Database=SpaceDB;");
    
    // 연결 풀 활성화    
    _ConnectionPtr pConnection = NULL;
    TESTHR(pConnection.CreateInstance(_T("ADODB.Connection")));
    pConnection->Open(connectionString,"","",adConnectUnspecified);
    
    for(int i=0; i<3; i++)
    {
        _ConnectionPtr pConnection1 = NULL;
        
        // Open a connection using OLE DB syntax.
        TESTHR(pConnection1.CreateInstance(_T("ADODB.Connection")));
        
        pConnection1->Open(connectionString,"","",adConnectUnspecified);
        pConnection1->Execute(_T("exec sp_help sp_help"), NULL, adCmdText);
        
        if (pConnection1)
            if (pConnection1->State == adStateOpen)
                pConnection1->Close();
    }
    
    ::CoUninitialize();
}

만약 루프를 도는 중간에 네트워크가 끊기면 어떻게 될까? 처음에 연 연결 객체를 닫고 다시 열어야 할까? 아니면 그냥 놔두면 네트워크가 복구됐을 때 연결 풀을 사용할까? 루프를 도는 중간에 중단점을 걸고 랜 선을 뽑았다가 다시 연결해보니, 연결 풀을 사용하고 있었다. 그러니 애플리케이션 초기화를 하거나 해당 데이터베이스에 처음 연결할 때, 전역 객체를 하나 생성해두면 연결 풀을 계속 사용할 수 있다.

이 글의 교훈? 알아서 해준다는 말을 너무 믿으면 안 된다.

Links

  1. http://www.flickr.com/photos/kaistizen/2263787353/
  2. http://farm3.static.flickr.com/2138/2263787353_c0697397b5_o.png
  3. http://technet.microsoft.com/en-us/library/ms175827.aspx
  4. http://www.flickr.com/photos/kaistizen/2264576968/
  5. http://farm3.static.flickr.com/2174/2264576968_4ddec8199a_o.png
  6. http://msdn2.microsoft.com/en-us/library/ms810829.aspx
  7. http://msdn2.microsoft.com/en-us/library/ms810829.aspx#pooling2_topic3