다음을 통해 공유


트랜잭션 범위를 사용하여 암시적 트랜잭션 구현

이 클래스는 TransactionScope 트랜잭션 자체와 상호 작용할 필요 없이 코드 블록을 트랜잭션에 참여하는 것으로 표시하는 간단한 방법을 제공합니다. 트랜잭션 범위는 앰비언트 트랜잭션을 자동으로 선택하고 관리할 수 있습니다. 사용 편의성과 효율성 때문에 트랜잭션 애플리케이션을 개발할 때 클래스를 TransactionScope 사용하는 것이 좋습니다.

또한 트랜잭션에 리소스를 명시적으로 등록할 필요가 없습니다. 모든 System.Transactions 리소스 관리자(예: SQL Server 2005)는 범위에서 만든 앰비언트 트랜잭션의 존재를 감지하고 자동으로 등록할 수 있습니다.

트랜잭션 범위 만들기

다음 샘플에서는 클래스의 간단한 사용을 보여 줍니다 TransactionScope .

// This function takes arguments for 2 connection strings and commands to create a transaction
// involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
// transaction is rolled back. To test this code, you can connect to two different databases
// on the same server by altering the connection string, or to another 3rd party RDBMS by
// altering the code in the connection2 code block.
static public int CreateTransactionScope(
    string connectString1, string connectString2,
    string commandText1, string commandText2)
{
    // Initialize the return value to zero and create a StringWriter to display results.
    int returnValue = 0;
    System.IO.StringWriter writer = new System.IO.StringWriter();

    try
    {
        // Create the TransactionScope to execute the commands, guaranteeing
        // that both commands can commit or roll back as a single unit of work.
        using (TransactionScope scope = new TransactionScope())
        {
            using (SqlConnection connection1 = new SqlConnection(connectString1))
            {
                // Opening the connection automatically enlists it in the
                // TransactionScope as a lightweight transaction.
                connection1.Open();

                // Create the SqlCommand object and execute the first command.
                SqlCommand command1 = new SqlCommand(commandText1, connection1);
                returnValue = command1.ExecuteNonQuery();
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue);

                // If you get here, this means that command1 succeeded. By nesting
                // the using block for connection2 inside that of connection1, you
                // conserve server and network resources as connection2 is opened
                // only when there is a chance that the transaction can commit.
                using (SqlConnection connection2 = new SqlConnection(connectString2))
                {
                    // The transaction is escalated to a full distributed
                    // transaction when connection2 is opened.
                    connection2.Open();

                    // Execute the second command in the second database.
                    returnValue = 0;
                    SqlCommand command2 = new SqlCommand(commandText2, connection2);
                    returnValue = command2.ExecuteNonQuery();
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
                }
            }

            // The Complete method commits the transaction. If an exception has been thrown,
            // Complete is not  called and the transaction is rolled back.
            scope.Complete();
        }
    }
    catch (TransactionAbortedException ex)
    {
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
    }

    // Display messages.
    Console.WriteLine(writer.ToString());

    return returnValue;
}
'  This function takes arguments for 2 connection strings and commands to create a transaction
'  involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
'  transaction is rolled back. To test this code, you can connect to two different databases
'  on the same server by altering the connection string, or to another 3rd party RDBMS
'  by altering the code in the connection2 code block.
Public Function CreateTransactionScope( _
  ByVal connectString1 As String, ByVal connectString2 As String, _
  ByVal commandText1 As String, ByVal commandText2 As String) As Integer

    ' Initialize the return value to zero and create a StringWriter to display results.
    Dim returnValue As Integer = 0
    Dim writer As System.IO.StringWriter = New System.IO.StringWriter

    Try
        ' Create the TransactionScope to execute the commands, guaranteeing
        '  that both commands can commit or roll back as a single unit of work.
        Using scope As New TransactionScope()
            Using connection1 As New SqlConnection(connectString1)
                ' Opening the connection automatically enlists it in the
                ' TransactionScope as a lightweight transaction.
                connection1.Open()

                ' Create the SqlCommand object and execute the first command.
                Dim command1 As SqlCommand = New SqlCommand(commandText1, connection1)
                returnValue = command1.ExecuteNonQuery()
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue)

                ' If you get here, this means that command1 succeeded. By nesting
                ' the using block for connection2 inside that of connection1, you
                ' conserve server and network resources as connection2 is opened
                ' only when there is a chance that the transaction can commit.
                Using connection2 As New SqlConnection(connectString2)
                    ' The transaction is escalated to a full distributed
                    ' transaction when connection2 is opened.
                    connection2.Open()

                    ' Execute the second command in the second database.
                    returnValue = 0
                    Dim command2 As SqlCommand = New SqlCommand(commandText2, connection2)
                    returnValue = command2.ExecuteNonQuery()
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue)
                End Using
            End Using

            ' The Complete method commits the transaction. If an exception has been thrown,
            ' Complete is called and the transaction is rolled back.
            scope.Complete()
        End Using
    Catch ex As TransactionAbortedException
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message)
    End Try

    ' Display messages.
    Console.WriteLine(writer.ToString())

    Return returnValue
End Function

TransactionScope 개체를 만들면 트랜잭션 범위가 시작됩니다. 코드를 통해 설명된 대로 using 문장이 포함된 문장을 사용하여 범위를 만드는 것이 좋습니다. 이 using 문은 C# 및 Visual Basic에서 모두 사용할 수 있으며, 범위가 제대로 삭제되었는지 확인하기 위해 try...finally 블록처럼 작동합니다.

인스턴스화 TransactionScope할 때 트랜잭션 관리자는 참여할 트랜잭션을 결정합니다. 일단 결정되면 범위는 항상 해당 트랜잭션에 참여합니다. 이 결정은 앰비언트 트랜잭션이 있는지 여부와 생성자의 매개 변수 값 TransactionScopeOption 이라는 두 가지 요소를 기반으로 합니다. 앰비언트 트랜잭션은 코드가 실행되는 트랜잭션입니다. 클래스의 정적 Transaction.Current 속성을 호출하여 앰비언트 트랜잭션에 대해 참조를 가져올 수 있습니다. 이 매개 변수를 사용하는 방법에 대한 자세한 내용은 이 항목의 TransactionScopeOption을 사용하여 트랜잭션 흐름 관리를 참조하세요.

트랜잭션 범위를 완료하기

애플리케이션이 트랜잭션에서 수행하려는 모든 작업을 완료하는 경우 트랜잭션 관리자에게 트랜잭션을 커밋할 수 있음을 알리기 위해 메서드를 한 번만 호출 TransactionScope.Complete 해야 합니다. Complete 호출을 using 블록의 마지막 문으로 두는 것이 매우 좋은 방법입니다.

트랜잭션 관리자가 이를 시스템 오류로 해석하거나 트랜잭션 범위 내에서 throw된 예외와 동일하므로 이 메서드를 호출하지 않으면 트랜잭션이 중단됩니다. 그러나 이 메서드를 호출해도 트랜잭션이 커밋된다는 보장은 없습니다. 트랜잭션 관리자에게 상태를 알리는 방법일 뿐입니다. 메서드를 호출한 Complete 후에는 더 이상 속성을 사용하여 Current 앰비언트 트랜잭션에 액세스할 수 없으며, 이렇게 시도하면 예외가 throw됩니다.

개체가 TransactionScope 처음에 트랜잭션을 만든 경우 트랜잭션 관리자가 트랜잭션을 커밋하는 실제 작업은 블록의 마지막 코드 using 줄 이후에 발생합니다. 트랜잭션을 만들지 않은 경우, 개체 소유자가 Commit를 호출할 때마다 CommittableTransaction의 커밋이 발생합니다. 이때 트랜잭션 관리자는 리소스 관리자를 호출하고, Complete 객체에 TransactionScope 메서드가 호출되었는지 여부에 따라 커밋하거나 롤백하도록 알립니다.

using 문은 예외가 발생하더라도 Dispose 객체의 TransactionScope 메서드가 호출되도록 보장합니다. 메서드는 Dispose 트랜잭션 범위의 끝을 표시합니다. 이 메서드를 호출한 후 발생하는 예외는 트랜잭션에 영향을 미치지 않을 수 있습니다. 또한 이 메서드는 앰비언트 트랜잭션을 이전 상태로 복원합니다.

트랜잭션이 범위 내에서 생성되고 중단되면 TransactionAbortedException가 던져집니다. 트랜잭션 관리자가 커밋 결정에 도달할 수 없는 경우 A TransactionInDoubtException 가 throw됩니다. 트랜잭션이 커밋된 경우 예외가 발생하지 않습니다.

트랜잭션 롤백

트랜잭션을 롤백하려는 경우 트랜잭션 범위 내에서 메서드를 Complete 호출하면 안 됩니다. 예를 들어 범위 내에서 예외를 발생시킬 수 있습니다. 참여하는 트랜잭션은 롤백됩니다.

TransactionScopeOption을 사용하여 트랜잭션 흐름 관리

트랜잭션 범위는, 다음 예제에서 TransactionScope 메서드 경우와 같이, RootMethod 자체 범위를 사용하는 메서드 내에서 사용하는 메서드를 호출하여 중첩될 수 있습니다.

void RootMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        SomeMethod();
        scope.Complete();
    }
}

void SomeMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        scope.Complete();
    }
}

최상위 트랜잭션 범위를 루트 범위라고 합니다.

클래스는 TransactionScope 범위의 트랜잭션 동작을 정의하는 형식 TransactionScopeOption의 열거를 허용하는 여러 오버로드된 생성자를 제공합니다.

TransactionScope 개체에는 다음 세 가지 옵션이 있습니다.

  • 앰비언트 트랜잭션을 조인하거나 없는 경우 새 트랜잭션을 만듭니다.

  • 새 루트 범위, 즉 새 트랜잭션을 시작하고 해당 트랜잭션이 자체 범위 내의 새 앰비언트 트랜잭션이 되도록 합니다.

  • 트랜잭션에 전혀 참여하지 않습니다. 결과적으로 앰비언트 트랜잭션이 없습니다.

Required로 범위가 인스턴스화되고 앰비언트 트랜잭션이 있는 경우, 범위는 해당 트랜잭션에 참여합니다. 반면에 앰비언트 트랜잭션이 없는 경우 범위는 새 트랜잭션을 만들고 루트 범위가 됩니다. 기본값입니다. 사용되는 경우 Required 범위 내의 코드는 루트인지 아니면 앰비언트 트랜잭션에 조인하는지와 다르게 동작할 필요가 없습니다. 두 경우 모두 동일하게 작동해야 합니다.

범위가 인스턴스화되면 RequiresNew항상 루트 범위입니다. 새 트랜잭션을 시작하고 해당 트랜잭션은 범위 내의 새 앰비언트 트랜잭션이 됩니다.

범위가 Suppress로 인스턴스화되면, 앰비언트 트랜잭션이 있는지 여부에 관계없이 트랜잭션에 절대 참여하지 않습니다. 이 값으로 인스턴스화된 범위는 항상 null을 앰비언트 트랜잭션으로 갖습니다.

위의 옵션은 다음 표에 요약되어 있습니다.

트랜잭션 범위 옵션 앰비언트 트랜잭션 범위가 포함됩니다.
필수 아니오 새 트랜잭션(루트가 됩니다)
새로 필요함 아니오 새 트랜잭션(루트가 됩니다)
억제하다 아니오 트랜잭션 없음
필수 앰비언트 트랜잭션
새로 필요함 새 트랜잭션(루트가 됩니다)
억제하다 트랜잭션 없음

개체가 TransactionScope 기존 앰비언트 트랜잭션을 조인하는 경우 범위가 트랜잭션을 중단하지 않는 한 범위 개체를 삭제해도 트랜잭션이 종료되지 않을 수 있습니다. 루트 범위에서 앰비언트 트랜잭션이 생성된 경우, 루트 범위가 삭제될 때만 해당 트랜잭션에서 Commit가 호출됩니다. 트랜잭션을 수동으로 만든 경우 트랜잭션이 중단되거나 작성자가 커밋할 때 종료됩니다.

다음 예제는 각각 다른 TransactionScope 값으로 인스턴스화된 세 개의 중첩된 범위 개체를 생성하는 TransactionScopeOption 개체를 보여줍니다.

using(TransactionScope scope1 = new TransactionScope())
//Default is Required
{
    using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
    {
        //...
    }

    using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        //...  
    }
  
    using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
    {
        //...  
    }
}

이 예제에서는 앰비언트 트랜잭션이 없는 코드 블록을 사용하여 새 범위(scope1)를 만드는 방법을 보여 줍니다 Required. 범위 scope1 는 새 트랜잭션(트랜잭션 A)을 만들고 트랜잭션 A를 앰비언트 트랜잭션으로 만들 때 루트 범위입니다. Scope1 그런 다음 각각 다른 TransactionScopeOption 값을 가진 세 개의 개체를 더 만듭니다. 예를 들어 scope2Required와 함께 생성되고, 이때 앰비언트 트랜잭션이 있어 scope1에 의해 생성된 첫 번째 트랜잭션에 결합됩니다. 새 scope3 트랜잭션의 루트 범위이며 scope4 앰비언트 트랜잭션이 없습니다.

기본값과 가장 일반적으로 사용되는 값 TransactionScopeOption 은 있지만 Required다른 각 값에는 고유한 용도가 있습니다.

트랜잭션 범위 내의 비트랜잭션 코드

Suppress 는 코드 섹션에서 수행하는 작업을 유지하려는 경우 유용하며 작업이 실패하는 경우 앰비언트 트랜잭션을 중단하지 않으려는 경우에 유용합니다. 예를 들어 로깅 또는 감사 작업을 수행하려는 경우 또는 앰비언트 트랜잭션 커밋 또는 중단 여부에 관계없이 구독자에 이벤트를 게시하려는 경우입니다. 이 값을 사용하면 다음 예제와 같이 트랜잭션 범위 내에 비트랜잭션 코드 섹션을 사용할 수 있습니다.

using(TransactionScope scope1 = new TransactionScope())
{
    try
    {
        //Start of non-transactional section
        using(TransactionScope scope2 = new
            TransactionScope(TransactionScopeOption.Suppress))  
        {  
            //Do non-transactional work here  
        }  
        //Restores ambient transaction here
   }
   catch {}  
   //Rest of scope1
}

중첩된 범위 내의 투표

중첩된 범위는 루트 범위의 앰비언트 트랜잭션을 조인할 수 있지만 중첩된 범위에서 호출 Complete 해도 루트 범위에는 영향을 주지 않습니다. 트랜잭션은 루트 범위부터 마지막 중첩된 범위까지 모든 범위가 트랜잭션을 커밋하도록 투표하는 경우에만 커밋됩니다. 중첩된 범위에서 호출 Complete 하지 않으면 앰비언트 트랜잭션이 즉시 중단되므로 루트 범위에 영향을 미칩니다.

TransactionScope 시간 제한 설정

오버로드된 생성자 중 TransactionScope 일부는 트랜잭션의 시간 제한을 제어하는 데 사용되는 형식 TimeSpan값을 허용합니다. 0으로 설정된 시간 제한은 무한 시간 제한을 의미합니다. 무한 시간 제한은 코드를 단계별로 실행하여 비즈니스 논리에서 문제를 격리하려는 경우 디버깅에 주로 유용하며, 문제를 찾는 동안 디버그하는 트랜잭션의 시간이 초과되지 않도록 합니다. 트랜잭션 교착 상태에 대한 보호 조치를 재정의하므로 다른 모든 경우에 무한 시간 제한 값을 사용하는 데 매우 주의해야 합니다.

일반적으로 두 경우에 TransactionScope 타임아웃을 기본값이 아닌 다른 값으로 설정합니다. 첫 번째는 개발 중에 애플리케이션이 중단된 트랜잭션을 처리하는 방법을 테스트하려는 경우입니다. 시간 제한을 작은 값(예: 1밀리초)으로 설정하면 트랜잭션이 실패하여 오류 처리 코드를 관찰할 수 있습니다. 값을 기본 시간 제한보다 작게 설정하는 두 번째 경우는 범위가 리소스 경합에 관여하여 교착 상태가 발생하는 경우입니다. 이 경우 가능한 한 빨리 트랜잭션을 중단하고 기본 시간 제한이 만료되기를 기다리지 않습니다.

범위가 앰비언트 트랜잭션을 조인하지만 앰비언트 트랜잭션이 설정된 시간 제한보다 작은 시간 제한을 지정하는 경우 개체에 TransactionScope 더 짧은 새 시간 제한이 적용되고 범위가 지정된 중첩된 시간 내에 끝나야 하거나 트랜잭션이 자동으로 중단됩니다. 중첩된 범위의 시간 제한이 앰비언트 트랜잭션보다 많으면 효과가 없습니다.

TransactionScope 격리 수준 설정

오버로드된 생성자 중 TransactionScope 일부는 시간 제한 값 외에도 격리 수준을 지정하기 위해 형식 TransactionOptions 구조를 수락합니다. 기본적으로 트랜잭션은 격리 수준이 .로 Serializable설정된 상태에서 실행됩니다. 읽기 집약적 시스템에 일반적으로 사용되는 격리 Serializable 수준 이외의 격리 수준을 선택합니다. 이를 위해서는 트랜잭션 처리 이론과 트랜잭션 자체의 의미 체계, 관련된 동시성 문제 및 시스템 일관성에 대한 결과를 확실하게 이해해야 합니다.

또한 모든 리소스 관리자가 모든 수준의 격리를 지원하는 것은 아니며 구성된 것보다 높은 수준에서 트랜잭션에 참여하도록 선택할 수 있습니다.

그 외의 Serializable 모든 격리 수준은 동일한 정보에 액세스하는 다른 트랜잭션으로 인한 불일치에 취약합니다. 다른 격리 수준 간의 차이는 읽기 및 쓰기 잠금을 사용하는 방식입니다. 트랜잭션이 리소스 관리자의 데이터에 액세스하는 경우에만 잠금을 유지하거나 트랜잭션이 커밋되거나 중단될 때까지 잠금을 보유할 수 있습니다. 전자는 처리량에 더 좋으며, 후자는 일관성을 위해 더 좋습니다. 두 종류의 잠금과 두 종류의 작업(읽기/쓰기)은 네 가지 기본 격리 수준을 제공합니다. 자세한 내용은 IsolationLevel을 참조하세요.

중첩된 TransactionScope 개체를 사용하는 경우 앰비언트 트랜잭션에 조인하려는 경우 정확히 동일한 격리 수준을 사용하도록 모든 중첩된 범위를 구성해야 합니다. 중첩된 TransactionScope 개체가 앰비언트 트랜잭션에 참여하려고 할 때 다른 격리 수준을 지정할 경우 예외 ArgumentException가 발생합니다.

COM+와의 상호운용성

TransactionScope 인스턴스를 만들 때 생성자 중 하나의 열거형을 사용하여 EnterpriseServicesInteropOption COM+와 상호 작용하는 방법을 지정할 수 있습니다. 이에 대한 자세한 내용은 Enterprise Services 및 COM+ 트랜잭션과의 상호 운용성을 참조하세요.

참고하십시오