파트 4: ASP.NET Core MVC - 모델 추가하기

등록일시: 2016-06-13 08:00,  수정일시: 2016-08-22 13:48
조회수: 16,432
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
이번 파트에서는 모델 클래스를 추가한 다음 이를 바탕으로 Entity Framework Core를 이용해서 데이터베이스를 생성해보고, 강력한 형식의 모델에 관해서도 살펴봅니다.

본문에서는 데이터베이스에 저장되는 영화 정보를 관리할 클래스를 추가해보겠습니다. 이 클래스가 MVC 응용 프로그램의 "모델(Model)" 역할을 수행하게 됩니다.

그리고 본 자습서에서는 Entity Framework Core라는 .NET 프레임워크의 데이터-접근 기술을 이용해서 필요한 데이터 모델 클래스를 정의하고 사용합니다. Entity Framework Core는 (EF Core라고 부르기도 합니다) Code First 라는 이름의 개발 패러다임을 특징으로 삼고 있습니다. 이 방식에서는 먼저 코드를 작성하면 그 코드를 기반으로 데이터베이스의 테이블들이 생성됩니다. Code First를 사용하면 간단한 클래스들을 작성해서 데이터 모델 개체들을 생성할 수 있습니다 (이런 클래스를 POCO 클래스라고 하는데, "Plain-Old CLR objects"의 약자입니다). 그러면 데이터베이스는 이 클래스들을 기반으로 자동으로 생성됩니다. 만약 여러분이 데이터베이스를 먼저 생성해야 하는 상황이라도, 계속 본 자습서를 통해서 MVC와 EF 응용 프로그램 개발에 대한 다양한 정보들을 살펴보시기를 권해드립니다.

데이터 모델 클래스 추가하기

먼저 솔루션 탐색기(Solution Explorer)에서 Models 폴더를 마우스 오른쪽 버튼으로 클릭한 다음, 추가(Add) > 클래스(Class)를 선택합니다. 그리고 클래스 이름을 Movie로 지정하여 생성하고 다음과 같은 속성들을 추가합니다:

using System;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}

이제 클래스에 영화 정보를 구성하는 속성들을 비롯해서 데이터베이스에서 기본 키로 사용할 ID 필드까지 추가되었습니다. 여기까지 작업을 마쳤으면 일단 프로젝트를 빌드합니다. 만약 지금 응용 프로그램을 빌드하지 않으면 다음 절에서 오류가 발생하게 됩니다. 이로써 마침내 MVC 응용 프로그램에 모델을 추가했습니다.

컨트롤러 스캐폴딩

다시 솔루션 탐색기(Solution Explorer)에서 마우스 오른쪽 버튼으로 Controllers 폴더를 클릭하고 추가(Add) > 컨트롤러(Controller)를 선택합니다.

그러면 스캐폴드 추가(Add Scaffold) 대화 상자가 나타나는데, 목록에서 MVC Controller with views, using Entity Framework 스캐폴드 확장을 선택하고 추가(Add) 버튼을 누릅니다.

역주: 2016년 6월 11일 현재, Update 2가 설치된 Visual Studio 2015 상에서 이 스캐폴드 확장은 아직 한글 번역이 안되어 있습니다.

그런 다음, 아래와 같이 컨트롤러 추가(Add Controller) 대화 상자의 항목들을 입력하여 완성합니다.

  • 모델 클래스(Model class): Movie(MvcMovie.Models)
  • 데이터 컨텍스트 클래스(Data context class): ApplicationDbContext(MvcMovie.Models)
  • 뷰(Views):: 기본으로 체크되어 있는 각 옵션의 상태를 그대로 유지합니다.
  • 컨트롤러 이름(Controller name): 기본으로 지정된 MoviesController 를 그대로 사용합니다.
  • 추가(Add) 버튼을 누릅니다.

그러면 Visual Studio의 스캐폴딩 엔진이 다음과 같은 파일들과 폴더들을 생성해줍니다:

  • Movies 컨트롤러 (Controllers/MoviesController.cs)
  • Create, Delete, Details, Edit 그리고 Index Razor 뷰 파일들 (Views/Movies)

다시 말해서 Visual Studio가 자동으로 CRUD(Create, Read, Update, Delete) 작업과 관련된 액션 메서드들과 뷰들을 생성해주는 것입니다 (이렇게 CRUD 액션 메서드들과 뷰들을 자동으로 생성해주는 작업을 스캐폴딩(Scaffolding) 이라고 합니다). 이제 준비가 거의 마무리 되었으므로, 잠시 후면 영화 정보를 생성하고 조회하고 수정하고 삭제할 수 있는 완벽한 기능의 웹 응용 프로그램을 만들 수 있습니다.

응용 프로그램을 실행하고 Mvc Movie 링크를 클릭해봅니다. 그러면 기대와는 달리 다음과 같은 오류가 발생할 것입니다:

오류 메시지가 매우 구체적인 편인데, 지금부터 이 메시지의 지시에 따라 Movie 응용 프로그램을 위한 데이터베이스를 준비해보겠습니다.

역주: 2016년 6월 11일 현재, 오류 메시지의 내용이 이 그림 보다도 더욱 자세하게 보완되었으며 마이그레이션을 즉시 실행할 수 있는 것으로 추측되는 "Apply Migrations"라는 버튼도 추가되었습니다.

데이터 마이그레이션을 이용해서 데이터베이스 생성하기

  • 프로젝트가 위치한 디렉터리에서 명령 프롬프트를 엽니다 (MvcMovie/src/MvcMovie). 다음의 지시대로 따르면 프로젝트 디렉터리에서 폴더를 신속하게 열 수 있습니다.

    • 프로젝트 루트에 위치한 임의의 파일을 엽니다 (본문에서는 Startup.cs 파일을 열어보겠습니다.)
    • 마우스 오른쪽 버튼으로 Startup.cs 파일을 클릭하고 상위 폴더 열기(Open Containing Folder)를 선택합니다.

    • 시프트 키를 누르고 마우스 오른쪽 버튼으로 아무 폴더나 클릭한 다음, 여기서 명령 창 열기(Open command window here)를 선택합니다.

    • 명령 프롬프트에서 cd .. 명령을 실행해서 한 단계 위의 프로젝트 디렉터리로 이동합니다.
  • 명령 프롬프트에 다음 명령들을 차례대로 실행합니다:

    역주: 결론적으로 MvcMovie\src\MvcMovie 폴더에서 아래 명령들을 수행하면 됩니다. 이때 MvcMovie라는 이름의 폴더가 두 개이므로 그 정확한 위치에 주의하시기 바랍니다. 참고로 이 폴더에서 그냥 dotnet ef 라고만 입력하면 멋진 유니콘을 만나실 수 있습니다.
    dotnet ef migrations add Initial
    dotnet ef database update
  • dotnet (.NET Core)은 크로스-플랫폼을 지원하는 .NET의 구현입니다. 자세한 정보는 여기에서 확인하실 수 있습니다.
  • dotnet ef migrations add Initial 명령은 Entity Framework .NET Core CLI의 마이그레이션 명령을 실행하고 첫 번째 마이그레이션을 생성합니다. 이 명령에서 "Initial" 대신 다른 인자 값을 이름으로 지정할 수도 있지만, 관례적으로 첫 번째 데이터베이스 마이그레이션에는 "Initial"이라는 이름을 지정하는 경우가 대부분입니다. 이 작업을 수행하면 데이터베이스에 Movie 테이블을 추가하는 (또는 드랍하는) 마이그레이션 명령이 구현된 Data/Migrations/2016<date-time>_Initial.cs 파일이 생성됩니다.
  • dotnet ef database update 명령은 방금 생성한 마이그레이션을 이용해서 데이터베이스를 갱신합니다.

응용 프로그램 테스트하기

  • 응용 프로그램을 실행하고 Mvc Movie 링크를 클릭합니다.
  • Create New 링크를 클릭한 다음, 영화 정보를 생성해봅니다.

노트

여러분이 사용 중인 로케일에 따라 Price 필드에 소수점이나 쉼표(",")를 정상적으로 입력할 수 없는 경우도 있습니다. 쉼표를 소수점으로 사용하는 비-영어권 로케일과 비 US-English 날짜 형식에 대해서 jQuery 유효성 검사 기능을 정상적으로 지원하려면 응용 프로그램이 국제화를 지원하도록 별도의 작업을 수행해야만 합니다. 구체적인 방법은 추가 자료 절을 참고하시기 바랍니다. 일단 지금은 간단히 10 같은 정수를 입력하시면 됩니다.

역주: 한글 환경에서 위의 그림과 동일한 날짜를 입력하면 Create 버튼을 눌러도 아무런 반응을 하지 않는 것처럼 보입니다. 실제로는 18일을 18월로 인식해서 모델의 유효성 검사에 실패하는 것이지만, 현재의 구성으로는 ReleaseDate 필드의 유효성 검사 오류 메시지가 출력되지 않아서 오류가 발생한 것을 감지하기가 어렵습니다.

모든 항목을 입력하고 Create 버튼을 클릭하면 서버로 폼이 제출되어 데이터베이스에 영화 정보가 저장됩니다. 그런 다음, 목록을 통해서 새로 생성된 영화 정보를 확인할 수 있는 /Movies URL로 재전송됩니다.

영화 정보들을 몇 개 더 생성해보면서 테스트를 수행해보시기 바랍니다. 그리고 Edit, Details, Delete 링크를 사용해서 나머지 다른 기능들도 모두 테스트해보시기 바랍니다.

생성된 코드 살펴보기

이번에는 Controllers/MoviesController.cs 파일을 열고 자동으로 생성된 Index 메서드의 코드를 살펴보겠습니다. Movies 컨트롤러에 생성된 Index 메서드 부분의 코드는 다음과 같습니다:

역주: 이제 기본적으로 스캐폴딩 되는 코드에 비동기 코드가 사용되고 있음을 알 수 있습니다.
public class MoviesController : Controller
{
    private readonly ApplicationDbContext _context;

    public MoviesController(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<IActionResult> Index()
    {
        return View(await _context.Movie.ToListAsync());
    }

이 컨트롤러의 생성자에서는 의존성 주입(Dependency Injection)을 이용해서 컨트롤러에 테이터베이스 컨텍스트를 주입하고 있습니다. 데이터베이스 컨텍스트는 컨트롤러의 각 CRUD 메서드들에서 사용됩니다.

이 Movies 컨트롤러에 요청이 전달되면 이 Index 메서드가 Movies 테이블에 존재하는 모든 영화 정보 항목들을 가져온 다음, 그 데이터를 Index 뷰에 전달해줍니다.

강력한 형식의 모델과 @model 키워드

이전 파트에서는 ViewData 사전을 이용해서 컨트롤러에서 데이터나 개체를 뷰 템플릿으로 전달하는 방법을 살펴봤습니다. 이 방식에 사용되는 ViewData 사전은 동적 개체로 뷰에 정보를 전달할 수 있는 편리한 런타임 바인딩(Late-Bound) 기법을 제공해줍니다.

그러나 MVC는 강력한 형식의 개체를 뷰 템플릿에 전달할 수 있는 기능도 제공하고 있습니다. 강력한 형식을 기반으로 하는 이 접근 방식을 사용하면, 컴파일 시점에 보다 상세한 코드 검사가 가능하며 Visual Studio에서 풍부한 인텔리센스 기능을 사용할 수 있습니다. 이미 본문에서 Visual Studio의 스캐폴딩 메커니즘이 MoviesController 클래스와 뷰 템플릿들을 만들때, 바로 이 접근 방식을 적용해서 (즉, 강력한 형식의 모델을 전달하는 방식으로) 메서드들과 뷰들을 생성했습니다.

그러면 이번에는 Controllers/MoviesController.cs 파일에 생성된 Details 메서드를 살펴보겠습니다:

// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

이 메서드의 id 매개변수에는 대부분 라우트 데이터가 전달되는데, 가령 http://localhost:1234/movies/details/1이라는 URL을 요청할 경우:

  • 컨트롤러에는 Movies 컨트롤러가 (첫 번째 URL 세그먼트)
  • 액션 메서드로는 details 메서드가 (두 번째 URL 세그먼트)
  • id 매개변수에는 1이 설정됩니다 (마지막 URL 세그먼트).

또는 다음과 같이 쿼리 문자열을 통해서 id 값을 전달할 수도 있습니다:

http://localhost:1234/movies/details?id=1

전달받은 id 매개변수 값과 일치하는 영화 정보가 존재하면, 다음과 같이 그에 해당하는 Movie 모델의 인스턴스가 Details 뷰로 전달됩니다:

return View(movie);

계속해서 Views/Movies/Details.cshtml 파일의 내용을 살펴보겠습니다:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h2>Details</h2>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Price)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Title)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.ID">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

이 코드에서 볼 수 있는 것처럼 뷰 템플릿 파일의 가장 상단에 @model 구문을 추가하면 뷰에서 사용하고자 하는 개체 형식을 지정할 수 있습니다. Details.cshtml 파일의 상단에는 Visual Studio가 Movies 컨트롤러 클래스를 생성하면서 다음과 같은 @model 구문을 자동으로 추가해놨습니다:

@model MvcMovie.Models.Movie

이렇게 @model 지시자를 지정하면 강력한 형식인 Model 개체를 이용해서 컨트롤러로에서 뷰에 전달된 영화 정보에 접근할 수 있습니다. 가령, Details.cshtml 뷰 템플릿의 코드에서도 이 강력한 형식의 Model 개체를 이용해서 영화 정보 필드들을 DisplayNameFor 헬퍼 메서드와 DisplayFor 헬퍼 메서드에 전달하고 있습니다. Create 메서드와 Edit 메서드 및 관련 뷰 템플릿들 역시 Movie 모델 개체를 전달하고 받습니다.

다시 Index.cshtml 뷰 템플릿과 Movies 컨트롤러의 Index 메서드를 살펴보겠습니다. 특히 Index 액션 메서드의 코드에서 View 메서드를 호출할 때, List 개체를 생성하는 방식을 자세히 살펴보시기 바랍니다. 이 코드는 생성된 Movie 개체들의 목록을 Index 액션 메서드에서 뷰로 전달합니다:

public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}

그리고 Visual Studio는 Movies 컨트롤러를 생성하면서 Index.cshtml 파일의 상단에도 자동으로 다음과 같은 @model 구문을 추가합니다:

@model IEnumerable<MvcMovie.Models.Movie>

역시 이번에도 @model 지시자로 인해서 강력한 형식인 Model 개체를 통해서 컨트롤러에서 뷰로 전달된 영화 정보의 목록에 접근할 수 있습니다. 가령 Index.cshtml 템플릿의 코드에서는 foreach 구문을 이용해서 강력한 형식인 Model 개체의 영화 정보 목록을 대상으로 반복 작업을 수행합니다:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>
<p>
    <a asp-action="Create">Create New</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

이 코드에서 Model 개체는 강력한 형식의 개체, 즉 IEnumerable<Movie> 개체이기 때문에 루프 내부의 item 개체는 Movie 형식일 수 밖에 없습니다. 결과적으로 다른 여러 가지 장점들과 함께, 컴파일 시점 코드 검사와 코드 편집기 내에서 완벽한 인텔리센스의 지원을 받을 수 있습니다:

이제 드디어 데이터베이스 뿐만 아니라 데이터를 출력하고, 편집하고, 수정하고, 삭제할 수 있는 페이지들까지 만들었습니다. 다음 파트에서는 데이터베이스와 관련된 작업들을 수행해 보겠습니다.