파트 2: ASP.NET MVC 응용 프로그램에서 Entity Framework로 CRUD 구현하기

등록일시: 2015-11-02 08:00,  수정일시: 2016-09-02 09:11
조회수: 13,050
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
이번 파트에서는 MVC의 스캐폴딩 기능이 자동으로 생성해준, Student 엔터티를 대상으로 간단한 CRUD 작업을 수행하는 코드들을 보완해보면서 Entity Framework의 기본적인 사용방법을 살펴봅니다.

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

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

이전 파트에서는 Entity Framework와 SQL Server LocalDB를 이용해서 데이터를 저장하거나 조회하는 MVC 응용 프로그램을 만들어봤습니다. 본문에서는 MVC의 스캐폴딩 기능이 자동으로 생성해준 컨트롤러와 뷰의 CRUD(Create, Read, Update, Delete) 관련 코드를 검토하고 개선해보겠습니다.

노트: 일반적으로 리파지터리 패턴을 구현해서 컨트롤러와 데이터 액세스 계층 간에 추상화 계층을 생성하는 경우가 많습니다. 그러나 본 자습서에서는 Entity Framework 자체의 사용법에 대해서만 관심을 집중하기 위해서 리파지터리를 사용하지 않고 있습니다. 리파지터리를 구현하는 방법에 대한 보다 정보는 ASP.NET Data Access Content Map 기사를 참고하시기 바랍니다.

본문에서는 다음과 같은 웹 페이지들을 구현해볼 것입니다:

Details 페이지 생성하기

이전 파트에서 스캐폴딩 기능을 이용해서 만든 Students 메뉴의 Index 페이지는 Student 엔터티의 Enrollments 속성을 무시하고 있는데, 그 이유는 이 속성이 컬렉션을 담고 있기 때문입니다. 이 컬렉션의 콘텐츠를 Details 페이지에 HTML 테이블의 형태로 출력해보겠습니다.

먼저 Controllers\StudentController.cs  파일에서 Details 뷰에 대응하는 액션 메서드를 살펴보면 Find 메서드를 이용해서 단일 Student 엔터티를 조회하고 있음을 알 수 있습니다.

public ActionResult Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

그리고 id 매개변수를 통해서 메서드에 키 값이 전달되고 있는데, 이 값은 Index 페이지에 출력된 Details 하이퍼링크의 라우트 데이터 로부터 비롯된 값입니다.

  1. 이번에는 Views\Student\Details.cshtml  파일을 열어봅니다. 다음 예제에서 볼 수 있는 것처럼 각 필드들은 DisplayFor 헬퍼 메서드를 이용해서 출력됩니다:

    <dt>
        @Html.DisplayNameFor(model => model.LastName)
    </dt>
    <dd>
        @Html.DisplayFor(model => model.LastName)
    </dd>
  2. 다음 예제 코드에 강조되어 있는 부분을 EnrollmentDate 필드 뒤에, 그리고 닫는 </dl> 태그 직전에 추가해서 수강 목록을 출력합니다:

            <dt>
                @Html.DisplayNameFor(model => model.EnrollmentDate)
            </dt>
    
            <dd>
                @Html.DisplayFor(model => model.EnrollmentDate)
            </dd>
    
            <dt>
                @Html.DisplayNameFor(model => model.Enrollments)
            </dt>
    
            <dd>
                <table class="table">
                    <tr>
                        <th>Course Title</th>
                        <th>Grade</th>
                    </tr>
                    @foreach (var item in Model.Enrollments)
                    {
                        <tr>
                            <td>
                                @Html.DisplayFor(modelItem => item.Course.Title)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => item.Grade)
                            </td>
                        </tr>
                    }
                </table>
            </dd>
    
        </dl>
    </div>
    <p>
        @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
        @Html.ActionLink("Back to List", "Index")
    </p>

    만약 코드를 붙여 넣은 다음, 들여쓰기가 올바르지 않다면 CTRL-K-D 단축키를 눌러서 들여쓰기를 정리합니다.

    이 코드는 Enrollments 탐색 속성에 담긴 엔터티들을 대상으로 루프를 돌면서, Enrollment 엔터티들의 강의 제목과 학점을 출력합니다. 이때, 강의 제목은 Enrollments 엔터티의 Course 탐색 속성에 저장된 Course 엔터티로부터 가져오게 되는데, 모든 데이터들은 필요한 시점에 데이터베이스로부터 자동으로 조회됩니다. (즉, 지연 로딩(Lazy Loading)이 사용됩니다. Courses 탐색 속성에 즉시 로딩(Eager Loading)을 사용하도록 지정하기 않았기 때문에, 학생 정보를 조회하는 쿼리에서 수강 정보까지 함께 조회되지는 않습니다. 그 대신, Enrollments 탐색 속성에 최초로 접근하는 순간, 새로운 쿼리가 데이터베이스로 전송되어 데이터를 가져오게 되는 것입니다. 지연 로딩과 즉시 로딩에 관해서는 본 자습서 시리즈 중, Reading Related Data 파트에서 보다 자세하게 살펴볼 것입니다.)

  3. 예제 응용 프로그램을 실행한 다음, Students 메뉴를 선택하고 Alexander Carson 학생의 Details 링크를 클릭해서 페이지를 실행합니다. (만약, Details.cshtml 파일을 편집 중인 상태에서 CTRL+F5 키를 눌러서 예제 응용 프로그램을 실행했다면 HTTP 400 오류가 발생할 것입니다. 그 이유는 기본적으로 Visual Studio가 편집 중이던 Details 페이지를 즉시 실행하려고 시도하기 때문인데, 이 경우 정보를 조회할 학생의 라우트 데이터가 설정되어 있는 링크를 클릭해서 이 페이지에 도달한 것이 아니므로, 응용 프로그램이 조회해야 할 학생이 누군지 알 수 없기 때문입니다. 따라서 간단히 URL에서 "Student/Details" 부분을 지우고 다시 시도하거나, 브라우저를 닫고 프로젝트를 마우스 오른쪽 버튼으로 클릭한 다음, 보기(View)브라우저에서 보기(View in Browser)를 순서대로 클릭하면 됩니다.)

    그러면 선택한 학생이 수강 중인 강의와 학점 정보들의 목록을 조회할 수 있습니다:

Create 페이지 수정하기

  1. 계속해서 이번에는 Controllers\StudentController.cs  파일에 스캐폴딩으로 생성된 HttpPost Create 액션 메서드를 다음 코드로 대체해서 try-catch 블럭을 추가하고 Bind 어트리뷰트에서 ID를 제외합니다:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include = "LastName,FirstMidName,EnrollmentDate")] Student student)
    {
        try
        {
            if (ModelState.IsValid)
            {
                db.Students.Add(student);
                db.SaveChanges();
                return RedirectToAction("Index");
            }
        }
        catch (DataException /* 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.");
        }
    
        return View(student);
    }

    이 코드는 ASP.NET MVC 모델 바인더에 의해 생성된 Student 엔터티를 Students 엔터티 집합에 추가한 다음, 데이터베이스에 변경 사항을 저장합니다. (모델 바인더(Model Binder) 는 폼을 통해서 제출된 데이터를 활용한 작업을 손쉽게 처리할 수 있도록 도와주는 ASP.NET MVC의 기능입니다. 모델 바인더는 전송된 폼 값들을 CLR 형식으로 변환한 다음, 이를 액션 메서드에 매개변수로 전달해줍니다. 예를 들어, 이번 예제에서는 모델 바인더가 Student 엔터티의 인스턴스를 자동으로 생성하고 그 속성 값들을 Form 컬렉션의 값들을 이용해서 채워줍니다.)

    그리고 Bind 어트리뷰트에서 ID를 제외시킨 이유는, ID 속성의 값이 로우가 입력되는 시점에 SQL Server가 자동으로 설정해주는 기본 키 값이기 때문입니다. 여기에 사용자가 입력한 ID 값은 사용되지 않습니다.

    보안 노트: 이 예제 코드에서처럼 ValidateAntiForgeryToken 어트리뷰트를 적용하면 크로스 사이트 요청 위조(Cross-Site Request Forgery) 공격을 방지하는데 도움이 됩니다. 잠시 뒤에 살펴보겠지만, 이 어트리뷰트가 정상적으로 동작하려면 뷰에서 이 어트리뷰트에 대응하는 Html.AntiForgeryToken() 구문을 사용해야 합니다.

    Bind 어트리뷰트 역시 데이터 생성 시나리오에서 오버포스팅(Over-Posting) 공격을 방지할 수 있는 한 가지 방법입니다. 가령, Student 엔터티에 웹 페이지에서는 변경이 불가능하도록 제한하고 싶은 Secret라는 가상의 속성이 존재한다고 가정해보겠습니다.

    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
        public string Secret { get; set; }
    
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }

    이 때, 웹 페이지에 Secret 필드를 노출하지 않았더라도 Fiddler 같은 도구를 이용하거나 간단한 JavaScript 코드만 작성해도 악의적인 사용자는 큰 어려움 없이 Secret 폼 값을 전송할 수 있습니다. 즉 Bind 어트리뷰트를 적용해서 모델 바인더가 사용할 수 있는 필드들을 제한하지 않는다면, 모델 바인더가 Student 엔터티의 인스턴스를 생성할 때 Secret 폼 값을 읽어서 그 값을 이용해서 Student 엔터티를 생성하게 됩니다. 결과적으로 악의적인 사용자가 Secret 폼 필드에 지정한 값으로 데이터베이스가 갱신되는 상황이 벌어지게 되는 것입니다. 다음은 전송되는 폼 값에 Fiddler 도구를 이용해서 Secret이라는 필드를("OverPost"라는 값이 지정된) 추가한 모습을 보여주고 있습니다.

    이런 상황이 벌어지면, 웹 페이지에서는 Secret 속성을 설정할 수 있도록 허용하지 않았는데도, 데이터베이스에 삽입될 로우의 Secret 속성에는 "OverPost"라는 값이 정상적으로 추가됩니다.

    이런 악의적인 공격에 대응할 수 있는 보안적으로 가장 좋은 방법은 Bind 어트리뷰트의 Include 매개변수를 이용해서 화이트 리스트(Whitelist) 필드들의 목록을 지정하는 것입니다. 다른 방법으로 Exclude 매개변수를 이용해서 배제하고자 하는 블랙 리스트(Blacklist) 필드들의 목록을 지정할 수도 있습니다. 보안적인 측면에서는 Include 매개변수를 사용하는 방식이 더 바람직한데, Exclude 매개변수를 사용하는 방식은 엔터티에 새로운 속성이 추가될 경우 해당 필드들이 자동으로 보호받지 못하기 때문입니다.

    또는 데이터베이스에서 엔터티를 읽어온 다음, TryUpdateModel 메서드를 호출해서 명시적으로 변경이 허용된 속성들의 목록을 전달하는 방식으로도 데이터 편집 시나리오에서 발생할 수 있는 오버포스팅 공격을 방지할 수 있습니다. 본 자습서 시리즈에서는 바로 이 방식을 사용하고 있습니다.

    마지막으로 오버포스팅 공격을 방지하기 위해서 많은 개발자들이 사용하고 있는 또 다른 대안으로는 엔터티 클래스 대신 뷰 모델을 이용해서 모델 바인딩을 수행하는 방법이 있습니다. 즉, 뷰 모델에는 수정하고자 하는 속성들만 포함시키고, MVC의 모델 바인더가 작업을 마치고 나면 뷰 모델의 속성들을 엔터티의 인스턴스로 복사하는데, 이때 필요에 따라 AutoMapper 같은 도구들을 이용할 수 있습니다. 그리고 db.Entry 메서드를 이용해서 엔터티 인스턴스의 상태를 Unchanged로 설정한 다음, 뷰 모델에 포함된 각각의 엔터티 속성들에 대해 Property("속성명").IsModified 속성을 true로 설정합니다. 이 방법은 데이터 편집 시나리오와 생성 시나리오 양쪽에 모두 사용 가능합니다.

    이 코드에서 Bind 어트리뷰트에 대한 변경을 제외한다면, 스캐폴딩으로 생성된 코드에 대한 남은 유일한 변경 사항은 try-catch 블럭을 추가한 것뿐입니다. 만약 변경 사항들이 저장되는 도중에 DataException에서 파생된 예외가 던져지면 포괄적인 오류 메시지가 출력됩니다. 이 DataException 예외는 프로그래밍 오류보다는 무언가 응용 프로그램 외부의 요인으로 인해서 발생하는 경우가 많으므로, 작업을 다시 시도하도록 사용자를 유도하는 메시지를 출력하고 있습니다. 본문의 예제 코드에는 구현되지 않았지만 운영 환경에서 실행되는 응용 프로그램에서는 발생한 예외의 내용을 로그로 기록하는 경우가 많습니다. 이에 대한 보다 많은 정보는 Monitoring and Telemetry (Building Real-World Cloud Apps with Azure) 문서의 Log for insight 절을 참고하시기 바랍니다.

    그리고 Views\Student\Create.cshtml  파일에 작성된 코드는 각 필드들에 대해 DisplayFor 헬퍼 메서드 대신 EditorFor 헬퍼 메서드와 ValidationMessageFor 헬퍼 메서드가 사용됐다는 점만 빼면 Details.cshtml  파일의 코드와 유사합니다. 다음은 그 관련 코드입니다:

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

    참고로 이 Create.chstml  파일에는 크로스 사이트 요청 위조(Cross-Site Request Forgery) 공격을 방지하기 위해서 컨트롤러에 지정된 ValidateAntiForgeryToken 어트리뷰트와 대응하여 동작하는 @Html.AntiForgeryToken() 메서드 호출이 포함되어 있습니다.

    Create.cshtml  파일은 변경할 필요가 없습니다.

  2. 예제 응용 프로그램을 실행한 다음, Students 메뉴를 선택해서 페이지를 실행하고 Create New 링크를 클릭합니다.

  3. 임의의 학생 이름과 유효하지 않은 날짜를 입력하고 Create 버튼을 클릭해보면 오류 메시지가 나타날 것입니다.

    이 오류 메시지는 기본적으로 지원되는 서버 측 유효성 검사를 통해서 얻어진 결과입니다. 본 자습서의 이후 과정에서는 클라이언트 측 유효성 검사를 위한 코드를 자동으로 생성해주는 어트리뷰트를 추가하는 방법에 대해서도 살펴보게 될 것입니다. 다음 코드에 강조된 부분이 바로 Create 메서드에서 모델 유효성 검사를 수행하는 코드입니다:

    if (ModelState.IsValid)
    {
        db.Students.Add(student);
        db.SaveChanges();
        return RedirectToAction("Index");
    }
  4. 다시 날짜를 유효한 날짜로 변경한 다음, Create 버튼을 클릭해보면 새로운 학생의 정보가 Index 페이지에 나타날 것입니다.

Edit HttpPost 메서드 수정하기

이번에 살펴볼 메서드는 Controllers\StudentController.cs  파일의 HttpGet Edit 메서드로 (HttpPost 어트리뷰트가 지정되지 않은 메서드), 이 메서드는 Details 메서드에서 살펴봤던 것처럼 Find 메서드를 이용해서 선택된 Student 엔터티를 조회합니다. 이 메서드는 변경할 필요가 없습니다.

그러나 HttpPost Edit 액션 메서드는 다음의 코드로 대체해야 합니다:

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    var studentToUpdate = db.Students.Find(id);
    if (TryUpdateModel(studentToUpdate, "", new string[] { "LastName", "FirstMidName", "EnrollmentDate" }))
    {
        try
        {
            db.SaveChanges();

            return RedirectToAction("Index");
        }
        catch (DataException /* 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.");
        }
    }
    return View(studentToUpdate);
}

변경된 이 코드는 오버포스팅(Overposting) 공격을 방지하기 위해서 보안상 권장되는 코드를 구현한 것입니다. 스캐폴딩 기능이 만들어준 변경 전의 코드는 Bind 어트리뷰트가 적용되고 모델 바인더가 생성해준 엔터티를 Modified 플래그 설정과 함께 엔터티 집합에 추가하는 방식으로 구현되어 있습니다. 그러나 이런 형태의 코드는 더 이상 권장되지 않는데, Bind 어트리뷰트는 Include 매개변수 목록에 포함되어 있지 않은 모든 기존 데이터들을 제거해버리기 때문입니다. 추후 MVC의 컨트롤러 스캐폴딩 기능도 Edit 메서드에서는 Bind 어트리뷰트를 적용하지 않는 방식으로 개선될 것입니다.

이 코드에서는 먼저 기존 엔터티를 읽어온 다음, TryUpdateModel 메서드를 호출해서 전송된 폼 데이터에 포함된 사용자가 입력한 데이터로 엔터티의 필드들을 갱신합니다. 그러면 Entity Framework의 자동 변경 내용 추적 기능이 해당 엔터티에 Modified 플래그를 설정해줍니다. 그리고 SaveChanges 메서드가 호출되면 Entity Framework가 이 Modified 플래그를 기준으로 데이터베이스의 로우를 갱신하는 SQL 구문을 생성하게 됩니다. 이때, 동시성 충돌(Concurrency Conflicts)은 무시되고 사용자가 변경하지 않은 컬럼들을 포함한 데이터베이스 로우의 모든 컬럼들이 갱신됩니다. (본 자습서 시리즈의 후반부 파트에서는 동시성 충돌을 처리하는 방법을 살펴봅니다. 만약 데이터베이스에서 개별적인 각각의 필드들만 갱신하고자 한다면, 엔터티 자체는 Unchanged로 설정하고 해당 필드들만 Modified로 설정할 수도 있습니다.)

오버포스팅 방지를 위한 권장 방식에 따라, TryUpdateModel 메서드에는 Edit 페이지를 이용해서 수정이 가능한 필드들의 화이트 리스트를 매개변수로 전달합니다. 지금으로서는 따로 보호해야 할 추가적인 필드들이 없지만, 이처럼 모델 바인더가 바인딩하기를 바라는 필드들의 목록을 지정해놓으면, 이후에 데이터 모델에 필드들을 추가하더라도 명시적으로 이 목록에 필드들을 추가하기 전까지는 자동으로 해당 필드들이 보호됩니다.

또한 이렇게 코드를 변경할 경우, HttpPost Edit 메서드의 시그니처와 HttpGet Edit 메서드의 시그니처가 서로 동일해지므로 메서드의 이름도 EditPost로 변경했습니다.

그리고 Views\Student\Edit.cshtml  파일의 HTML과 Razor 코드는 Create.cshtml  파일에서 살펴본 내용과 비슷한며, 수정할 필요가 없습니다.

예제 응용 프로그램을 실행한 다음, Students 메뉴를 선택해서 페이지를 실행하고 특정 학생의 Edit 링크를 클릭합니다.

일부 데이터를 수정하고 Save 버튼을 클릭합니다. 그러면 Index 페이지에서 변경된 데이터를 확인하실 수 있습니다.

Delete 페이지 수정하기

지금까지 Details 메서드와 Edit 메서드에서 살펴본 것처럼, 템플릿을 통해서 자동으로 만들어진 Controllers\StudentController.cs  파일의 HttpGet Delete 메서드의 코드는 Find 메서드를 이용해서 선택된 Student 엔터티를 조회합니다. SaveChanges 메서드 호출이 실패할 경우 사용자 지정 오류 메시지를 출력하기 위한 몇 가지 기능을, 이 메서드와 그에 대응하는 뷰에 추가해보도록 하겠습니다.

이미 수정 작업과 생성 작업에서 살펴본 것처럼, 삭제 작업에서도 두 개의 액션 메서드가 필요합니다. 먼저 GET 요청에 대한 응답으로 호출되는 메서드는 사용자가 삭제 작업을 승인하거나 취소할 수 있는 기회를 제공해주는 뷰를 출력합니다. 그리고 이 뷰에서 사용자가 삭제를 승인하면 POST 요청이 만들어집니다. 그러면, HttpPost Delete 메서드가 호출되어 실제로 삭제 작업이 수행됩니다.

데이터베이스를 갱신할 때 발생할 수 있는 모든 유형의 오류들에 대응하기 위해서 HttpPost Delete 메서드에 try-catch 블럭을 추가합니다. 만약 오류가 발생하면, HttpPost Delete 메서드는 다시 HttpGet Delete 메서드를 호출하는데, 이때 오류가 발생했음을 나타내는 매개변수를 함께 전달합니다. 그러면 HttpGet Delete 메서드에서는 오류 메시지와 함께 삭제 확인 페이지를 다시 출력해서, 사용자가 작업을 취소하거나 다시 시도할 수 있도록 선택의 기회를 제공해줍니다.

  1. HttpGet Delete 액션 메서드를 오류 메시지 처리가 포함된 다음의 코드로 대체합니다:
    public ActionResult Delete(int? id, bool? saveChangesError=false)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
    
        if (saveChangesError.GetValueOrDefault())
        {
            ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
        }
    
        Student student = db.Students.Find(id);
        if (student == null)
        {
            return HttpNotFound();
        }
        return View(student);
    }

    이 코드는 삭제 작업에 실패해서 다시 메서드가 호출된 상태인지 여부를 나타내는 선택적 매개변수(Optional Parameter)를 전달받고 있습니다. 이 매개변수는 삭제 작업의 실패 없이 HttpGet Delete 메서드가 호출된 경우에는 false로 설정되고, 데이터베이스 갱신 실패로 인해서 HttpPost Delete 메서드에서 호출된 경우에만 true로 설정되어 뷰에 오류 메시지가 전달됩니다.

  2. HttpPost Delete 액션 메서드(DeleteConfirmed라는 이름을 갖고 있는 메서드)를 다음의 코드로 대체합니다. 이 메서드는 실제로 삭제 작업을 수행하고 데이터베이스 갱신 중 발생하는 모든 오류들을 잡습니다.

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Delete(int id)
    {
        try
        {
            Student student = db.Students.Find(id);
            db.Students.Remove(student);
            db.SaveChanges();
        }
        catch (DataException/* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            return RedirectToAction("Delete", new { id = id, saveChangesError = true });
        }
        return RedirectToAction("Index");
    }

    이제 변경된 코드에서는 선택된 엔터티를 조회한 다음, Remove 메서드를 호출해서 엔터티의 상태를 Deleted로 설정합니다. 그리고 SaveChanges 메서드가 호출되면 SQL DELETE 명령이 생성됩니다. 또한 액션 메서드의 이름도 DeleteConfirmed에서 Delete로 변경되었습니다. MVC의 스캐폴딩 기능은 HttpPost 메서드에 유일한 시그니처를 부여하기 위해서 HttpPost Delete 메서드의 이름을 DeleteConfirmed로 생성합니다. (CLR의 오버로드된 메서드들은 서로 다른 메서드 매개변수들을 가져야만 합니다.) 그러나 변경된 코드의 시그니처들은 서로 유일하므로, MVC의 규약을 준수하도록 HttpPostHttpGet 삭제 메서드들의 이름을 동일하게 통일합니다.

    만약 대용량 응용 프로그램에서 성능 개선에 대한 필요성이 높은 경우, Find 메서드와 Remove 메서드를 호출하는 코드 줄들을 다음 코드로 대체해서 해당 로우를 조회하기 위한 불필요한 SQL 쿼리가 생성되는 것을 피할 수도 있습니다:

    Student studentToDelete = new Student() { ID = id };
    db.Entry(studentToDelete).State = EntityState.Deleted;

    이 코드에서는 기본 키 값만 사용해서 새로운 Student 엔터티의 인스턴스를 생성한 다음, 엔터티의 상태를 Deleted로 설정합니다. Entity Framework가 엔터티를 삭제하기 위해서 필요한 작업은 이것이 전부입니다.

    이미 살펴봤던 것처럼 HttpGet Delete 메서드는 데이터를 삭제하지 않습니다. GET 요청에 대한 응답으로 삭제 작업을 (그리고 편집 작업, 생성 작업, 또는 다른 모든 유형의 데이터 변경 작업을) 수행할 경우, 보안상의 위험이 발생할 수 있습니다. 보다 자세한 정보는 Stephen Walther의 블로그, ASP.NET MVC Tip #46 - Don't use Delete Links because they create Security Holes 포스트를 참고하시기 바랍니다.

  3. 다음 예제처럼 Views\Student\Delete.cshtml  파일의 h2 제목과 h3 제목 사이에 오류 메시지를 추가합니다:

    <h2>Delete</h2>
    <p class="error">@ViewBag.ErrorMessage</p>
    <h3>Are you sure you want to delete this?</h3>

    예제 응용 프로그램을 실행한 다음, Students 메뉴를 선택해서 페이지를 실행하고 특정 학생의 Delete 링크를 클릭합니다:

  4. 그리고 Delete 버튼을 클릭합니다. 그러면 선택한 학생이 삭제된 Index 페이지가 나타날 것입니다. (실제 오류 처리 코드에 대한 예제는 Concurrency Tutorial 파트에서 다시 자세하게 살펴보게 될 것입니다.)

데이터베이스 연결 닫기

데이터베이스 연결을 닫고 사용 중인 리소스들을 최대한 빨리 해제하려면, 데이터베이스 컨텍스트를 이용한 작업이 끝나는 즉시 인스턴스를 삭제해야 합니다. StudentController.cs  파일에 스캐폴딩 기능으로 생성된 StudentController 클래스의 코드 마지막 부분에 다음과 같은 Dispose 메서드가 제공되는 이유가 바로 이 때문입니다:

protected override void Dispose(bool disposing)
{
    db.Dispose();
    base.Dispose(disposing);
}

이미 기본 Controller 클래스에서 IDisposable 인터페이스를 구현하고 있으므로, 이 코드에서는 간단하게 컨텍스트의 인스턴스를 명시적으로 삭제도록 Dispose(bool) 메서드를 재정의하고 있습니다.

트랜잭션 처리하기

기본적으로 Entity Framework는 암시적 트랜잭션을 구현하고 있습니다. 복수의 로우 및 테이블을 변경하는 시나리오에서 SaveChanges 메서드를 호출하면, 자동으로 Entity Framework가 변경 사항들이 모든 성공하거나 모두 실패하는 두 가지 상태 중 한 가지 상태가 되도록 보장해줍니다. 예를 들어서 일부 변경 사항들이 먼저 완료된 상태에서 오류가 발생하게 되면, 다시 모든 변경 사항들이 자동으로 롤백됩니다. 보다 세밀한 제어가 필요한 시나리오, 가령 Entity Framework 외부에서 수행되는 작업을 트랜잭션 내부 포함시키고 싶은 등의 경우에는 MSDN의 Working with Transactions 문서를 참고하시기 바랍니다.

요약

본문에서는 Student 엔터티를 대상으로 간단한 CRUD 작업을 수행하는 전체 페이지들의 모음을 구현해봤습니다. 그리고 MVC 헬퍼 메서드들을 이용해서 데이터 필드에 대한 UI 요소들을 생성했습니다. MVC 헬퍼 메서드들에 대한 더 자세한 정보는 Rendering a Form Using HTML Helpers 문서를 참고하시기 바랍니다(이 페이지는 MVC 3를 대상으로 작성된 문서지만 MVC 5에서도 여전히 유효합니다).

다음 자습서에서는 정렬과 페이징을 추가해서 Index 페이지의 기능을 확장시켜 보겠습니다.

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

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

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