파트 5: Entity Framework를 이용한 Code First 마이그레이션과 배포

등록일시: 2016-04-11 08:00,  수정일시: 2016-09-02 09:13
조회수: 12,638
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
이번 파트에서는 Azure 클라우드에 예제 응용 프로그램을 배포해보고 Code First 마이그레이션을 이용해서 데이터베이스의 스키마를 갱신하는 방법을 살펴봅니다.

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

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

지금까지 본 자습서 시리즈의 예제 응용 프로그램은 여러분의 개발용 컴퓨터에 설치된 IIS Express에서 로컬로 실행되었습니다. 이 예제 응용 프로그램을 다른 사람들이 인터넷을 통해서 사용할 수 있는 현실적인 응용 프로그램으로 만들려면, 웹 호스팅 공급자에 응용 프로그램을 배포해야 합니다. 이번 파트에서는 Contoso University 응용 프로그램을 Azure 클라우드에 배포해봅니다.

본문은 다음과 같은 내용의 절들을 담고 있습니다:

  • Code First 마이그레이션 활성화시키기. 데이터 모델을 변경할 경우, 마이그레이션 기능을 이용하면 데이터베이스를 드랍시킨 다음 다시 생성하지 않고도 데이터베이스의 스키마를 갱신해서 운영 환경에 변경사항들을 배포할 수 있습니다.
  • Azure에 배포하기. 이 절은 선택 사항으로, 프로젝트를 배포하지 않아도 나머지 자습서 과정들을 계속 진행할 수 있습니다.

기본적으로 권장하는 배포 방식은 소스 제어와 함께 지속적인 통합 프로세스를 도입하는 것이지만, 본 자습서 시리즈는 이 부분에 대해서는 다루지 않습니다. 이 주제에 대한 보다 자세한 정보는 Building Real-World Cloud Apps with Azure 자습서 시리즈의 Source Control 장과 Continuous Integration and Continuous Delivery 장을 참고하시기 바랍니다.

Code First 마이그레이션 활성화시키기

응용 프로그램을 새로 개발하는 도중에는 데이터 모델이 자주 변경되는데, 매번 모델이 변경될 때마다 데이터베이스와 동기화가 어긋나게 됩니다. 본 자습서의 예제 응용 프로그램은 현재 매번 데이터 모델이 변경될 때마다 Entity Framework가 자동으로 데이터베이스를 드랍시켰다가 다시 생성하도록 구성되어 있습니다. 엔터티 클래스들을 추가, 제거, 또는 변경하거나 DbContext 클래스를 변경한 다음 응용 프로그램을 실행하면, 자동으로 기존 데이터베이스가 삭제되고 모델과 일치하는 새로운 데이터베이스가 만들어지고, 테스트 데이터가 입력됩니다.

운영 환경에 응용 프로그램을 배포하기 전까지는 이런 식으로 데이터베이스와 데이터 모델 간의 동기화를 유지하는 방법도 잘 동작합니다. 그러나 일단 응용 프로그램이 운영 환경에서 사용되기 시작하면 동시에 유지해야 할 데이터도 저장되기 시작하므로, 새로운 컬럼을 추가하는 등 변경사항이 발생할 때마다 매번 모든 데이터를 잃어버리고 싶은 사람은 없을 것입니다. 이 경우 Code First 마이그레이션을 이용하면 데이터베이스를 드랍하고 다시 생성하는 대신 기존 데이터베이스의 스키마를 갱신하도록 Code First를 활성화시켜셔 문제점을 해결할 수 있습니다. 본문에서는 예제 응용 프로그램을 배포해보게 될텐데, 먼저 그 준비의 일환으로 Code First 마이그레이션을 활성화시켜 보겠습니다.

  1. 본 자습서 시리즈의 초반부에서 응용 프로그램의 Web.config 파일에 contexts 요소를 추가해서 설정했던 이니셜라이저를 주석으로 처리하거나 삭제해서 비활성화시킵니다.

    <entityFramework>
      <!--<contexts>
        <context type="ContosoUniversity.DAL.SchoolContext, ContosoUniversity">
          <databaseInitializer type="ContosoUniversity.DAL.SchoolInitializer, ContosoUniversity" />
        </context>
      </contexts>-->
      <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
        <parameters>
          <parameter value="v11.0" />
        </parameters>
      </defaultConnectionFactory>
      <providers>
        <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
      </providers>
    </entityFramework>
  2. 그리고 응용 프로그램의 Web.config 파일에서 연결 문자열의 데이터베이스 이름을 ContosoUniversity2로 변경합니다.

    <connectionStrings>
      <add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=ContosoUniversity2;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
    </connectionStrings>

    이렇게 설정을 변경하면 첫 번째 마이그레이션 시에 새로운 데이터베이스가 생성되게 됩니다. 이 작업은 반드시 필요한 과정은 아니지만 잠시 뒤에 왜 이렇게 설정을 했는지 그 이유를 알게될 것입니다.

  3. 계속해서 도구(Tools) 메뉴에서 NuGet 패키지 관리자(Library Package Manager)를 선택하고 다시 패키지 관리자 콘솔(Package Manager Console)을 선택합니다.

  4. 그리고 PM> 프롬프트에 다음 명령을 순서대로 입력합니다:

    enable-migrations
    add-migration InitialCreate

    먼저 enable-migrations 명령을 실행하면 ContosoUniversity 프로젝트에 Migrations 라는 폴더가 만들어지고, 이 폴더에 마이그레이션의 구성을 편집할 수 있는 Configuration.cs 클래스 파일이 생성됩니다.

    (만약 앞에서 연결 문자열의 데이터베이스 이름을 변경하는 단계를 생략했다면, 마이그레이션이 이미 생성되어 있는 기존 데이터베이스를 감지하고 자동으로 add-migration 명령을 수행하게 됩니다. 그래도 별다른 문제는 없습니다. 다만, 잠시 후 데이터베이스를 배포해보기 전에 마이그레이션 코드를 테스트해보는 과정이 생략될 뿐입니다. 그리고 나중에 update-database 명령을 실행하더라도 데이터베이스가 이미 존재하기 때문에 아무 일도 일어나지 않을 것입니다.)

    Configuration 클래스에는 본 자습서 시리즈의 전반부에서 살펴봤던 이니셜라이저 클래스 같이 Seed라는 메서드가 존재합니다.

    internal sealed class Configuration : DbMigrationsConfiguration<ContosoUniversity.DAL.SchoolContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }
    
        protected override void Seed(ContosoUniversity.DAL.SchoolContext context)
        {
            //  This method will be called after migrating to the latest version.
    
            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
        }
    }

    Seed 메서드의 용도는 Code First가 데이터베이스를 생성하거나 갱신한 뒤에 데스트 데이터를 입력하거나 갱신하기 위한 것입니다. 이 메서드는 데이터베이스가 생성되거나 데이터 모델이 수정되어 데이터베이스의 스키마가 변경될 때마다 매번 호출됩니다.

Seed 메서드 설정하기

데이터 모델이 변경되어 데이터베이스가 드랍됐다가 다시 생성되면, 매번 이니셜라이저 클래스의 Seed 메서드가 호출되어 다시 테스트 데이터가 입력됩니다. 모델이 변경될 때마다 데이터베이스가 드랍되므로 모든 테스트 데이터가 사라지기 때문입니다. 그러나 Code First 마이그레이션을 사용하는 경우에는 데이터베이스가 변경된 뒤에도 테스트 데이터가 그대로 남아있기 때문에, 특별한 이유가 없다면 테스트 데이터를 Seed 메서드에 포함시킬 필요가 없습니다. 사실 마이그레이션을 이용해서 운영 환경에 데이터베이스를 배포한다는 얘기 자체가 Seed 메서드가 실행되는 위치도 운영 환경이라는 뜻이므로, 많은 경우 Seed 메서드를 이용해서 테스트 데이터를 입력하는 일 자체가 불필요합니다. 대신 실제로 운영 환경에 필요한 데이터만 데이터베이스에 입력하는 Seed 메서드는 필요할 수도 있습니다. 가령 운영 환경에서 응용 프로그램을 정상적으로 사용하려면 Department 테이블에 실제 학과들의 이름이 입력되어 있는 데이터베이스가 필요한 경우도 있을 수 있으니 말입니다.

본문에서는 마이그레이션을 이용해서 배포를 수행하고는 있지만, 다량의 데이터를 수작업으로 일일이 입력하지 않고도 응용 프로그램의 기능들이 동작하는 모습을 손쉽게 살펴볼 수 있도록, Seed 메서드가 테스트 데이터를 입력하게 만들어봅니다.

  1. 새로운 데이터베이스에 테스트 데이터를 적재하는 다음의 코드로 Configuration.cs 파일의 내용을 교체합니다.

    namespace ContosoUniversity.Migrations
    {
        using ContosoUniversity.Models;
        using System;
        using System.Collections.Generic;
        using System.Data.Entity;
        using System.Data.Entity.Migrations;
        using System.Linq;
    
        internal sealed class Configuration : DbMigrationsConfiguration<ContosoUniversity.DAL.SchoolContext>
        {
            public Configuration()
            {
                AutomaticMigrationsEnabled = false;
            }
    
            protected override void Seed(ContosoUniversity.DAL.SchoolContext context)
            {
                var students = new List<Student>
                {
                    new Student { FirstMidName = "Carson",   LastName = "Alexander",
                        EnrollmentDate = DateTime.Parse("2010-09-01") },
                    new Student { FirstMidName = "Meredith", LastName = "Alonso",
                        EnrollmentDate = DateTime.Parse("2012-09-01") },
                    new Student { FirstMidName = "Arturo",   LastName = "Anand",
                        EnrollmentDate = DateTime.Parse("2013-09-01") },
                    new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                        EnrollmentDate = DateTime.Parse("2012-09-01") },
                    new Student { FirstMidName = "Yan",      LastName = "Li",
                        EnrollmentDate = DateTime.Parse("2012-09-01") },
                    new Student { FirstMidName = "Peggy",    LastName = "Justice",
                        EnrollmentDate = DateTime.Parse("2011-09-01") },
                    new Student { FirstMidName = "Laura",    LastName = "Norman",
                        EnrollmentDate = DateTime.Parse("2013-09-01") },
                    new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                        EnrollmentDate = DateTime.Parse("2005-08-11") }
                };
                students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
                context.SaveChanges();
    
                var courses = new List<Course>
                {
                    new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3, },
                    new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3, },
                    new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3, },
                    new Course {CourseID = 1045, Title = "Calculus",       Credits = 4, },
                    new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4, },
                    new Course {CourseID = 2021, Title = "Composition",    Credits = 3, },
                    new Course {CourseID = 2042, Title = "Literature",     Credits = 4, }
                };
                courses.ForEach(s => context.Courses.AddOrUpdate(p => p.Title, s));
                context.SaveChanges();
    
                var enrollments = new List<Enrollment>
                {
                    new Enrollment { 
                        StudentID = students.Single(s => s.LastName == "Alexander").ID,
                        CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                        Grade = Grade.A
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alexander").ID,
                        CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                        Grade = Grade.C
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alexander").ID,
                        CourseID = courses.Single(c => c.Title == "Macroeconomics").CourseID,
                        Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                        CourseID = courses.Single(c => c.Title == "Calculus").CourseID,
                        Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                        CourseID = courses.Single(c => c.Title == "Trigonometry").CourseID,
                        Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                        CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                        Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Anand").ID,
                        CourseID = courses.Single(c => c.Title == "Chemistry").CourseID
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Anand").ID,
                        CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                        Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                        CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                        Grade = Grade.B       
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Li").ID,
                        CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                        Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Justice").ID,
                        CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                        Grade = Grade.B
                    }
                };
    
                foreach (Enrollment e in enrollments)
                {
                    var enrollmentInDataBase = context.Enrollments.Where(
                        s => s.Student.ID == e.StudentID &&
                             s.Course.CourseID == e.CourseID).SingleOrDefault();
                    if (enrollmentInDataBase == null)
                    {
                        context.Enrollments.Add(e);
                    }
                }
                context.SaveChanges();
            }
        }
    }

    Seed 메서드는 데이터베이스 컨텍스트 개체를 입력 매개변수로 받은 다음, 이 개체를 이용해서 데이터베이스에 새로운 엔터티들을 추가합니다. 먼저 각각의 엔터티 형식에 대한 새로운 엔터티들의 컬렉션을 생성한 다음, 이를 적절한 DbSet 속성에 추가하고 변경사항들을 데이터베이스에 저장합니다. 반드시 이 코드처럼 각 엔터티들에 대한 코드 블럭들 뒤에서 매번 SaveChanges 메서드를 호출해야만 하는 것은 아닙니다. 그러나 이런 코드 패턴을 사용하면 데이터베이스에 입력하는 도중 예외가 발생할 경우, 문제가 발생한 소스의 위치를 파악하기가 조금 더 수월합니다.

    이 코드에서 AddOrUpdate 메서드를 이용하여 데이터를 입력하는 일부 구문들은 일명 "upsert" 작업을 수행하고 있습니다. 왜냐하면 update-database 명령을 실행할 때마다, 보통 각각의 마이그레이션 이후에, Seed 메서드가 실행되므로 추가하려는 로우가 이미 데이터베이스가 생성되는 첫 번째 마이그레이션 시에 입력되어 아직까지 존재하고 있을 수도 있으므로, 무조건 데이터를 입력하기만 해서는 안되기 때문입니다. "upsert" 작업은 이미 존재하는 로우를 입력하려고 시도할 때 발생할 수 있는 오류는 방지해주지만 응용 프로그램을 테스트하는 동안 변경됐던 모든 데이터들은 그대로 덮어 쓰게 됩니다. 그러나 일부 테이블들의 테스트 데이터들은 데이터베이스가 업데이트된 후에도 테스트를 수행하는 동안 변경된 데이터가 계속 유지되기를 원하는 경우도 있습니다. 만약 그렇다면 조건에 따라, 즉 기존 데이터가 존재하지 않는 경우에만, 입력 작업이 수행돼야 합니다. 참고로 이번 예제의 Seed 메서드에서는 두 가지 접근방식을 모두 사용하고 있습니다.

    AddOrUpdate 메서드에 전달되는 첫 번째 매개변수에는 로우가 이미 존재하는지 여부를 확인하기 위해 사용될 속성을 지정합니다. 가령 예제 코드에서 제공되는 테스트용 학생 데이터는 목록에 중복되는 성(Last name)이 존재하지 않기 때문에 LastName 속성을 해당 용도로 사용할 수 있습니다:

    context.Students.AddOrUpdate(p => p.LastName, s)

    다시 말해서 이 코드는 성이 유일하다고 전제하고 있는 셈입니다. 따라서 만약 중복된 성을 가진 학생 정보를 직접 수작업으로 입력했다면, 다음 마이그레이션 수행 시 다음과 같은 예외가 발생하게 됩니다.

    Sequence contains more than one element

    동일한 "Alexander Carson"이라는 이름을 가진 두 명의 학생이 존재하는 경우 같이, 불필요한 데이터를 처리하는 방법에 대한 보다 자세한 정보는 Rick Anderson의 Seeding and Debugging Entity Framework (EF) DBs 블로그 포스트를 참고하시기 바랍니다. 그리고 AddOrUpdate 메서드에 대한 더 자세한 정보는 Julie Lerman의 Take care with EF 4.3 AddOrUpdate Method 블로그 포스트를 참고하시기 바랍니다.

    또한 students 컬렉션을 생성하는 코드에서 ID 속성을 전혀 설정하지 않고 있음에도 불구하고, Enrollment 엔터티들을 생성하는 코드에서는 students 컬렉션의 엔터티들이 ID 값을 갖고 있다고 가정하고 있습니다.

    new Enrollment {
        StudentID = students.Single(s => s.LastName == "Alexander").ID,
        CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
        Grade = Grade.A
    },

    이 코드에서 ID 속성을 사용할 수 있는 이유는 students 컬렉션에 대해 SaveChanges 메서드를 호출하는 시점에 ID 값이 설정되기 때문입니다. Entity Framework는 데이터베이스에 엔터티를 입력할 때 자동으로 기본 키 값을 구한 다음, 메모리에 위치한 해당 엔터티의 ID 속성을 갱신합니다.

    그리고 Enrollments 엔터티 집합에 Enrollment 엔터티들을 추가하는 코드에서는 AddOrUpdate 메서드를 사용하지 않습니다. 그대신 먼저 해당 엔터티가 이미 존재하는지 여부부터 확인한 다음, 엔터티가 존재하지 않는 경우에만 이를 입력합니다. 이 접근방식을 사용하면 응용 프로그램의 UI를 이용해서 변경한 수강 학점의 내역들까지 그대로 유지됩니다. 이 코드는 Enrollment List의 각 요소들을 대상으로 루프를 돌면서 해당 수강 정보가 데이터베이스에 존재하지 않는 경우에만 데이터베이스에 입력합니다. 결과적으로 데이터베이스를 최초로 갱신하는 시점에는 데이터베이스가 비어 있으므로 모든 수강 정보들이 입력될 것입니다.

    foreach (Enrollment e in enrollments)
    {
        var enrollmentInDataBase = context.Enrollments.Where(
            s => s.Student.ID == e.Student.ID &&
                 s.Course.CourseID == e.Course.CourseID).SingleOrDefault();
        if (enrollmentInDataBase == null)
        {
            context.Enrollments.Add(e);
        }
    }
  2. 프로젝트를 빌드합니다.

최초 마이그레이션 실행하기

두 번째 명령인 add-migration 명령이 실행되면 Code First 마이그레이션이 데이터베이스를 빈 상태에서 처음부터 생성하는 클래스 코드를 만들어냅니다. 이 클래스 역시 Migrations 폴더에 위치하게 되며 <timestamp>_InitialCreate.cs 같은 형태의 파일명을 갖고 있습니다. 이 InitialCreate 클래스의 Up 메서드는 데이터 모델 엔터티 집합과 일치하는 데이터베이스 테이블들을 생성하고, Down 메서드는 해당 테이블들을 삭제합니다.

public partial class InitialCreate : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Course",
            c => new
                {
                    CourseID = c.Int(nullable: false),
                    Title = c.String(),
                    Credits = c.Int(nullable: false),
                })
            .PrimaryKey(t => t.CourseID);
        
        CreateTable(
            "dbo.Enrollment",
            c => new
                {
                    EnrollmentID = c.Int(nullable: false, identity: true),
                    CourseID = c.Int(nullable: false),
                    StudentID = c.Int(nullable: false),
                    Grade = c.Int(),
                })
            .PrimaryKey(t => t.EnrollmentID)
            .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
            .ForeignKey("dbo.Student", t => t.StudentID, cascadeDelete: true)
            .Index(t => t.CourseID)
            .Index(t => t.StudentID);
        
        CreateTable(
            "dbo.Student",
            c => new
                {
                    ID = c.Int(nullable: false, identity: true),
                    LastName = c.String(),
                    FirstMidName = c.String(),
                    EnrollmentDate = c.DateTime(nullable: false),
                })
            .PrimaryKey(t => t.ID);
    }
    
    public override void Down()
    {
        DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
        DropForeignKey("dbo.Enrollment", "CourseID", "dbo.Course");
        DropIndex("dbo.Enrollment", new[] { "StudentID" });
        DropIndex("dbo.Enrollment", new[] { "CourseID" });
        DropTable("dbo.Student");
        DropTable("dbo.Enrollment");
        DropTable("dbo.Course");
    }
}

Code First 마이그레이션은 Up 메서드를 호출하여 해당 마이그레이션에 대한 데이터 모델의 변경사항들을 구현합니다. 반대로 변경사항들을 롤백하는 명령을 입력하면 Code First 마이그레이션이 Down 메서드를 호출합니다.

이 마이그레이션은 add-migration InitialCreate 명령을 입력해서 생성된 최초의 마이그레이션입니다. 여기서 명령의 매개변수(이 경우에는 InitialCreate)는 파일명의 일부로 사용되는 단어로 사용자가 원하는 대로 지정할 수 있으며, 대게 해당 마이그레이션이 수행하는 작업을 요악하는 단어나 문구를 선택하는 것이 일반적입니다. 가령, 이후의 다른 마이그레이션에서는 "AddDepartmentTable"라는 이름을 지정할 수도 있습니다.

데이터베이스가 이미 존재하는 상태에서 최초 마이그레이션을 생성했다면, 데이터베이스와 데이터 모델이 이미 일치하는 상태이므로 데이터베이스 생성 코드가 만들어는 지지만 실행되지는 않습니다. 만약 데이터베이스가 아직 존재하지 않는 다른 환경에 응용 프로그램을 배포한다면, 이 코드가 실행되어 데이터베이스가 생성될 것입니다. 그러므로 이 동작을 먼저 테스트 해보는 것이 좋을 것입니다. 바로 이런 이유 때문에 앞에서 연결 문자열의 데이터베이스 이름을 변경했던 것입니다. 그리고 그 결과로 마이그레이션이 새로운 데이터베이스를 빈 상태에서 처음부터 새로 생성하게 됩니다.

  1. 패키지 관리자 콘솔(Package Manager Console) 창에 다음 명령을 입력합니다:

    update-database

    update-database 명령은 Up 메서드를 실행해서 데이터베이스를 생성하고 Seed 메서드를 호출해서 데이터베이스를 채워 넣습니다. 그리고 이어지는 절에서 살펴보겠지만, 이와 동일한 과정이 응용 프로그램을 배포하고 난 뒤에 운영 환경에서 자동으로 실행됩니다.

  2. 본 자습서의 첫 번째 파트에서처럼 서버 탐색기(Server Explorer)를 이용해서 생성된 데이터베이스를 확인해보시기 바랍니다. 그리고 응용 프로그램을 실행해서 기존처럼 모든 기능이 정상적으로 동작하는지도 살펴보시기 바랍니다.

Azure에 배포하기

지금까지 예제 응용 프로그램은 개발용 컴퓨터에 설치된 IIS Express에서 로컬로 실행됐습니다. 이 응용 프로그램을 다른 사람들도 인터넷을 통해서 사용할 수 있게 만들려면, 웹 호스팅 공급자에 응용 프로그램을 배포해야 합니다. 이번 절에서는 응용 프로그램을 Azure에 배포해볼 것입니다. 이번 절은 선택사항이므로 그냥 건너뛰고 이어지는 자습서의 나머지 파트들을 진행하거나 이번 절의 지시사항들을 여러분이 선택한 다른 호스팅 공급자에 적합하게 조정해도 무방합니다.

Code First 마이그레이션을 이용해서 데이터베이스 배포하기

이번 절에서는 Code First 마이그레이션을 이용해서 데이터베이스를 배포해볼 것입니다. 이를 위해 Visual Studio에서 배포 설정을 구성하기 위해 필요한 게시 프로필을 생성하면서 Code First 마이그레이션 실행(응용 프로그램 시작 시 실행)(Execute Code First Migrations (runs on application start))이라는 라벨이 붙은 체크 박스를 선택해볼 것입니다. 이 설정 항목은 배포 프로세스로 하여금 대상 서버의 응용 프로그램 Web.config 파일을 자동으로 구성해서 Code First가 MigrateDatabaseToLatestVersion 이니셜라이저 클래스를 사용하도록 만들어줍니다.

Visual Studio는 배포 과정이 진행되는 동안 데이터베이스를 대상으로 어떤 작업도 수행하지 않으며, 단지 프로젝트를 대상 서버로 복사할 뿐입니다. 대신 응용 프로그램이 배포된 이후 배포된 응용 프로그램을 실행해서 데이터베이스에 처음으로 접근할 때, Code First가 데이터베이스와 데이터 모델이 일치하는지 여부를 검사합니다. 만약 일치하지 않는다면 Code First가 자동으로 데이터베이스를 생성하거나(데이터베이스가 아직 존재하지 않을 경우), 데이터베이스의 스키마를 마지막 버전으로 갱신합니다(데이터베이스가 존재하지만 모델과 일치하지 않을 경우). 그리고 응용 프로그램이 마이그레이션의 Seed 메서드를 구현하고 있다면, 데이터베이스가 생성되거나 스키마가 갱신된 이후 이 메서드가 실행됩니다.

예제 응용 프로그램의 마이그레이션 Seed 메서드는 현재 테스트 데이터를 입력하도록 구현되어 있습니다. 따라서 운영 환경에 배포할 경우에는 Seed 메서드를 수정해서 운영 데이터베이스에서 관리하고자 하는 데이터들만 입력되도록 만들어야 합니다. 가령 현재 데이터 모델을 기준으로 본다면 개발 데이터베이스에 강의 정보는 실제 정보를, 학생 정보는 가상의 정보를 입력하고 싶을 수도 있습니다. 이런 경우, 개발 당시에는 두 가지 정보를 모두 적재하도록 Seed 메서드를 작성했다가 운영 환경에 배포하기 전에 가상의 학생 정보들을 주석으로 처리할 수도 있습니다. 또는 강의 정보들만 적재하도록 Seed 메서드를 작성하고 가상의 학생 정보들은 응용 프로그램의 UI를 사용해서 테스트 데이터베이스에 수작업으로 입력할 수도 있습니다.

Azure 계정 만들기

이번 절을 직접 따라해보려면 Azure 계정이 필요합니다. 아직 Azure 계정은 없지만 MSDN을 구독하고 있다면, MSDN 구독 혜택에서 계정을 활성화시킬 수 있습니다. 또는 잠시만 시간을 들이면 1개월 무료 평가판 계정을 사용할 수 있습니다. 더 자세한 정보는 Azure 무료 평가판 페이지를 참고하시기 바랍니다.

Azure에 웹 사이트 및 SQL 데이터베이스 만들기

본문에서 Azure에 배포해볼 웹 응용 프로그램은 결과적으로 공유 호스팅 환경에서 실행되는데, 이 말은 응용 프로그램이 다른 Azure 사용자들과 공유되는 가상 머신(VMs, Virtual Machines)에서 실행될 것이라는 뜻입니다. 공유 호스팅 환경은 클라우드를 도입할 수 있는 가장 저렴한 방법입니다. 나중에 웹 트래픽이 증가하면 전용 가상 머신에서 응용 프로그램을 실행하여 필요에 부합하도록 확장할 수 있습니다.

데이터베이스는 Azure SQL 데이터베이스에 배포해볼 텐데, 이 SQL 데이터베이스는 SQL Server 기술을 기반으로 한 클라우드 기반의 관계형 데이터베이스 서비스입니다. SQL Server를 대상으로 동작하는 도구나 응용 프로그램들은 SQL 데이터베이스를 대상으로도 동작합니다.

  1. Azure 관리 포털(Azure Management Portal)의 좌측 탭에서 웹 앱(WEB APPS)을 클릭한 다음, 새로 만들기(New)를 클릭합니다.
    역주: 2016년 4월 현재, 웹 사이트(Web Sites)웹 앱(Web Apps)으로 명칭과 기능이 변경되었습니다.

  2. 그런 다음, 사용자 지정 만들기(CUSTOM CREATE)를 클릭합니다.

    그러면 새 웹 앱 - 사용자 지정 만들기(NEW WEB APP - CUSTOM CREATE) 마법사가 나타납니다.
  3. 마법사의 웹 앱 만들기(Create Web App) 단계에서 먼저 URL 텍스트 상자에 응용 프로그램에서 사용할 유일한 URL을 입력합니다. 전체 URL은 여기에 입력한 내용과 텍스트 상자 옆에 표시되는 접미사가 결합되어 구성됩니다. 다음 그림에는 "ConU"라고 입력되어 있지만, 이 URL은 이미 사용됐을 것이므로 여러분은 다른 URL을 입력해야 합니다.

  4. 지역(Region) 드롭다운 목록에서 가까운 지역을 선택합니다. 이 설정은 웹 앱이 실행될 데이터 센터를 선택하는 항목입니다.
  5. 데이터베이스(Database) 드롭다운 목록에서 무료 SQL 데이터베이스 만들기(Create a free SQL database)를 선택합니다.

  6. DB 연결 문자열 이름(DB CONNECTION STRING NAME) 항목에는 SchoolContext 라고 입력합니다.

  7. 마법사 우측 하단에 위치한 오른쪽 화살표를 클릭합니다. 그러면 마법사가 데이터베이스 설정 지정(Database Settings) 단계로 이동합니다.
  8. 이름(Name) 텍스트 상자에는 ContosoUniversityDB 라고 입력합니다.
  9. 서버(Server) 드롭다운 목록에서 새 SQL 데이터베이스 서버(New SQL Database server)를 선택합니다. 만약 이미 생성한 서버가 존재한다면 해당 서버를 선택해도 무방합니다.
  10. 서버 로그인 이름(LOGIN NAME)서버 로그인 암호(PASSWORD)를 입력합니다. 만약 앞에서 새 SQL 데이터베이스 서버(New SQL Database server)를 선택했다면 입력할 수 있는 기존 이름과 암호가 없으므로, 이후 데이터베이스에 접근할 때 사용하기 위해서 새로 정한 이름과 암호를 여기에 입력합니다. 반면 기존에 만들었던 서버를 선택했다면 해당 서버의 신원 정보를 입력하면 됩니다. 본문에서는 고급 데이터베이스 설정 구성(Advanced) 체크 박스는 선택하지 않습니다. 이 옵션은 데이터베이스의 테이터 정렬(Collation)을 설정할 수 있는 추가적인 단계를 제공합니다.
  11. 지역(Region) 항목은 웹 앱과 동일한 지역을 선택합니다.
  12. 마법사 우측 하단에 위치한 체크 표시를 클릭해서 작업을 마법사를 완료합니다.

    그러면 관리 포털 화면이 다시 웹 앱 페이지로 돌가가게 되고 상태(Status) 컬럼에 웹 앱을 만드는 중이라는 표시가 나타납니다. 그리고 잠시 후(대부분 몇 분 정도), 상태(Status) 컬럼이 정상적으로 생성되어 실행 중이라는 표시로 변경됩니다. 관리 포털 좌측의 네비게이션 바를 살펴보면, 웹 앱(Web Apps) 아이콘 옆에는 현재 로그인 한 계정에 생성된 웹 앱의 갯수가 나타나고, SQL 데이터베이스(SQL Databases) 아이콘 옆에는 데이터베이스의 갯수가 나타납니다.

Azure에 응용 프로그램 배포하기

  1. 다시 Visual Studio로 이동한 다음, 마우스 오른쪽 버튼으로 솔루션 탐색기(Solution Explorer)에서 프로젝트를 클릭하고 컨텍스트 메뉴에서 게시(Publish)를 선택합니다.

  2. 웹 게시(Publish Web) 마법사의 프로필(Profile) 탭에서 가져오기(Import) 버튼을 클릭합니다.

    역주: 지난 몇 년간 마이크로소프트의 다양한 제품과 서비스들 가운데, Azure와 관련된 제품들은 더욱 더 역동적으로 확장되고 발전되어 왔으며 지금도 계속해서 변해가고 있는 중입니다. 2016년 4월 현재는 본문의 원문이 작성된지 이미 만 2년 가량 지난 시점으로, 대부분의 캡처 이미지들이 현재의 모습과는 많이 다릅니다. 그 중에서도 이 부분의 몇 가지 단계들은 내용이 너무 상이하여 이해를 방해하는 수준으로 판단되어 과감히 번역에서 제외하기로 결정했습니다. 다행히도 Azure SDK가 설치된 최신의 Visual Studio 2015 환경에서 Azure 구독을 등록하고 게시 프로필을 다운로드 하는 방법은 매우 간단합니다.

    먼저 서버 탐색기(Server Explorer)에서 Azure 구독 계정으로 로그인을 한 다음, 방금 만든 웹 앱(Web Apps)을 찾아서 오른쪽 버튼으로 클릭하고 컨텍스트 메뉴에서 게시 프로필 다운로드(Download Publish Profile)를 선택하기만 하면 됩니다.


    그런 다음, 가져오기(Import) 버튼을 이용해서 다운로드 받은 프로필을 선택하면 바로 다음 단계가 진행됩니다. 그 밖에도 게시 프로필은 관리 포털이나 Cloud Explorer를 통해서도 다운로드 할 수 있으므로 참고하시기 바랍니다.
  3. 연결(Connection) 탭에서 연결 유효성 검사(Validate Connection) 버튼을 클릭해서 불러온 설정이 올바른지 확인합니다.

  4. 연결이 유효한 경우, 연결 유효성 검사(Validate Connection) 버튼 옆에 녹색 체크 표시가 나타납니다. 이제 다음(Next) 버튼을 클릭합니다.

  5. SchoolContext 영역의 원격 연결 문자열(Remote connection string) 드롭다운 목록에서 이전 단계에서 생성한 데이터베이스에 대한 연결 문자열을 선택합니다.
  6. 그리고 Code First 마이그레이션 실행(응용 프로그램 시작 시 실행)(Execute Code First Migrations (runs on application start)) 체크 박스를 선택합니다.

    이미 설명했던 바와 같이, 이 설정 항목은 배포 프로세스로 하여금 대상 서버의 응용 프로그램 Web.config 파일을 자동으로 구성하여 Code First가 MigrateDatabaseToLatestVersion 이니셜라이저 클래스를 사용하도록 만들어줍니다.
  7. 다시 다음(Next) 버튼을 클릭합니다.
  8. 미리 보기(Preview) 탭에서 미리 보기 시작(Start Preview) 버튼을 클릭합니다.

    그러면 서버로 복사될 파일들의 목록이 나타나게 됩니다. 응용 프로그램을 게시하기 위해서 반드시 미리 보기를 수행해야만 하는 것은 아니지만 파악하고 있으면 유용한 기능입니다. 지금은 이 파일 목록에 대해 어떠한 작업도 수행할 필요가 없습니다. 그러나 나중에 다시 응용 프로그램을 배포할 때는 목록에서 변경된 파일들만 선택할 수도 있습니다.

  9. 게시(Publish) 버튼을 누릅니다. 그러면 Visual Studio가 Azure 서버로 파일들을 복사하기 시작합니다.
  10. 이때 출력(Output) 창을 살펴보면 어떤 배포 작업이 진행 중인지, 배포가 성공적으로 완료됐는지 등의 정보를 확인할 수 있습니다.

  11. 정상적으로 배포가 완료되고 나면, 자동으로 기본 브라우저에서 배포된 웹 사이트의 URL이 열립니다. 이제 웹 응용 프로그램이 클라우드 상에서 실행되고 있는 것입니다. 계속해서 브라우저에서 Students 메뉴를 선택합니다.

    그러면 바로 이 시점에 Azure SQL 데이터베이스에 SchoolContext 데이터베이스가 생성되는데, 이는 앞에서 Code First 마이그레이션 실행(응용 프로그램 시작 시 실행)(Execute Code First Migrations (runs on application start)) 체크 박스를 선택했기 때문입니다. 다시 말해서 배포된 웹 사이트의 Web.config 파일은 응용 프로그램의 코드가 최초로 데이터베이스의 데이터를 읽거나 쓰는 시점에(즉 Students 메뉴를 선택한 시점에) MigrateDatabaseToLatestVersion 이니셜라이저가 실행되도록 변경된 상태입니다.

    뿐만 아니라, 배포 프로세스는 Code First 마이그레이션이 데이터베이스 스키마를 갱신하거나 데이터베이스에 기초 데이터를 입력할 때 사용하기 위한 새로운 연결 문자열(SchoolContext_DatabasePublish)을 함께 생성합니다.

    배포 버전의 Web.config 파일은 로컬 컴퓨터의 ContosoUniversity\obj\Release\Package\PackageTmp\Web.config 파일을 통해서 살펴볼 수 있습니다. 또는 FTP를 이용해서 배포된 Web.config 파일 자체에 접근할 수도 있습니다. 이에 관한 정보는 ASP.NET Web Deployment using Visual Studio: Deploying a Code Update 자습서를 참고하시기 바랍니다. "To use an FTP tool, you need three things: the FTP URL, the user name, and the password."라는 문장으로 시작하는 지시를 따라하시면 됩니다.

    노트: 본문의 예제 웹 응용 프로그램은 보안을 전혀 감안하지 않았으므로 URL을 알고 있는 사람은 누구라도 데이터를 변경할 수 있습니다. 보안을 감안하여 웹 사이트를 구현하는 방법은 Deploy a Secure ASP.NET MVC app with Membership, OAuth, and SQL Database to Azure 문서를 참고하시기 바랍니다. 일단 지금은 Azure 관리 포털이나 Visual Studio의 서버 탐색기(Server Explorer)에서 사이트를 중지하여 다른 사람들이 사이트를 사용하는 일을 막을 수 있습니다.

고급 마이그레이션 시나리오

만약 여러 대의 서버에서 실행되는 웹 사이트를 배포할 때, 본 자습서에서처럼 마이그레이션을 이용해서 자동으로 데이터베이스를 배포한다면 동시에 다수의 서버에서 마이그레이션을 수행하려고 시도하는 상황이 벌어지게 됩니다. 마이그레이션은 원자적(Atomic)이기 때문에, 만약 두 서버에서 동일한 마이그레이션을 수행하려고 시도할 경우, 한 서버는 성공하고 다른 서버는 실패하게 됩니다 (작업을 두 번 수행할 수 없다고 가정하고). 이런 경우 문제를 피하기 위해서는 마이그레이션이 한 번만 수행되도록 코드를 구성하고 직접 수작업으로 마이그레이션을 호출할 수 있습니다. 이에 관한 더 자세한 정보는 Rowan Miller의 Running and Scripting Migrations from Code 블로그 포스트와 MSDN의 Migrate.exe 문서(명령 프롬프트에서 마이그레이션을 실행하는 방법을 다루는)를 참고하시기 바랍니다.

그 밖의 다른 마이그레이션 시나리오들에 관한 정보는 Migrations Screencast Series 포스트를 참고하시기 바랍니다.

Code First 이니셜라이저

본문에서는 마이그레이션을 이용한 배포 시 MigrateDatabaseToLatestVersion 이니셜라이저가 사용되고 있는 것을 살펴봤습니다. 그 밖에도 CreateDatabaseIfNotExists (기본), DropCreateDatabaseIfModelChanges (이미 사용해봤습니다), 그리고 DropCreateDatabaseAlways를 비롯한 다른 이니셜라이저들이 제공됩니다. 가령 DropCreateDatabaseAlways 이니셜라이저의 경우 단위 테스트 조건을 설정할 때 유용하게 사용할 수 있습니다. 직접 사용자 지정 이니셜라이저를 작성할 수도 있고, 응용 프로그램이 데이터베이스의 데이터를 읽거나 쓸 때까지 기다리는 것이 싫다면 명시적으로 이니셜라이저를 호출할 수도 있습니다. 본 자습서를 작성 중인 2013년 11월 현재는 마이그레이션을 활성화시키기 전에는 Create와 DropCreate 이니셜라이저만 사용이 가능합니다. Entity Framework 팀에서는 이런 이니셜라이저들을 마이그레이션과 함께 사용할 수 있도록 노력하고 있습니다.

이니셜라이저에 대한 더 많은 정보는 Understanding Database Initializers in Entity Framework Code First 문서와 Julie Lerman와 Rowan Miller가 집필한 Programming Entity Framework: Code First 서적의 6장을 참고하시기 바랍니다.

요약

본문에서는 마이그레이션을 활성화시키고 응용 프로그램을 배포하는 방법을 살펴봤습니다. 다음 단계에서는 데이터 모델을 확장시켜보면서 보다 고급의 주제들을 살펴보도록 하겠습니다.

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

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

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