트랜잭션 작업

참고 항목

EF6 이상만 - 이 페이지에서 다루는 기능, API 등은 Entity Framework 6에 도입되었습니다. 이전 버전을 사용하는 경우 이 정보의 일부 또는 전체가 적용되지 않습니다.

이 문서에서는 수월한 트랜잭션 작업을 위해 EF5부터 추가된 개선 사항을 포함하여 EF6에서 트랜잭션을 사용하는 방법을 설명합니다.

EF가 기본적으로 수행하는 작업

모든 버전의 Entity Framework에서 SaveChanges()를 실행하여 데이터베이스에 삽입, 업데이트 또는 삭제할 때마다 프레임워크는 트랜잭션에 해당 작업을 래핑합니다. 이 트랜잭션은 작업을 실행할 수 있을 만큼 지속된 다음 완료됩니다. 다른 작업을 실행하면 새 트랜잭션이 시작됩니다.

EF6 Database.ExecuteSqlCommand()를 시작하면 기본적으로 명령이 아직 없는 경우 트랜잭션에 명령을 래핑합니다. 원하는 경우 이 동작을 재정의할 수 있는 이 메서드의 오버로드가 있습니다. 또한 EF6에서 ObjectContext.ExecuteFunction()과 같은 API를 통해 모델에 포함된 저장 프로시저를 실행할 때도 동일한 작업을 수행합니다(현재 기본 동작을 재정의할 수 없다는 점 제외).

두 경우 모두 트랜잭션의 격리 수준은 데이터베이스 공급자가 기본 설정으로 간주하는 격리 수준입니다. 예를 들어 기본적으로 SQL Server에서는 READ COMMITTED입니다.

Entity Framework는 트랜잭션에 쿼리를 래핑하지 않습니다.

이 기본 기능은 많은 사용자에게 적합하며, 이 기능을 사용할 경우 EF6에서는 다른 작업을 수행할 필요가 없고 평소와 같이 코드를 작성하기만 하면 됩니다.

그러나 일부 사용자는 트랜잭션을 보다 세밀하게 컨트롤해야 합니다. 이 내용은 다음 섹션에서 설명합니다.

API 작동 방식

EF6 Entity Framework 이전에는 데이터베이스 연결 자체를 열어야 했습니다(이미 열려 있는 연결을 통과한 경우 예외 발생). 열려 있는 연결에서만 트랜잭션을 시작할 수 있으므로 이 말은 곧 사용자가 여러 작업을 하나의 트랜잭션으로 래핑할 수 있는 유일한 방법은 TransactionScope를 사용하거나 ObjectContext.Connection 속성을 사용하고 반환된 BeginTransaction() 개체에서 직접 Open()BeginTransaction() 호출을 시작하는 것임을 의미합니다. 또한 기본 데이터베이스 연결에서 직접 트랜잭션을 시작한 경우 데이터베이스에 연결한 API 호출이 실패합니다.

참고 항목

Entity Framework 6에서는 닫힌 연결만 허용하는 제한 사항이 제거되었습니다. 자세한 내용은 연결 관리를 참조하세요.

EF6부터는 이제 프레임워크에서 다음을 제공합니다.

  1. Database.BeginTransaction(): 사용자가 보다 간편하게 기존 DbContext 내에서 트랜잭션을 직접 시작하고 완료할 수 있는 방법입니다. 여러 작업을 동일한 트랜잭션 내에서 결합할 수 있으므로 모두 커밋되거나 모두 하나로 롤백됩니다. 또한 사용자가 트랜잭션에 대한 격리 수준을 보다 쉽게 지정할 수 있습니다.
  2. Database.UseTransaction(): DbContext가 Entity Framework 외부에서 시작된 트랜잭션을 사용할 수 있도록 합니다.

여러 작업을 동일한 컨텍스트 내 하나의 트랜잭션으로 결합

Database.BeginTransaction()에는 두 가지 재정의가 있습니다. 하나는 명시적 IsolationLevel을 사용하는 재정의이고, 다른 하나는 인수를 사용하지 않고 기본 데이터베이스 공급자의 기본 IsolationLevel을 사용하는 재정의입니다. 두 재정의 모두 기본 저장소 트랜잭션에서 커밋과 롤백을 수행하는 Commit()Rollback() 메서드를 제공하는 DbContextTransaction 개체를 반환합니다.

DbContextTransaction은 커밋 또는 롤백 후에 삭제됩니다. 이 작업을 간편하게 수행하는 한 가지 방법은 using 블록이 완료되면 Dispose()를 자동으로 호출하는 using(...) {...} 구문입니다.

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void StartOwnTransactionWithinContext()
        {
            using (var context = new BloggingContext())
            {
                using (var dbContextTransaction = context.Database.BeginTransaction())
                {
                    context.Database.ExecuteSqlCommand(
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'"
                        );

                    var query = context.Posts.Where(p => p.Blog.Rating >= 5);
                    foreach (var post in query)
                    {
                        post.Title += "[Cool Blog]";
                    }

                    context.SaveChanges();

                    dbContextTransaction.Commit();
                }
            }
        }
    }
}

참고 항목

트랜잭션을 시작하려면 기본 저장소 연결이 열려 있어야 합니다. 따라서 연결이 아직 열려 있지 않은 경우 Database.BeginTransaction()을 호출하면 연결을 열 수 있습니다. DbContextTransaction이 연결을 열었다면 Dispose()를 호출하여 연결을 닫을 수 있습니다.

컨텍스트에 기존 트랜잭션 전달

경우에 따라 범위가 훨씬 더 넓고 동일한 데이터베이스에 대한 작업이 포함되지만 완전히 EF 외부에 있는 트랜잭션을 사용하고자 할 수 있습니다. 이렇게 하려면 연결을 열고 직접 트랜잭션을 시작한 다음 EF에 a) 이미 열려 있는 데이터베이스 연결을 사용하고 b) 해당 연결에서 기존 트랜잭션을 사용하도록 지시해야 합니다.

이렇게 하려면 i) 기존 연결 매개 변수 및 ii) contextOwnsConnection 부울을 사용하는 DbContext 생성자 중 하나에서 상속되는 컨텍스트 클래스에서 생성자를 정의하고 사용해야 합니다.

참고 항목

이 시나리오에서 호출될 때 contextOwnsConnection 플래그를 false로 설정해야 합니다. 이 설정을 완료해야 연결되었을 때 연결을 닫지 말아야 함을 Entity Framework에 알릴 수 있습니다(예: 아래 4번째 줄 참조).

using (var conn = new SqlConnection("..."))
{
    conn.Open();
    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
    {
    }
}

또한 기본 설정을 하지 않으려면 IsolationLevel을 포함하여 직접 트랜잭션을 시작하고 연결에서 이미 시작된 기존 트랜잭션이 있음을 Entity Framework에 알려야 합니다(아래 33번째 줄 참조).

이렇게 하면 SqlConnection 자체 또는 DbContext에서 직접 데이터베이스 작업을 자유롭게 실행할 수 있습니다. 이러한 모든 작업은 하나의 트랜잭션 내에서 실행됩니다. 사용자는 트랜잭션을 커밋 또는 롤백한 후 해당 트랜잭션에 대해 Dispose()를 호출하고 데이터베이스 연결을 닫고 삭제해야 합니다. 예시:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
     class TransactionsExample
     {
        static void UsingExternalTransaction()
        {
            using (var conn = new SqlConnection("..."))
            {
               conn.Open();

               using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
               {
                   var sqlCommand = new SqlCommand();
                   sqlCommand.Connection = conn;
                   sqlCommand.Transaction = sqlTxn;
                   sqlCommand.CommandText =
                       @"UPDATE Blogs SET Rating = 5" +
                        " WHERE Name LIKE '%Entity Framework%'";
                   sqlCommand.ExecuteNonQuery();

                   using (var context =  
                     new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        context.Database.UseTransaction(sqlTxn);

                        var query =  context.Posts.Where(p => p.Blog.Rating >= 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                       context.SaveChanges();
                    }

                    sqlTxn.Commit();
                }
            }
        }
    }
}

트랜잭션 지우기

Database.UseTransaction()에 null을 전달하여 현재 트랜잭션에 대한 Entity Framework의 지식을 지울 수 있습니다. 이 작업을 수행할 때 Entity Framework는 기존 트랜잭션을 커밋하거나 롤백하지 않으므로 확실하게 원하는 작업일 경우에만 주의해서 사용하세요.

UseTransaction의 오류

다음과 같은 경우에 트랜잭션을 전달하면 Database.UseTransaction()에서 예외가 표시됩니다.

  • Entity Framework에 이미 기존 트랜잭션이 있는 경우
  • Entity Framework가 TransactionScope 내에서 이미 작동 중인 경우
  • 전달된 트랜잭션의 연결 개체가 null인 경우. 즉, 트랜잭션이 연결과 관련이 없습니다. 일반적으로 트랜잭션이 이미 완료되었다는 신호입니다.
  • 전달된 트랜잭션의 연결 개체가 Entity Framework의 연결과 일치하지 않는 경우

다른 기능과 함께 트랜잭션 사용

이 섹션에서는 위의 트랜잭션이 상호 작용하는 방법을 자세히 설명합니다.

  • 연결 복원력
  • 비동기 메서드
  • TransactionScope 트랜잭션

연결 복원력

새 연결 복원력 기능은 사용자가 시작한 트랜잭션에서 작동하지 않습니다. 자세한 내용은 재시도 실행 전략을 참조하세요.

비동기 프로그래밍

이전 섹션에서 설명한 접근 방식은 비동기 쿼리 및 저장 메서드를 사용하여 작업하기 위한 추가 옵션이나 설정이 필요하지 않습니다. 그러나 비동기 메서드 내에서 수행하는 작업에 따라 장기 실행 트랜잭션이 발생할 수 있으며, 이로 인해 교착 상태가 발생하거나 차단되어 전체 애플리케이션의 성능이 저하될 수 있습니다.

TransactionScope 트랜잭션

EF6 이전 버전에서는 더 큰 범위의 트랜잭션을 제공하기 위해 TransactionScope 개체를 사용하도록 권장했습니다.

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void UsingTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required))
            {
                using (var conn = new SqlConnection("..."))
                {
                    conn.Open();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    sqlCommand.ExecuteNonQuery();

                    using (var context =
                        new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                        context.SaveChanges();
                    }
                }

                scope.Complete();
            }
        }
    }
}

SqlConnection 및 Entity Framework는 모두 앰비언트 TransactionScope 트랜잭션을 사용하므로 함께 커밋됩니다.

.NET 4.5.1 TransactionScope부터는 TransactionScopeAsyncFlowOption 열거형을 사용하여 비동기 메서드로도 작동하도록 업데이트되었습니다.

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        public static void AsyncTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                using (var conn = new SqlConnection("..."))
                {
                    await conn.OpenAsync();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    await sqlCommand.ExecuteNonQueryAsync();

                    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }

                        await context.SaveChangesAsync();
                    }
                }
                
                scope.Complete();
            }
        }
    }
}

TransactionScope 접근 방식에는 여전히 몇 가지 제한 사항이 있습니다.

  • 비동기 메서드를 사용하려면 .NET 4.5.1 이상이 필요합니다.
  • 연결이 하나만 있는지 확실히 알 수 없으면 클라우드 시나리오에서 사용할 수 없습니다(클라우드 시나리오는 분산 트랜잭션을 지원하지 않음).
  • 이전 섹션의 Database.UseTransaction() 접근 방식과 결합할 수 없습니다.
  • DDL을 실행하고 MSDTC 서비스를 통해 분산 트랜잭션을 사용하도록 설정하지 않은 경우 예외가 throw됩니다.

TransactionScope 접근 방식의 이점:

  • 지정된 데이터베이스에 여러 연결을 만들거나 동일한 트랜잭션 내에서 한 데이터베이스에 대한 연결과 다른 데이터베이스에 대한 연결을 결합하는 경우, 로컬 트랜잭션을 분산 트랜잭션으로 자동으로 업그레이드합니다(참고: 분산 트랜잭션이 작동하도록 MSDTC 서비스를 구성해야 함).
  • 코딩이 쉽습니다. 트랜잭션을 명시적으로 컨트롤하지 않고 백그라운드에서 암시적으로 주변 처리되도록 하려면 TransactionScope 접근 방식이 더 적합할 수 있습니다.

간단히 말해서 위의 새 Database.BeginTransaction() 및 Database.UseTransaction() API를 사용하면 대부분의 사용자는 TransactionScope 접근 방식을 사용하지 않아도 됩니다. TransactionScope를 계속 사용하는 경우 위 제한 사항에 유의하세요. 가능한 경우 이전 섹션에 설명된 접근 방식을 대신 사용하는 것이 좋습니다.