파트 4: Entity Framework를 이용한 연결 복원 및 명령 가로채기

등록일시: 2016-04-04 08:00,  수정일시: 2016-04-11 09:06
조회수: 6,553
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
이번 파트에서는 Entity Framework의 연결 복원 기능을 살펴보고, 데이터베이스로 전송되는 SQL 명령을 로그로 기록하거나 변경하는 방법을 살펴봅니다.

전체 프로젝트 다운로드PDF 다운로드

본 자습서의 Contoso University 예제 웹 응용 프로그램은 Entity Framework 6와 Visual Studio 2013을 이용해서 ASP.NET MVC 5 응용 프로그램을 구축하는 방법을 보여줍니다. 보다 자세한 정보는 본 자습서 시리즈의 첫 번째 자습서를 참고하시기 바랍니다.

지금까지 본 자습서 시리즈의 예제 응용 프로그램은 여러분의 개발용 컴퓨터에 설치된 IIS Express에서 로컬로 실행되었습니다. 이 예제 응용 프로그램을 다른 사람들이 인터넷을 통해서 사용할 수 있는 현실적인 응용 프로그램으로 만들려면, 웹 호스팅 공급자가 제공해주는 서비스에 응용 프로그램을 배포하고 데이터베이스도 데이터베이스 서버에 배포해야 합니다.

이번 파트에서는 응용 프로그램을 클라우드 환경에 배포할 때 특히 유용한 Entity Framework 6의 두 가지 기능인, 연결 복원(Connection Resiliency: 일시적으로 오류가 발생한 경우 자동으로 작업을 다시 시도하는 기능)과 명령 가로채기(Command Interception: 데이터베이스로 전송되는 모든 SQL 질의를 가로채어 로그를 기록하거나 질의 자체에 변경을 가할 수 있는 기능)의 사용 방법을 살펴봅니다.

이 연결 복원 기능 및 명령 가로채기 기능에 관한 이번 자습서는 선택사항입니다. 다만 이번 파트를 건너뛰면 이어지는 자습서들의 내용에 몇 가지 간단한 차이점이 발생하게 되므로 감안하시기 바랍니다.

연결 복원 기능 활성화시키기

웹 응용 프로그램을 Windows Azure에 배포하는 경우, 데이터베이스도 클라우드 데이터베이스 서비스인 Windows Azure SQL Database에 배포하는 것이 일반적입니다. 그리고 일시적인 연결 오류는 이렇게 클라우드 데이터베이스 서비스를 사용할 때가, 동일한 데이터센터 내에 위치한 웹 서버와 데이터베이스 서버가 서로 직접 연결될 때보다 보다 더 빈번하게 발생합니다. 설령 클라우드 웹 서버와 클라우드 데이터베이스 서비스가 동일한 데이터센터 내에서 호스트 되는 경우에도, 그 중간에는 로드 밸런서 같이 문제가 발생할 가능성이 존재하는 더 많은 네트워크 연결들이 존재합니다.

일반적으로 클라우드 서비스는 다른 사용자들과 공유되는 경우가 많으므로, 다른 사용자들로 인해서 응답성에 안좋은 영향을 받을 가능성도 있습니다. 데이터베이스 접근 자체가 제한(Throttling)될 수도 있습니다. 이를테면 여러분이 체결한 서비스 수준 계약(SLA, Service Level Agreement)에서 허용하는 기준치보다 훨씬 빈번하게 접근을 시도할 경우, 데이터베이스 서비스에서 예외를 던질 수도 있습니다.

클라우드 서비스에 접근할 때 발생할 수 있는 연결과 관련된 많은 오류들은 대부분 일시적으로 발생하는 문제들이기 때문에 극히 짧은 시간이 흐르고 나면 자체적으로 문제가 해결되는 경우가 많습니다. 따라서 임의의 데이터베이스 작업을 시도했지만 전형적인 형태의 일시적인 오류가 발생했다면, 잠시 후 작업을 다시 시도할 수 있으며, 단지 그것만으로도 대부분 해당 작업은 성공할 것입니다. 결국 자동으로 작업을 다시 시도해서 일시적인 오류를 처리할 수만 있어도 사용자들 모르게 많은 오류들을 정상적으로 처리할 수 있으므로, 사용자들에게 보다 향상된 사용자 경험을 제공할 수 있습니다. 그리고 Entity Framework 6가 제공해주는 연결 복원 기능은 이런 실패한 SQL 질의를 처리하기 위한 재시도 절차를 자동화해줍니다.

연결 복원 기능은 대상 데이터베이스 서비스에 적합하게 구성되어야 합니다:

  • 일시적으로 발생할 가능성이 높은 예외가 어떤 것들인지 미리 알고 있어야 합니다. 애시당초 임시적인 네트워크 연결 손실로 인한 오류들을 재시도하려는 것이지, 프로그램 버그로 인한 손실을 재시도하려는 것은 아니기 때문입니다.
  • 적당한 간격을 두고 실패한 작업을 재시도해야 합니다. 가령, 현재 사용자가 응답을 기다리고 있을 온라인 웹 페이지에 대한 재시도 간격보다는, 배치 처리 시의 재시도 간격을 더 길게 설정할 수 있을 것입니다.
  • 작업을 포기하기 전까지 적절한 횟수를 재시도 해야 합니다. 온라인 응용 프로그램에서 재시도하는 횟수보다는 배치 처리를 수행할 때 재시도를 더 많이 할 수 있을 것입니다.

이런 설정들을 Entity Framework 공급자가 지원하는 모든 데이터베이스 환경에서 수작업으로 직접 구성할 수도 있습니다. 그러나 기본적으로 Windows Azure SQL Database를 사용하는 온라인 응용 프로그램 환경에서 정상적으로 잘 동작하는 기본값들이 이미 구성되어 있습니다. 본문에서는 바로 이 설정들을 Contoso University 응용 프로그램에서 실제로 구현해보고자 합니다.

연결 복원 기능을 활성화시키기 위해서는 간단히 어셈블리 내에 DbConfiguration 클래스로부터 파생된 클래스를 생성한 다음, 이 클래스에서 SQL Database 실행 전략(Execution Strategy) 을 설정하기만 하면 됩니다. 이는 재시도 정책(Retry Policy) 을 부르는 EF의 또 다른 말입니다.

  1. DAL 폴더에 SchoolConfiguration.cs 라는 이름의 클래스 파일을 추가합니다.

  2. 자동으로 생성된 템플릿 코드를 다음 코드로 대체합니다:

    using System.Data.Entity;
    using System.Data.Entity.SqlServer;
    
    namespace ContosoUniversity.DAL
    {
        public class SchoolConfiguration : DbConfiguration
        {
            public SchoolConfiguration()
            {
                SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            }
        }
    }

    Entity Framework는 DbConfiguration 클래스로부터 파생된 클래스를 찾으면 자동으로 그 코드를 실행합니다. 이 클래스를 이용해서 코드로 원하는 구성 작업을 수행하거나, 아니면 Web.config 파일에서 구성 작업을 수행할 수 있습니다. 여기에 관한 더 자세한 정보는 EntityFramework Code-Based Configuration 문서를 참고하시기 바랍니다.

  3. 이번에는 StudentController.cs 파일에 System.Data.Entity.Infrastructure 네임스페이스에 대한 using 문을 추가합니다.

    using System.Data.Entity.Infrastructure;
  4. 그리고 DataException 예외를 잡는 모든 catch 블럭을 찾아서 대신 다음과 같이 RetryLimitExceededException 예외를 잡도록 변경합니다:

    catch (RetryLimitExceededException /* dex */)
    {
        //Log the error (uncomment dex variable name and add a line here to write a log.
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
    }

    지금까지는 DataException 예외를 잡아서 일시적으로 발생할 수 있는 오류를 식별하고 사용자에게 "다시 시도하라"는 친철한 안내 메시지를 제공했습니다. 그러나 이제부터는 재시도 정책을 활성화시켰기 때문에, 발생하는 모든 일시적인 오류는 몇 차례 재도했으나 결국 실패한 오류들만 남게 되며, 발생한 실제 예외는 RetryLimitExceededException 예외에 감싸여 반환됩니다.

보다 자세한 관련 정보들은 Entity Framework Connection Resiliency / Retry Logic 문서를 참고하시기 바랍니다.

명령 가로채기 기능 활성화시키기

그런데 이전 절에서 활성화시킨 재시도 정책이 실제로 기대한 것처럼 동작하는지 여부는 어떻게 확인해야 할까요? 일시적인 오류를 일부러 발생시키는 일은, 응용 프로그램을 로컬에서 실행하고 있는 경우에는 특히 더 쉽지 않으며, 실제로 발생할 수 있는 일시적인 오류를 자동화된 단위 테스트에 통합시키는 일도 만만치 않은 작업입니다. 따라서 연결 복원 기능을 테스트하기 위해서는 Entity Framework가 SQL Server로 전송하는 질의를 가로채고 그 응답을 통상적으로 발생하는 일시적인 예외 형식으로 대체할 수 있는 방법이 필요합니다.

그리고 질의 가로채기 기능을 이용하면, 클라우드 응용 프로그램에서 데이터베이스 서비스 같은 외부 서비스를 대상으로 한 모든 호출들에 대한 대기 시간이나 성공 및 실패 여부를 매우 효과적으로 로그에 기록할 수 있습니다. EF6는 손쉽게 로그를 기록할 수 있게 지원해주는 전용 로깅 API도 지원해주지만, 본문에서는 직접 Entity Framework의 가로채기 기능을 이용해서 로그를 기록하고 일시적인 오류를 재현하는 방법에 관해서 살펴보겠습니다.

로깅 인터페이스 및 클래스 생성하기

로그를 기록하는 가장 좋은 방법은 System.Diagnostics.Trace 클래스나 로깅 클래스를 하드 코딩으로 호출하는 대신, 인터페이스를 사용하는 것입니다. 이 방식을 사용하면 추후 로깅 메커니즘을 변경해야 하는 상황이 발생하더라도 작업이 훨씬 수월합니다. 이번 절에서도 먼저 로깅 인터페이스를 생성한 다음, 이 인터페이스를 구현하는 클래스를 작성해볼 것입니다.

  1. 프로젝트에 Logging 이라는 이름으로 새 폴더를 생성합니다.

  2. 그리고 Logging 폴더에 ILogger.cs 라는 이름으로 클래스 파일을 생성한 다음, 자동으로 생성된 템플릿 코드를 다음 코드로 대체합니다:

    using System;
    
    namespace ContosoUniversity.Logging
    {
        public interface ILogger
        {
            void Information(string message);
            void Information(string fmt, params object[] vars);
            void Information(Exception exception, string fmt, params object[] vars);
    
            void Warning(string message);
            void Warning(string fmt, params object[] vars);
            void Warning(Exception exception, string fmt, params object[] vars);
    
            void Error(string message);
            void Error(string fmt, params object[] vars);
            void Error(Exception exception, string fmt, params object[] vars);
    
            void TraceApi(string componentName, string method, TimeSpan timespan);
            void TraceApi(string componentName, string method, TimeSpan timespan, string properties);
            void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars);
        }
    }

    이 인터페이스는 로그의 상대적인 중요도를 나타내기 위한 세 가지 추적 수준과, 데이터베이스 질의 같은 외부 서비스 호출에 대한 대기 시간 정보를 제공하기 위해 설계된 하나의 메서드을 제공하고 있습니다. 각 로깅 메서드들은 예외를 전달할 수 있는 오버로드 된 버전을 갖고 있습니다. 이 메서드는 응용 프로그램 내부의 여기 저기에서 로깅 메서드를 호출할 때마다 매번 개별적으로 예외 정보를 수집하는 대신, 이 인터페이스를 구현한 클래스를 통해서 스택 트레이스 및 내부 예외를 비롯한 신뢰할 수 있는 예외 정보를 기록하기 위한 것입니다.

    그리고 TraceApi 메서드를 이용하면 SQL 데이터베이스 같은 외부 서비스들에 대한 각각의 호출 대기 시간을 추적할 수 있습니다.

  3. 이번에는 Logging 폴더에 Logger.cs 라는 이름의 클래스 파일을 생성한 다음, 자동으로 생성된 템플릿 코드를 다음 코드로 대체합니다:

    using System;
    using System.Diagnostics;
    using System.Text;
    
    namespace ContosoUniversity.Logging
    {
        public class Logger : ILogger
        {
            public void Information(string message)
            {
                Trace.TraceInformation(message);
            }
    
            public void Information(string fmt, params object[] vars)
            {
                Trace.TraceInformation(fmt, vars);
            }
    
            public void Information(Exception exception, string fmt, params object[] vars)
            {
                Trace.TraceInformation(FormatExceptionMessage(exception, fmt, vars));
            }
    
            public void Warning(string message)
            {
                Trace.TraceWarning(message);
            }
    
            public void Warning(string fmt, params object[] vars)
            {
                Trace.TraceWarning(fmt, vars);
            }
    
            public void Warning(Exception exception, string fmt, params object[] vars)
            {
                Trace.TraceWarning(FormatExceptionMessage(exception, fmt, vars));
            }
    
            public void Error(string message)
            {
                Trace.TraceError(message);
            }
    
            public void Error(string fmt, params object[] vars)
            {
                Trace.TraceError(fmt, vars);
            }
    
            public void Error(Exception exception, string fmt, params object[] vars)
            {
                Trace.TraceError(FormatExceptionMessage(exception, fmt, vars));
            }
    
            public void TraceApi(string componentName, string method, TimeSpan timespan)
            {
                TraceApi(componentName, method, timespan, ""); 
            }
    
            public void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars)
            {
                TraceApi(componentName, method, timespan, string.Format(fmt, vars));
            }
    
            public void TraceApi(string componentName, string method, TimeSpan timespan, string properties)
            {
                string message = String.Concat("Component:", componentName, ";Method:", method, ";Timespan:", timespan.ToString(), ";Properties:", properties);
                Trace.TraceInformation(message);
            }
    
            private static string FormatExceptionMessage(Exception exception, string fmt, object[] vars)
            {
                // Simple exception formatting: for a more comprehensive version see 
                // http://code.msdn.microsoft.com/windowsazure/Fix-It-app-for-Building-cdd80df4
                var sb = new StringBuilder();
                sb.Append(string.Format(fmt, vars));
                sb.Append(" Exception: ");
                sb.Append(exception.ToString());
                return sb.ToString();
            }
        }
    }

    이 구현 클래스는 System.Diagnostics 네임스페이스를 이용해서 추적을 하고 있습니다. 이 네임스페이스는 .NET에서 제공되는 내장 기능으로 손쉽게 추적 정보를 생성하거나 사용할 수 있게 도와줍니다. 로그를 파일에 기록한다거나 Azure의 Blob 저장소에 기록하는 등, System.Diagnostics 네임스페이스와 함께 사용할 수 있는 다양한 "리스너(Listeners)"들이 존재하므로 참고하시기 바랍니다. Troubleshooting Azure Web Sites in Visual Studio 문서에서는 몇 가지 옵션들에 대한 정보와 더 많은 정보를 제공하는 다른 리소스들에 대한 링크가 제공됩니다. 본문의 예제에서는 Visual Studio의 출력(Output) 창을 통해서만 로그를 확인할 수 있습니다.

    나중에 실제 제품 응용 프로그램에서는 System.Diagnostics가 아닌 다른 추적 패키지의 사용이 고려될 수도 있습니다. 그리고 그럴 경우, ILogger 인터페이스를 도입했으므로 비교적 수월하게 다른 추적 메커니즘으로 전환이 가능합니다.

인터셉터 클래스 생성하기

이번에는 Entity Framework가 질의를 데이터베이스에 전송할 때마다 매번 호출하게 될 두 개의 클래스를 생성해보겠습니다. 한 클래스는 일시적인 오류를 재현하기 위한 클래스이고, 다른 클래스는 로깅 작업을 수행하기 위한 클래스입니다. 이런 인터셉터(Interceptor) 클래스들은 반드시 DbCommandInterceptor 클래스를 상속 받아야만 합니다. 그리고 해당 클래스들에 질의가 실행될 때 자동으로 호출될 재정의된 메서드들을 작성하면 됩니다. 이 메서드들에서 데이터베이스로 전송되는 질의를 검토하거나 로그를 기록하고, 데이터베이스로 질의가 전송되기 전에 변경한다거나 심지어 질의를 데이터베이스로 전달하지 않고도 직접 Entity Framework로 무언가를 반환할 수도 있습니다.

  1. 먼저 DAL 폴더에 SchoolInterceptorLogging.cs 라는 이름의 클래스 파일을 생성하고, 자동으로 생성된 템플릿 코드를 다음 코드로 대체하여 데이터베이스에 전송되는 모든 질의를 로그로 기록하는 인터셉터 클래스를 생성합니다:

    using System;
    using System.Data.Common;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure.Interception;
    using System.Data.Entity.SqlServer;
    using System.Data.SqlClient;
    using System.Diagnostics;
    using System.Reflection;
    using System.Linq;
    using ContosoUniversity.Logging;
    
    namespace ContosoUniversity.DAL
    {
        public class SchoolInterceptorLogging : DbCommandInterceptor
        {
            private ILogger _logger = new Logger();
            private readonly Stopwatch _stopwatch = new Stopwatch();
    
            public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
            {
                base.ScalarExecuting(command, interceptionContext);
                _stopwatch.Restart();
            }
    
            public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
            {
                _stopwatch.Stop();
                if (interceptionContext.Exception != null)
                {
                    _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
                }
                else
                {
                    _logger.TraceApi("SQL Database", "SchoolInterceptor.ScalarExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
                }
                base.ScalarExecuted(command, interceptionContext);
            }
    
            public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
            {
                base.NonQueryExecuting(command, interceptionContext);
                _stopwatch.Restart();
            }
    
            public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
            {
                _stopwatch.Stop();
                if (interceptionContext.Exception != null)
                {
                    _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
                }
                else
                {
                    _logger.TraceApi("SQL Database", "SchoolInterceptor.NonQueryExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
                }
                base.NonQueryExecuted(command, interceptionContext);
            }
    
            public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
            {
                base.ReaderExecuting(command, interceptionContext);
                _stopwatch.Restart();
            }
    
            public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
            {
                _stopwatch.Stop();
                if (interceptionContext.Exception != null)
                {
                    _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
                }
                else
                {
                    _logger.TraceApi("SQL Database", "SchoolInterceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
                }
                base.ReaderExecuted(command, interceptionContext);
            }
        }
    }

    이 코드는 질의 및 명령이 정상적으로 수행된 경우, 대기 시간 정보를 담고 있는 정보 로그를 기록합니다. 반면 예외가 발생한 경우에는 Error 로그를 기록합니다.

  2. 이번에는 DAL 폴더에 SchoolInterceptorTransientErrors.cs 라는 이름의 클래스 파일을 생성하고 자동으로 생성된 템플릿 코드를 다음 코드로 대체합니다. 이 클래스는 Search 텍스트 상자에 "Throw"라고 입력할 경우, 인위적으로 일시적인 오류를 만들어내는 인터셉터 클래스입니다:

    using System;
    using System.Data.Common;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure.Interception;
    using System.Data.Entity.SqlServer;
    using System.Data.SqlClient;
    using System.Diagnostics;
    using System.Reflection;
    using System.Linq;
    using ContosoUniversity.Logging;
    
    namespace ContosoUniversity.DAL
    {
        public class SchoolInterceptorTransientErrors : DbCommandInterceptor
        {
            private int _counter = 0;
            private ILogger _logger = new Logger();
    
            public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
            {
                bool throwTransientErrors = false;
                if (command.Parameters.Count > 0 && command.Parameters[0].Value.ToString() == "%Throw%")
                {
                    throwTransientErrors = true;
                    command.Parameters[0].Value = "%an%";
                    command.Parameters[1].Value = "%an%";
                }
    
                if (throwTransientErrors && _counter < 4)
                {
                    _logger.Information("Returning transient error for command: {0}", command.CommandText);
                    _counter++;
                    interceptionContext.Exception = CreateDummySqlException();
                }
            }
    
            private SqlException CreateDummySqlException()
            {
                // The instance of SQL Server you attempted to connect to does not support encryption
                var sqlErrorNumber = 20;
    
                var sqlErrorCtor = typeof(SqlError).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 7).Single();
                var sqlError = sqlErrorCtor.Invoke(new object[] { sqlErrorNumber, (byte)0, (byte)0, "", "", "", 1 });
    
                var errorCollection = Activator.CreateInstance(typeof(SqlErrorCollection), true);
                var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.Instance | BindingFlags.NonPublic);
                addMethod.Invoke(errorCollection, new[] { sqlError });
    
                var sqlExceptionCtor = typeof(SqlException).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 4).Single();
                var sqlException = (SqlException)sqlExceptionCtor.Invoke(new object[] { "Dummy", errorCollection, null, Guid.NewGuid() });
    
                return sqlException;
            }
        }
    }

    이 클래스 코드는 다중 로우 데이터를 반환할 수 있는 질의들을 대상으로 호출되는 ReaderExecuting 메서드만 오버라이드 하고 있습니다. 만약 다른 유형의 질의들에 대해서도 연결 복원 기능을 확인하고 싶다면, 바로 위에서 로깅 인터셉터 클래스에 작성한 것처럼 NonQueryExecuting 메서드와 ScalarExecuting 메서드까지 오버라이드 하면 됩니다.

    이 코드는 웹 브라우저에서 Student 페이지를 실행하고 검색 문자열에 "Throw"를 입력하면, 대표적인 일시적인 오류 유형으로 알려진, 오류 번호 20번에 대한 더미 SQL 데이터베이스 예외를 생성합니다. 그 밖에도 일시적인 오류로 인식되는 다른 오류 번호들로는 64번, 233번, 10053번, 10054번, 10060번, 10928번, 10929번, 40197번, 40501번, 그리고 40613번이 있습니다. 다만, 이 번호들은 새로운 버전의 SQL 데이터베이스에서는 변경될 가능성도 있으므로 참고하시기 바랍니다.

    이 코드는 질의를 실행한 다음 그 결과를 다시 Entity Framework에 전달하는 대신 예외를 반환합니다. 그리고 이렇게 일시적인 예외를 네 차례 반환하고 난 다음, 다시 데이터베이스에 질의를 전달하는 정상적인 과정으로 복귀합니다.

    그 모든 과정이 로그로 기록되기 때문에, 질의가 최종적으로 성공하기 전까지 Entity Framework가 총 네 차례 질의를 실행하는 것을 직접 확인할 수 있으며, 응용 프로그램을 사용하는 사용자가 볼 때 평소와 다른 유일한 차이점은 질의 결과가 페이지에 렌더되는 시간이 다소 오래 걸린다는 점 뿐입니다.

    Entity Framework가 재시도 하는 횟수 역시 조정 가능합니다. 가령 본문의 예제 코드에는 그 횟수가 네 번으로 지정되어 있는데, 그 이유는 SQL Database 실행 정책의 기본값이 4회이기 때문입니다. 만약 실행 정책을 변경한다면 일시적인 오류가 생성되는 횟수를 지정하는 코드도 함께 변경해야 합니다. 아니면 오히려 Entity Framework가 RetryLimitExceededException 예외를 던지는 경우를 테스트해보기 위해서, 의도적으로 더 많은 횟수의 오류를 생성하도록 코드를 변경할 수도 있습니다.

    검색 상자에 입력한 값은 command.Parameters[0]command.Parameters[1]에 담겨집니다 (각각 LastName 컬럼과 비교될 매개변수와 FirstMidName 컬럼과 비교될 매개변수입니다). 이 코드에서는 입력된 값이 "%Throw%"인 경우, 검색 문자열과 일부 학생들의 정보가 일치하여 결과가 반환되도록 이 매개변수들에 담겨 있는 "Throw"라는 문자열을 "an"으로 교체합니다.

    이 예제 코드는 응용 프로그램의 UI에 입력된 특정 값을 변경함으로서 간단하게 연결 복원 기능을 테스트할 수 있는 방법을 보여주고 있습니다. DbInterception.Add 메서드에 관해서 간단하게 살펴보고 나서 다시 설명하겠지만, 모든 질의 및 갱신문을 대상으로 일시적인 오류를 생성하도록 코드를 작성할 수도 있습니다.

  3. Global.asax 파일에 다음의 using 구문을 추가합니다:

    using ContosoUniversity.DAL;
    using System.Data.Entity.Infrastructure.Interception;
  4. 그리고 Application_Start 메서드에 다음에 강조된 라인들을 추가합니다:

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        DbInterception.Add(new SchoolInterceptorTransientErrors());
        DbInterception.Add(new SchoolInterceptorLogging());
    }

    이 코드 라인들은 Entity Framework가 데이터베이스에 질의들을 전송할 때 인터셉터 코드가 실행되도록 구성해줍니다. 일시적인 오류를 재현하는 인터셉터 클래스와 로그를 기록하는 인터셉터 클래스를 별개로 생성했기 때문에, 각각 독립적으로 활성화시키거나 비활성화시킬 수 있다는 점에 주목하시기 바랍니다.

    또한 코드 어디에서나 DbInterception.Add 메서드를 호출하여 인터셉터를 추가할 수 있습니다. 다시 말해서 본문의 예제에서처럼 이 메서드를 Application_Start 메서드 내에서만 호출할 수 있는 것은 아닙니다. 이를테면 이전 절에서 실행 정책을 구성하기 위해서 작성했던 DbConfiguration 클래스로 이 코드를 옮길 수도 있습니다.

    public class SchoolConfiguration : DbConfiguration
    {
        public SchoolConfiguration()
        {
            SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            DbInterception.Add(new SchoolInterceptorTransientErrors());
            DbInterception.Add(new SchoolInterceptorLogging());
        }
    }

    다만 어디에 이 코드를 작성하던지 주의해야 할 사항은 동일한 인터셉터를 대상으로 DbInterception.Add 메서드를 여러 번 호출하면 안된다는 점입니다. 인터셉터 클래스의 불필요한 인스턴스가 만들어지게 되기 때문입니다. 예를 들어서, 로그 기록 인터셉터를 두번 추가한다면 모든 SQL 질의가 로그에 두 번씩 기록될 것입니다.

    인터셉터는 등록된 순서대로, 즉 DbInterception.Add 메서드가 호출된 순서대로 실행됩니다. 따라서 인터셉터가 처리하는 작업이 무언지에 따라 이 순서가 문제가 될 수도 있습니다. 예를 들어, 특정 인터셉터가 CommandText 속성에서 가져온 SQL 명령을 변경할 수도 있다고 가정해보겠습니다. 만약 실제로 이 인터셉터가 SQL 명령을 변경한다면, 다음 인터셉터는 본래의 SQL 명령 대신 변경된 SQL 명령을 얻게 됩니다.

    지금까지 UI에 특정 값을 입력했을 때 일시적인 오류를 발생시키는 방법을 사용해서 일시적인 오류를 재현하는 코드를 구현해봤습니다. 그러나 이런 식으로 특정 매개변수 값을 검사하는 대신, 언제나 일련의 일시적인 오류들을 생성하도록 인터셉터 코드를 작성할 수도 있습니다. 그런 다음, 일시적인 오류를 생성하고 싶을 때만 인터셉터를 추가하면 됩니다. 다만 이 방법을 사용할 경우에는 데이터베이스의 초기화가 완료되기 전까지는 인터셉터를 추가하면 안됩니다. 다시 말해서, 일시적인 오류를 생성하기 전에 적어도 한 번은 엔터티 집합 중 하나를 대상으로 임의의 데이터베이스 작업을 수행해야 합니다. Entity Framework는 데이터베이스 초기화 과정 중 몇 가지 질의를 수행하는데, 이 질의들은 트랜젝션으로 묶여 있지 않습니다. 따라서 초기화 도중 오류가 발생하면 컨텍스트의 상태에 일관성이 없어질 수도 있습니다.

로깅 및 연결 복원 테스트하기

  1. F5 키를 눌러서 디버그 모드에서 응용 프로그램을 실행하고 Students 메뉴를 클릭합니다.

  2. 출력된 추적 정보는 Visual Studio의 출력(Output) 창에서 확인할 수 있습니다. 경우에 따라서는 인터셉터 로거에 의해 작성된 로그들을 살펴보기 위해서 스크롤 바를 위로 이동하여 JavaScript 오류에 관한 일부 출력들을 지나쳐야 할 수도 있습니다.

    그러면 데이터베이스로 전송되는 실제 SQL 질의들을 살펴볼 수 있습니다. 먼저 Entity Framework가 구동하면서 수행하는 질의들과, 데이터베이스의 버전 및 마이그레이션 이력 테이블을 확인하는 일부 초기 질의들과 명령들이 나타납니다 (마이그레이션에 관해서는 다음 자습서에서 살펴봅니다). 그리고 페이징을 위한 질의와, 얼마나 많은 학생들이 존재하는지 확인하기 위한 질의, 그리고 마지막으로 학생 데이터를 가져오기 위한 질의를 확인할 수 있습니다.

    역주: 사용 중인 Entity Framework의 버전에 따라 로그에 기록되는 질의들에 약간씩 차이가 존재할 수도 있습니다.

  3. 계속해서 Students 페이지에 "Throw"라는 검색 문자열을 입력하고 Search 버튼을 클릭해봅니다.

    그러면 이번에는 Entity Framework가 몇 차례 질의를 재시도 하는 몇 초 동안 브라우저의 응답이 느려진 것처럼 보이는 것을 확인할 수 있습니다. 이 때, 첫 번째 재시도는 매우 빠르게 시도되지만, 다시 재시도 할 때마다 대기 시간이 증가하게 됩니다. 이렇게 매번 재시도 될 때마다 대기 시간이 증가하도록 처리하는 처리 방식을 지수 백오프(Exponential Backoff) 라고 합니다.

    페이지가 출력되면 이름에 "an"이라는 문자열이 포함된 학생들의 목록이 나타납니다. 그리고 출력 창을 살펴보면 동일한 질의가 다섯 번 시도되었으며, 그 중 처음 네 번은 일시적인 예외가 반환된 것을 확인할 수 있습니다. 이 로그에서 확인할 수 있는 각각의 일시적인 오류들("Returning transient error for command...")은 SchoolInterceptorTransientErrors 클래스에서 일시적인 오류를 발생시키면서 직접 기록한 로그인 반면, 나머지 로그는 예외가 발생할 때마다 SchoolInterceptorLogging 클래스가 자체적으로 기록한 로그입니다.

    그리고 검색 문자열을 입력했기 때문에 학생들의 데이터를 반환하는 질의에 매개변수가 추가되어 있습니다:

    SELECT TOP (3) 
        [Project1].[ID] AS [ID], 
        [Project1].[LastName] AS [LastName], 
        [Project1].[FirstMidName] AS [FirstMidName], 
        [Project1].[EnrollmentDate] AS [EnrollmentDate]
        FROM ( SELECT [Project1].[ID] AS [ID], [Project1].[LastName] AS [LastName], [Project1].[FirstMidName] AS [FirstMidName], [Project1].[EnrollmentDate] AS [EnrollmentDate], row_number() OVER (ORDER BY [Project1].[LastName] ASC) AS [row_number]
            FROM ( SELECT 
                [Extent1].[ID] AS [ID], 
                [Extent1].[LastName] AS [LastName], 
                [Extent1].[FirstMidName] AS [FirstMidName], 
                [Extent1].[EnrollmentDate] AS [EnrollmentDate]
                FROM [dbo].[Student] AS [Extent1]
                WHERE ([Extent1].[LastName] LIKE @p__linq__0 ESCAPE N'~') OR ([Extent1].[FirstMidName] LIKE @p__linq__1 ESCAPE N'~')
            )  AS [Project1]
        )  AS [Project1]
        WHERE [Project1].[row_number] > 0
        ORDER BY [Project1].[LastName] ASC:

    본문의 예제 코드에서는 이 매개변수들의 값을 로그에 기록하지 않지만, 필요하다면 물론 기록도 가능합니다. 만약 매개변수의 값들을 확인하고 싶다면, 인터셉터 메서드에서 접근할 수 있는 DbCommand 개체의 Parameters 속성으로부터 매개변수 값들을 가져와서 로그에 기록하도록 코드를 작성하면 됩니다.

    주의해야 할 부분은 이 테스트는 응용 프로그램을 중지하고 다시 시작하지 않으면 다시 반복해서 재현할 수 없다는 점입니다. 응용 프로그램을 한 번만 실행하고 연결 복원 기능을 여러 차례 테스트 하려면, SchoolInterceptorTransientErrors 클래스에 저장되는 오류 횟수를 초기화시킬 수 있도록 코드를 변경하면 됩니다.

  4. 이 코드에서처럼 실행 전략(재시도 정책)을 설정함으로 인해서 비롯되는 차이점을 확인하고 싶다면, SchoolConfiguration.cs 클래스에서 SetExecutionStrategy 라인을 주석처리 한 다음, 다시 디버그 모드로 Students 페이지를 실행하고, "Throw"를 검색해보면 됩니다.

    그러면 이번에는 질의를 처음 실행하려고 시도할 때, 첫 번째 예외가 생성되는 그 즉시 디버거가 중지할 것입니다.

  5. 다시 SchoolConfiguration.cs 클래스에서 SetExecutionStrategy 라인의 주석을 제거합니다.

요약

본문에서는 연결 복원 기능을 활성화시키고, Entity Framework가 생성해서 데이터베이스로 전송하는 SQL 명령을 로그로 기록하는 방법들을 살펴봤습니다. 다음 자습서에서는 응용 프로그램을 인터넷에 배포하고, Code First 마이그레이션을 이용해서 데이터베이스를 배포해보겠습니다.

자습서의 내용 중 만족스러웠던 점이나 개선사항에 대한 의견이 있으시면 피드백으로 남겨주시기 바랍니다. 또한 Show Me How With Code를 통해서 새로운 주제를 요청하실 수도 있습니다.

다른 Entity Framework 리소스들에 대한 링크들은 ASP.NET Data Access - Recommended Resources 기사를 참고하시기 바랍니다.

이 기사는 2014년 2월 14일에 최초 작성되었습니다.