파트 9: 새로운 필드 추가하기

등록일시: 2015-06-08 08:00,  수정일시: 2016-09-02 09:02
조회수: 8,867
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

이번 단계에서는 모델 클래스를 일부 수정한 다음, Entity Framework Code First 마이그레이션을 이용해서 데이터베이스에 변경된 사항들을 적용해보도록 하겠습니다.

본 자습서 시리즈에서 이미 살펴본 것처럼, Entity Framework Code First를 이용해서 데이터베이스를 자동으로 생성하면 Code First가 생성된 데이터베이스 스키마와 그에 대응하는 모델 클래스 사이에 동기화가 필요한지 여부를 지속적으로 추적하기 위한 용도의 테이블을 데이터베이스에 함께 추가해줍니다. 그리고 만약 데이터베이스 스키마가 모델 클래스와 일치하지 않으면 Entity Framework가 오류를 발생시켜줍니다. 따라서 어쩌면 뒤늦게 런타임에 가서야 발견될 수도 있는 (모호한 오류로 인한) 문제점을 개발 시에 손쉽게 파악할 수 있습니다.

모델 변경을 위해 Code First 마이그레이션 구성하기

먼저 솔루션 탐색기(Solution Explorer)에서 마우스 오른쪽 버튼으로 Movies.mdf  파일을 클릭하고 삭제(Delete)를 선택해서 데이터베이스를 삭제합니다. 만약 솔루션 탐색기(Solution Explorer)Movies.mdf  파일이 나타나지 않는다면, 다음 그림에 빨간색 상자로 표시된 모든 파일 표시(Show All Files) 아이콘을 클릭합니다.

그리고 응용 프로그램을 빌드해서 오류가 발생하지 않음을 확인합니다.

도구(Tools) 메뉴에서 NuGet 패키지 관리자(Library Package Manager) 하위의 패키지 관리자 콘솔(Package Manager Console)을 선택합니다.

그런 다음, 패키지 관리자 콘솔(Package Manager Console) 창의 PM> 프롬프트에 다음 명령을 입력합니다:

Enable-Migrations -ContextTypeName MvcMovie.Models.MovieDBContext

Enable-Migrations 명령어을 수행하면 Migrations 라는 새로운 폴더가 생성되고 다시 그 하위에 Configuration.cs 라는 파일이 생성됩니다.

파일 생성이 완료되면 Visual Studio가 새로 생성된 Configuration.cs 파일을 자동으로 열어주는데, 이 Configuration.cs 파일의 Seed 메서드를 다음 코드로 대체합니다:

protected override void Seed(MvcMovie.Models.MovieDBContext context)
{
    context.Movies.AddOrUpdate( i => i.Title,
        new Movie
        {
            Title = "When Harry Met Sally",
            ReleaseDate = DateTime.Parse("1989-1-11"),
            Genre = "Romantic Comedy",
            Price = 7.99M
        },

        new Movie
        {
            Title = "Ghostbusters ",
            ReleaseDate = DateTime.Parse("1984-3-13"),
            Genre = "Comedy",
            Price = 8.99M
        },

        new Movie
        {
            Title = "Ghostbusters 2",
            ReleaseDate = DateTime.Parse("1986-2-23"),
            Genre = "Comedy",
            Price = 9.99M
        },

        new Movie
        {
            Title = "Rio Bravo",
            ReleaseDate = DateTime.Parse("1959-4-15"),
            Genre = "Western",
            Price = 3.99M
        }
    );
}

그러면 Movie 클래스 명 밑에 구불구불한 빨간색 밑줄이 표시됩니다. 마우스 오른쪽 버튼으로 클래스 명을 클릭하고 확인(Resolve)을 선택한 다음, 다시 using MvcMovie.Models;를 선택합니다.

그러면 다음과 같은 using 구문이 추가됩니다:

using MvcMovie.Models;
마이그레이션을 수행할 때마다 (즉, 패키지 관리자 콘솔에서 update-database 명령을 호출할 때마다) Code First 마이그레이션에 의해서 Seed 메서드가 호출됩니다. 그러면 이 메서드에 의해서 이미 입력되어 있는 데이터 로우들이 갱신되거나 존재하지 않는 데이터가 새로 입력됩니다.

다음 코드의 AddOrUpdate 메서드는 일명 "Upsert"라고 부르는 작업을 수행합니다:
context.Movies.AddOrUpdate(i => i.Title,
    new Movie
    {
        Title = "When Harry Met Sally",
        ReleaseDate = DateTime.Parse("1989-1-11"),
        Genre = "Romantic Comedy",
        Rating = "PG",
        Price = 7.99M
    }
매번 마이그레이션을 수행할 때마다 Seed 메서드가 다시 실행되기 때문에 이 메서드에서는 단지 데이터를 입력하기만 해서는 안됩니다. 이번에 입력하고자 하는 데이터 로우들이 데이터베이스가 처음 생성된 최초 마이그레이션 시에 이미 입력되어 존재하고 있을 수도 있기 때문입니다. "Upsert" 작업은 이미 존재하는 로우를 다시 입력하려고 시도할 때 발생하는 오류를 방지해주기는 하지만, 응용 프로그램을 테스트하면서 반영된 모든 데이터 변경사항들은 단순히 다시 덮어써질 뿐입니다. 물론 그런 결과를 바라지 않는 테스트 데이터들을 담고 있는 테이블도 충분히 있을 수 있습니다. 즉 데이터베이스를 마이그레이션 하고 난 뒤에도 테스트 중에 변경된 데이터들이 그대로 남아 있기를 원할 수도 있는 것입니다. 그런 경우에는 데이터 로우가 이미 존재하지 않는 경우에만 새로 입력되도록 선택적으로 입력 작업을 수행해야 합니다.

이 코드에 사용된 AddOrUpdate 메서드에 전달되는 첫 번째 매개변수에는 지정한 데이터가 이미 데이터베이스에 존재하는 로우인지 여부를 확인하기 위한 속성을 지정합니다. 본문의 예제에 사용된 테스트 영화 데이터의 경우, 각 영화 제목들이 전체 목록에서 유일하기 때문에 Title 속성을 이 용도로 사용할 수 있습니다:
context.Movies.AddOrUpdate(i => i.Title,
다만, 이 코드는 모든 영화 제목들이 유일하다고 가정하에 작성된 코드라는 점에 주의하십시오. 만약 의도적으로 중복된 제목을 입력하면 다음 번 마이그레이션 시 다음과 같은 예외가 발생하는 것을 확인할 수 있을 것입니다.

Sequence contains more than one element

AddOrUpdate 메서드에 대한 더 많은 정보는 Take care with EF 4.3 AddOrUpdate Method 문서를 참고하시기 바랍니다.

이제 CTRL-SHIFT-B 키를 눌러서 프로젝트를 빌드합니다. (지금 프로젝트를 빌드하지 않으면 이후의 과정들이 실패하게 됩니다.)

다음으로 할 일은 초기 마이그레이션을 위한 DbMigration 클래스를 생성하는 것입니다. 이 초기 마이그레이션 시에 새로운 데이터베이스가 생성되는데, 바로 이 점 때문에 본문의 첫 번째 단계에서 movie.mdf  파일을 삭제했던 것입니다.

패키지 관리자 콘솔(Package Manager Console) 창에 add-migration Initial 명령을 입력해서 초기 마이그레이션을 생성합니다. 이 명령에서 "Initial"라는 이름은 임의로 입력한 것으로, 자동으로 생성되는 마이그레이션 파일의 이름을 구성하는데 사용됩니다.

그러면 Code First 마이그레이션이 Migrations 폴더에 클래스 파일을 하나 더 생성해주는데, 이 클래스에는 데이터베이스 스키마를 생성하는 코드가 담겨 있습니다. 또한 마이그레이션 파일의 이름은 작성 시점을 기준으로 정렬될 수 있도록 {DateStamp}_Initial.cs 와 같이 파일 이름 앞에 타임스템프가 전치사로 붙습니다. {DateStamp}_Initial.cs 파일에 생성된 코드를 살펴보면 데이터베이스에 Movies라는 이름의 테이블을 생성하는 명령이 생성되어 있습니다. 바로 다음에 살펴볼 지시에 따라 데이터베이스를 갱신할 때, 이 {DateStamp}_Initial.cs 파일이 실행되어 데이터베이스 스키마를 생성하게 됩니다. 그런 다음 Seed 메서드가 실행되어 데이터베이스에 테스트 데이터들을 채워 넣게 되는 것입니다.

패키지 관리자 콘솔(Package Manager Console) 창에 update-database 명령을 입력해서 데이터베이스를 생성하고 Seed 메서드를 실행합니다.

만약 이 명령을 실행할 때, 테이블이 이미 존재하기 때문에 테이블 생성이 불가능하다는 오류가 발생한다면, 아마도 데이터베이스를 삭제하고 update-database 명령을 수행하기 전에 응용 프로그램을 실행해본 적이 있기 때문일 것입니다. 그런 경우에는 Movies.mdf  파일을 다시 지운 다음, update-database 명령을 재시도해보시기 바랍니다. 그래도 계속해서 오류가 발생한다면, Migrations 폴더와 그 하위의 파일들을 모두 지운 다음, 본문의 내용을 처음부터 (Movies.mdf  파일을 삭제하고 Enable-Migrations 명령을 실행하는 과정부터) 하나씩 다시 실행해보시기 바랍니다.

이제 응용 프로그램을 실행하고 /Movies URL로 이동해보면, 다음 그림과 같이 시드된 데이터가 출력될 것입니다.

Movie 모델에 Rating 속성 추가하기

계속해서 이번에는 기존의 Movie 클래스에 Rating라는 이름을 가진 새로운 속성을 추가해보겠습니다. Models\Movie.cs 파일을 열고 다음과 같이 Rating 속성을 추가합니다:

public string Rating { get; set; }

이 작업을 마치고 나면 Movie 클래스의 모습은 다음과 비슷할 것입니다:

public class Movie
{
    public int ID { get; set; }
    public string Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; }
    public decimal Price { get; set; }
    public string Rating { get; set; }
}

그런 다음, Ctrl+Shift+B 키를 눌러서 다시 응용 프로그램을 빌드합니다.

이제 Movie 클래스에 새로운 필드가 추가됐으므로 바인딩 될 속성들을 지정하는 화이트 리스트(White List)에도 이 필드를 추가해줘야 합니다. 다음과 같이 Create 액션 메서드와 Edit 액션 메서드에 지정된 bind 어트리뷰트의 Include 속성에 Rating 속성을 추가합니다:

[Bind(Include = "ID,Title,ReleaseDate,Genre,Price,Rating")]

당연한 얘기지만 브라우저에서 이 새로운 Rating 속성의 값을 출력하고 생성하고 편집하려면 뷰 템플릿들도 수정해야 합니다.

먼저 \Views\Movies\Index.cshtml 파일을 열고 Price 컬럼 바로 뒤에 <th>Rating</th> 컬럼 헤더를 추가합니다. 그리고 템플릿의 마지막 부분에 @item.Rating 값을 렌더할 <td> 컬럼도 추가해줍니다. 다음 코드는 이렇게 변경을 마친 Index.cshtml 뷰 템플릿의 마크업을 보여주고 있습니다:

@model IEnumerable<MvcMovie.Models.Movie>
@{
    ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
    @Html.ActionLink("Create New", "Create")
    @using (Html.BeginForm("Index", "Movies", FormMethod.Get))
    {
    <p>
        Genre: @Html.DropDownList("movieGenre", "All")
        Title: @Html.TextBox("SearchString")
        <input type="submit" value="Filter" />
    </p>
    }
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Genre)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Price)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Rating)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Title)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.ReleaseDate)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Genre)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Price)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Rating)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
            @Html.ActionLink("Details", "Details", new { id=item.ID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.ID })
        </td>
    </tr>
}

</table>

다시 이번에는 \Views\Movies\Create.cshtml 파일을 열고 다음 코드에 표시된 마크업을 입력해서 Rating 필드를 추가합니다. 이 마크업은 텍스트 박스를 렌더해주는데, 새로운 영화 정보를 생성할 때 이 텍스트 박스를 이용해서 등급을 지정할 수 있습니다.

        <div class="form-group">
            @Html.LabelFor(model => model.Price, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Price)
                @Html.ValidationMessageFor(model => model.Price)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Rating, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Rating)
                @Html.ValidationMessageFor(model => model.Rating)
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

지금까지 새로운 Rating 속성을 지원하기 위해서 응용 프로그램의 코드를 수정해봤습니다.

그러나 막상 응용 프로그램을 실행하고 /Movies URL로 이동해보면, 다음 오류 중 한 가지 오류가 발생할 것입니다:

이렇게 오류가 발생하는 이유는 변경된 응용 프로그램의 Movie 모델 클래스와 데이터베이스의 Movie 테이블 스키마가 서로 일치하지 않기 때문입니다. (즉, 데이터베이스의 테이블에는 아직 Rating 속성에 해당하는 컬럼이 존재하지 않습니다.)

다음과 같이 이 오류를 해결할 수 있는 몇 가지 접근 방법이 존재합니다:

  1. Entity Framework가 자동으로 데이터베이스를 드랍시키고 새로운 모델 클래스 스키마를 기반으로 데이터베이스를 재생성하도록 만들 수 있습니다. 이 방식은 모델과 데이터베이스를 신속하게 변경할 수 있어서 테스트 데이터베이스를 이용해서 개발이 한창 진행 중인 초기에 매우 편리합니다. 그러나 데이터베이스에 존재하던 기존 데이터들이 사라지게 되므로, 절대로 운영 데이터베이스를 대상으로는 사용하면 안된다 는 단점이 있습니다! 이니셜라이저를 이용해서 데이터베이스에 자동으로 테스트 데이터를 시드하는 방식은 생산적인 응용 프로그램 개발에 많은 도움이 됩니다. Entity Framework 데이터베이스 이니셜라이저에 대한 더 많은 정보는 Tom Dykstra가 집필한 멋진 ASP.NET MVC/Entity Framework 자습서를 참고하시기 바랍니다.
  2. 명시적으로 직접 데이터베이스의 스키마를 변경해서 모델 클래스와 일치시킬 수도 있습니다. 이 방법의 장점은 기존 데이터를 그대로 유지할 수 있다는 점입니다. 직접 수작업으로 데이터베이스를 변경할 수도 있고 데이터베이스 변경 스크립트를 작성해서 필요한 작업을 수행할 수도 있습니다.
  3. Code First 마이그레이션을 이용해서 데이터베이스의 스키마를 갱신할 수도 있습니다.

본문에서는 이 세 가지 방법 중 Code First 마이그레이션 방식을 사용해보도록 하겠습니다.

먼저 새로 추가된 컬럼에도 값을 제공할 수 있도록 Seed 메서드를 수정합니다. Migrations\Configuration.cs 파일을 열고 Rating 필드를 다음과 같은 방식으로 모든 Movie 개체에 추가합니다.

new Movie
{
    Title = "When Harry Met Sally",
    ReleaseDate = DateTime.Parse("1989-1-11"),
    Genre = "Romantic Comedy",
    Rating = "PG",
    Price = 7.99M
},

그리고 솔루션을 빌드한 다음, 패키지 관리자 콘솔(Package Manager Console) 창을 열고 다음 명령을 입력합니다:

add-migration Rating

이 명령은 마이그레이션 프레임워크에게 응용 프로그램의 현재 모델과 데이터베이스의 현재 스키마를 검토해서 데이터베이스를 새로운 모델로 마이그레이션 하는데 필요한 코드를 생성하도록 지시합니다. 이 명령에 사용된 Rating 라는 이름은 임의로 지정한 것으로, 마이그레이션 파일의 이름을 구성하는데 사용됩니다. 따라서 마이그레이션 단계를 잘 나타낼 수 있는 의미 있는 이름을 지정하는 것이 좋습니다.

이 명령을 수행하고 나면 Visual Studio가 DbMIgration 클래스를 상속 받는 새로운 클래스 파일을 열어주는데, 이 클래스의 Up 메서드를 살펴보면 새로운 컬럼을 추가해주는 코드를 확인할 수 있습니다.

public partial class AddRatingMig : DbMigration
{
    public override void Up()
    {
        AddColumn("dbo.Movies", "Rating", c => c.String());
    }
    
    public override void Down()
    {
        DropColumn("dbo.Movies", "Rating");
    }
}

다시 솔루션을 빌드한 다음, 이번에는 패키지 관리자 콘솔(Package Manager Console) 창에 update-database 명령을 입력합니다.

다음 그림은 패키지 관리자 콘솔(Package Manager Console) 창의 출력을 보여줍니다. (여러분이 직접 이 명령을 실행해보면 Rating 앞 부분에 나타나는 타임스템프 부분은 이 그림과 다를 것입니다.)

다시 응용 프로그램을 실행하고 /Movies URL로 이동합니다. 그러면 이번에는 새로 추가된 Rating 필드가 정상적으로 나타나는 것을 확인할 수 있습니다.

역주: 다음 그림에는 지난 번 과정에서 구현했던 검색 기능이 누락되어 있습니다. 단순한 편집 상의 실수인 것으로 보입니다.

계속해서 Create New 링크를 클릭해서 새로운 영화 정보를 입력해 보겠습니다. 이제 영화의 등급을 지정할 수 있다는 점에 주목하시기 바랍니다.

역주: 여러분이 본문의 지시대로 예제를 충실히 수행해왔다면 직접 작성한 Rating 필드의 스타일과 아래 그림에 나타난 Rating 필드의 스타일이 다를 것입니다. 그 이유는 본문의 뷰 템플릿 마크업 자체와 추가한 코드 조각에 Bootstrap 관련 클래스들을 지정하는 익명 개체가 누락되어 있기 때문입니다. 그러나 뷰의 스타일링은 본문에서 전달하고자 하는 주제 자체와 큰 관련이 없기 때문에 이 점은 참고만 하고 그냥 넘어가도록 하겠습니다.

적절한 정보들을 입력한 다음 Create 버튼을 클릭하면, 등급 정보가 포함된 새로운 영화 정보가 목록에 나타나는 것을 확인할 수 있을 것입니다:

이제 프로젝트에서 마이그레이션을 사용할 수 있으므로 새로운 필드를 추가하거나 다양한 이유로 스키마를 갱신해야 할 때 데이터베이스를 드랍시킬 필요가 없습니다. 이후의 과정에서는 더 많은 스키마를 변경하고 마이그레이션을 이용해서 데이터베이스를 갱신해보게 될 것입니다.

마지막으로 Edit 뷰와 Details 뷰, 그리고 Delete 뷰 템플릿에도 Rating 필드에 대한 마크업을 추가해줘야 합니다.

지금 상태에서는 패키지 관리자 콘솔(Package Manager Console) 창에 다시 한 번 update-database 명령을 입력한다고 하더라도, 데이터베이스의 스키마와 모델이 서로 일치하기 때문에 마이그레이션 코드가 실행되지 않습니다. 그러나 이런 경우에도 Seed 메서드는 매번 다시 실행되므로 주의해야만 합니다. 변경된 시드 데이터가 존재한다면 Seed 메서드가 데이터를 다시 입력하거나 원래 상태로 갱신하기 때문에 변경된 모든 내용들이 사라지게 됩니다. Seed 메서드에 대한 더 많은 정보는 Tom Dykstra의 ASP.NET MVC/Entity Framework 자습서를 참고하시기 바랍니다.

지금까지 모델 개체를 변경하는 방법과 변경 사항을 데이터베이스에 반영하는 방법을 살펴봤습니다. 그리고 새로 생성된 데이터베이스에 기본 데이터를 채워 넣을 필요가 있는 상황의 시나리오도 살펴봤습니다. 하지만 본문에서 살펴본 내용들은 Code First에 대한 간단한 소개 정도에 불과하므로, 이 주제에 관해서 더 자세한 내용을 다루고 있는 자습서인 Creating an Entity Framework Data Model for an ASP.NET MVC Application을 살펴볼 것을 권해드립니다. 계속해서 다음 과정에서는 모델 클래스에 다채로운 유효성 검사 로직을 추가하는 방법과 특정 업무 로직을 강제하는 방법을 살펴보도록 하겠습니다.

이 기사는 2013년 10월 17일에 최초 작성되었습니다.