SQL Server와 System.Transactions의 통합(ADO.NET)
업데이트: November 2007
.NET Framework 버전 2.0에는 System.Transactions 네임스페이스를 통해 액세스할 수 있는 트랜잭션 프레임워크가 추가되었습니다. 이 프레임워크는 ADO.NET을 포함하여 .NET Framework에 완전히 통합된 방식으로 트랜잭션을 노출시킵니다.
프로그래밍 기능이 향상된 것 외에도 System.Transactions 및 ADO.NET이 함께 작동하여 트랜잭션 사용 시 최적화되도록 조정할 수 있습니다. 승격 가능한 트랜잭션이란 필요에 따라 완전 분산 트랜잭션으로 자동 승격될 수 있는 간단한(로컬) 트랜잭션입니다.
ADO.NET 2.0부터 System.Data.SqlClient에서는 SQL Server 2005 사용 시 승격 가능한 트랜잭션이 지원됩니다. 승격 가능한 트랜잭션은 추가 오버헤드가 필요한 경우를 제외하고 분산 트랜잭션의 추가 오버헤드를 호출하지 않습니다. 또한 승격 가능한 트랜잭션은 개발자의 개입 없이 자동으로 수행됩니다.
승격 가능한 트랜잭션은 SQL Server 2005에서 .NET Framework Data Provider for SQL Server(SqlClient)를 사용할 때만 사용할 수 있습니다.
승격 가능한 트랜잭션 만들기
.NET Framework Provider for SQL Server는 승격 가능한 트랜잭션을 지원합니다. 승격 가능한 트랜잭션은 .NET Framework System.Transactions 네임스페이스에서 클래스를 통해 처리됩니다. 승격 가능한 트랜잭션은 필요할 때까지 분산 트랜잭션 만들기를 연기하여 분산 트랜잭션을 최적화합니다. 리소스 관리자만 필요할 경우 분산 트랜잭션은 발생하지 않습니다.
참고: |
---|
부분적으로 신뢰할 수 있는 시나리오에서 트랜잭션을 분산 트랜잭션으로 승격시키려면 DistributedTransactionPermission이 필요합니다. 자세한 내용은 트랜잭션 관리 에스컬레이션을 참조하십시오. |
승격 가능한 트랜잭션 시나리오
분산 트랜잭션에서는 일반적으로 MS DTC(Microsoft Distributed Transaction Coordinator)를 통해 관리되는 많은 양의 시스템 리소스를 사용합니다. MS DTC는 트랜잭션에서 액세스하는 모든 리소스 관리자를 통합합니다. 승격 가능한 트랜잭션은 실제로 작업을 단순 SQL Server 2005 트랜잭션에 효과적으로 위임하는 System.Transactions 트랜잭션의 특수한 형태입니다. System.Transactions, System.Data.SqlClient 및 SQL Server 2005는 트랜잭션 처리 관련 작업을 조정하며 필요에 따라 이 트랜잭션을 완전 분산 트랜잭션으로 승격시킵니다.
승격 가능한 트랜잭션을 사용하면 활성 TransactionScope 트랜잭션을 사용하여 연결이 열리고, 다른 연결이 열려 있지 않은 경우 완전 분산 트랜잭션의 추가 오버헤드를 발생시키는 대신 간단한 트랜잭션으로 커밋됩니다.
연결 문자열 키워드
ConnectionString 속성에서는 System.Data.SqlClient에서 트랜잭션 컨텍스트를 찾은 후 분산 트랜잭션에 연결을 자동으로 인리스트먼트할지 여부를 나타내는 키워드인 Enlist를 지원합니다. Enlist=true인 경우 연결이 열려 있는 스레드의 현재 트랜잭션 컨텍스트에 자동으로 인리스트먼트됩니다. Enlist=false인 경우에는 SqlClient 연결이 분산 트랜잭션과 상호 작용하지 않습니다. Enlist의 기본값은 true입니다. 연결 문자열에 Enlist가 지정되지 않으면 연결이 열릴 때 감지된 연결이 분산 트랜잭션에 자동으로 인리스트먼트됩니다.
SqlConnection 연결 문자열의 Transaction Binding 키워드는 인리스트먼트된 System.Transactions 트랜잭션과 연결 사이의 관계를 제어합니다. 이 키워드는 SqlConnectionStringBuilder의 TransactionBinding 속성을 통해서도 사용할 수 있습니다.
다음 표에서는 사용 가능한 값을 설명합니다.
키워드 |
설명 |
---|---|
Implicit Unbind |
기본값입니다. 트랜잭션이 끝나면 자동으로 연결이 끊어지고 자동 커밋 모드로 전환됩니다. |
Explicit Unbind |
트랜잭션을 닫을 때까지 트랜잭션에 대한 연결이 유지됩니다. 연결된 트랜잭션이 활성 상태가 아니거나 Current와 일치하지 않으면 연결이 실패합니다. |
자세한 내용은 트랜잭션 범위를 사용하여 암시적 트랜잭션 구현을 참조하십시오.
TransactionScope 사용
TransactionScope 클래스는 분산 트랜잭션에 연결을 암시적으로 인리스트먼트하여 코드 블록을 트랜잭션에 사용할 수 있도록 합니다. Complete 메서드는 TransactionScope 블록을 벗어나기 전에 이 블록의 끝 부분에서 호출해야 합니다. 블록을 벗어나면 Dispose 메서드가 호출됩니다. 예외가 throw되어 코드가 범위를 벗어날 경우 트랜잭션이 중단된 것으로 간주됩니다.
using 블록을 사용하여 해당 블록을 종료할 때 Dispose가 TransactionScope 개체에서 호출되도록 하는 것이 좋습니다. 보류 중인 트랜잭션을 커밋하거나 롤백하지 못하면 TransactionScope의 기본 시간 제한이 1분이므로 성능이 크게 저하될 수 있습니다. using 문을 사용하지 않는 경우에는 Try 블록의 모든 작업을 수행하고 Finally 블록의 Dispose 메서드를 명시적으로 호출해야 합니다.
TransactionScope에서 예외가 발생하면 트랜잭션이 일관성이 없는 것으로 표시되어 중단됩니다. 또한 TransactionScope가 삭제되면 트랜잭션이 롤백됩니다. 예외가 발생하지 않으면 참여하는 트랜잭션이 커밋됩니다.
참고: |
---|
기본적으로 TransactionScope 클래스는 IsolationLevel이 Serializable인 트랜잭션을 만듭니다. 응용 프로그램에 따라 응용 프로그램에서 경합을 줄이기 위해 격리 수준을 낮추는 방안을 고려해 볼 수 있습니다. |
참고: |
---|
업데이트, 삽입 및 삭제는 데이터베이스 리소스를 많이 사용하므로 분산 트랜잭션에서는 이러한 작업만 수행하는 것이 좋습니다. SELECT 문은 데이터베이스 리소스를 불필요하게 잠글 수 있으며, 경우에 따라서는 선택 시 트랜잭션을 사용해야 할 수도 있습니다. 다른 트랜잭트 리소스 관리자를 사용해야 하는 경우가 아니면 데이터베이스 작업이 아닌 다른 작업은 트랜잭션 범위 밖에서 수행해야 합니다. 트랜잭션 범위에서 예외를 통해 트랜잭션이 커밋되는 것을 금지하고 있지만, TransactionScope 클래스에는 트랜잭션 자체 범위를 벗어난 코드 변경을 롤백할 수 있는 규정이 없습니다. 트랜잭션이 롤백될 때 특별한 조치를 취해야 하는 경우에는 고유한 IEnlistmentNotification 인터페이스 구현을 작성한 후 트랜잭션에 명시적으로 인리스트먼트해야 합니다. |
예제
System.Transactions를 사용하려면 System.Transactions.dll을 참조해야 합니다.
다음 함수는 서로 다른 두 SqlConnection 개체(TransactionScope 블록에 래핑됨)로 표현되는 두 개의 서로 다른 SQL Server 인스턴스에 대해 승격 가능한 트랜잭션을 만드는 방법을 보여 줍니다. 이 코드에서는 using 문을 사용하여 TransactionScope 블록을 만든 후 첫 번째 연결을 엽니다. 그리고 나면 이 연결은 TransactionScope에 자동으로 참여합니다. 이 트랜잭션은 처음에 완전 분산 트랜잭션이 아니라 간단한 트랜잭션으로 인리스트먼트됩니다. 두 번째 연결은 첫 번째 연결의 명령이 예외를 throw하지 않을 경우에만 TransactionScope에 인리스트먼트됩니다. 두 번째 연결이 열리면 트랜잭션이 완전 분산 트랜잭션으로 자동 승격됩니다. 예외가 throw되지 않은 경우에만 트랜잭션을 커밋하는 Complete 메서드가 호출됩니다. TransactionScope 블록의 한 지점에서 예외가 throw된 경우에는 Complete가 호출되지 않으며 using 블록 끝에서 TransactionScope가 삭제되면 분산 트랜잭션이 롤백됩니다.
' This function takes arguments for the 2 connection strings and commands in order
' to create a transaction involving two SQL Servers. It returns a value > 0 if the
' transaction committed, 0 if the transaction 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 RDBMS such as Oracle 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
' Create the TransactionScope in which to execute the commands, guaranteeing
' that both commands will commit or roll back as a single unit of work.
Using scope As New TransactionScope()
Using connection1 As New SqlConnection(connectString1)
Try
' 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 by opening connection2
' only when there is a chance that the transaction can commit.
Using connection2 As New SqlConnection(connectString2)
Try
' The transaction is promoted 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)
Catch ex As Exception
' Display information that command2 failed.
writer.WriteLine("returnValue for command2: {0}", returnValue)
writer.WriteLine("Exception Message2: {0}", ex.Message)
End Try
End Using
Catch ex As Exception
' Display information that command1 failed.
writer.WriteLine("returnValue for command1: {0}", returnValue)
writer.WriteLine("Exception Message1: {0}", ex.Message)
End Try
End Using
' If an exception has been thrown, Complete will
' not be called and the transaction is rolled back.
scope.Complete()
End Using
' The returnValue is greater than 0 if the transaction committed.
If returnValue > 0 Then
writer.WriteLine("Transaction was committed.")
Else
' You could write additional business logic here, notify the caller by
' throwing a TransactionAbortedException, or log the failure.
writer.WriteLine("Transaction rolled back.")
End If
' Display messages.
Console.WriteLine(writer.ToString())
Return returnValue
End Function
// This function takes arguments for the 2 connection strings and commands in order
// to create a transaction involving two SQL Servers. It returns a value > 0 if the
// transaction committed, 0 if the transaction 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 RDBMS such as Oracle 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();
// Create the TransactionScope in which to execute the commands, guaranteeing
// that both commands will commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
try
{
// 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 by opening connection2
// only when there is a chance that the transaction can commit.
using (SqlConnection connection2 = new SqlConnection(connectString2))
try
{
// The transaction is promoted 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);
}
catch (Exception ex)
{
// Display information that command2 failed.
writer.WriteLine("returnValue for command2: {0}", returnValue);
writer.WriteLine("Exception Message2: {0}", ex.Message);
}
}
catch (Exception ex)
{
// Display information that command1 failed.
writer.WriteLine("returnValue for command1: {0}", returnValue);
writer.WriteLine("Exception Message1: {0}", ex.Message);
}
}
// If an exception has been thrown, Complete will not
// be called and the transaction is rolled back.
scope.Complete();
}
// The returnValue is greater than 0 if the transaction committed.
if (returnValue > 0)
{
writer.WriteLine("Transaction was committed.");
}
else
{
// You could write additional business logic here, notify the caller by
// throwing a TransactionAbortedException, or log the failure.
writer.WriteLine("Transaction rolled back.");
}
// Display messages.
Console.WriteLine(writer.ToString());
return returnValue;
}