파트 7: Entity Framework를 이용해서 관련 데이터 읽기

등록일시: 2016-05-09 08:00,  수정일시: 2016-05-08 07:10
조회수: 7,006
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
이번 파트에서는 Entity Framework가 탐색 속성에 데이터를 로드하는 방법인 지연 로드, 즉시 로드, 그리고 명시적 로드의 특징과 관련 예제 코드를 살펴봅니다.

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

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

본 자습서 시리즈의 이전 파트에서는 School 데이터 모델을 완성했습니다. 계속해서 본문에서는 Entity Framework가 탐색 속성에 로드하는 관련 데이터를 읽고 출력해봅니다.

다음 그림은 본문의 작업을 통해서 만들어보게 될 페이지들을 보여줍니다.

관계 데이터 지연 로드, 즉시 로드, 그리고 명시적 로드

Entity Framework가 특정 엔터티의 탐색 속성에 관계 데이터를 로드하는 방법에는 몇 가지가 있습니다:

  • 지연 로드(Lazy Loading). 엔터티가 처음 읽힐 때 관계 데이터는 검색되지 않습니다. 그러나 탐색 속성에 접근하려고 처음 시도하는 순간, 탐색 속성에 필요한 데이터가 자동으로 조회됩니다. 그 결과 엔터티 자체에 대해서 한 번, 그리고 엔터티의 관계 데이터를 조회해야할 때마다 각각 한 번씩, 이렇게 질의가 여러차례 데이터베이스로 전송됩니다. DbContext 클래스는 기본적으로 지연 로드를 사용합니다.

  • 즉시 로드(Eager Loading). 엔터티가 읽힐 때 관련 데이터가 함께 조회됩니다. 그리고 그 결과, 일반적으로 필요한 모든 데이터를 가져오는 단일 조인 질의가 만들어집니다. 즉시 로드는 Include 메서드를 사용해서 지정할 수 있습니다.

  • 명시적 로드(Explicit Loading). 이 방식은 지연 로드와 비슷합니다. 다만 탐색 속성에 접근할 때 자동으로 조회가 이루어지지는 않으므로 코드를 이용해서 명시적으로 관련 데이터를 조회해야 합니다. 먼저 엔터티에 대한 개체 상태 관리자 항목을 얻은 다음, 컬렉션을 대상으로 Collection.Load 메서드를 호출하거나 단일 엔터티를 담고 있는 속성을 대상으로 Reference.Load 메서드를 호출하는 방식으로 관련 데이터를 수작업으로 로드합니다. (예를 들어서 아래 예제 코드에서 Administrator 탐색 속성을 로드하고 싶다면, Collection(x => x.Courses) 부분을 Reference(x => x.Administrator)로 대체하면 됩니다.) 일반적으로 명시적 로드는 지연 로드를 비활성화시킨 경우에만 사용하게 됩니다.

지연 로드 및 명시적 로드, 두 방식은 모두 속성 값을 즉시 조회하지 않기 때문에 지연된 로드(Deferred Loading)라고도 합니다.

성능 고려사항

조회되는 모든 엔터티들의 관련 데이터가 필요한 상황이라는 점이 확실한 경우에는 대부분 즉시 로드가 최상의 성능을 제공해주는데, 그 이유는 데이터베이스에 단일 질의를 전송하는 편이 각각 조회된 엔터티마다 개별적으로 매번 질의를 전송하는 것보다 일반적으로 더 효율적이기 때문입니다. 가령 위의 예제에서 각 학과마다 열 개의 관련 강의를 갖고 있다고 가정해본다면, 즉시 로드 예제에서는 단일 (조인) 질의와 데이터베이스에 대한 단일 라운드 트립만 만들어집니다. 반면 지연 로드 및 명시적 로드 예제에서는 열한 개의 질의들과 열한 번의 데이터베이스 라운드 트립이 만들어집니다. 대기 시간이 긴 환경일수록 데이터베이스에 대한 추가적인 라운드 트립은 성능에 더욱 부정적인 결과를 가져옵니다.

그러나 다른 한편으로 일부 시나리오에서는 지연 로드가 더 효율적인 경우도 있습니다. 즉시 로드는 SQL Server가 효과적으로 처리할 수 없는 매우 복잡한 조인을 만들어내는 경우도 있습니다. 그리고 처리해야 할 전체 엔터티들의 집합 중 일부 부분 집합을 대상으로만 엔터티의 탐색 속성들에 접근해야 하는 경우라면 즉시 로드는 불필요한 데이터까지 조회하기 때문에 지연 로드가 처리에 더 적합합니다. 만약 성능이 중요한 상황이라면, 올바른 선택을 위해 두 가지 방식 모두에 대해 성능 테스트를 수행하는 것이 최선입니다.

때로는 지연 로드가 잠재적인 성능 문제를 야기하는 코드를 가려버리는 경우도 있습니다. 예를 들어서, 즉시 로드나 명시적 로드가 지정되지 않은 상태로 대량의 엔터티들을 처리하면서 매 반복마다 여러 탐색 속성들을 사용하는 코드는 (데이터베이스에 대한 대량의 라운드 트립으로 인해서) 대단히 비효율적인 결과를 가져올 수도 있습니다. 결과적으로 온-프레미스 SQL Server를 사용하는 개발 환경에서는 정상적으로 잘 동작하던 응용 프로그램이 Azure SQL Database로 옮겨지면 늘어난 대기 시간과 지연 로드로 인해 성능 상에 문제가 발생할 수도 있습니다. 이런 경우, 현실적인 테스트 부하를 이용하여 데이터베이스 질의 프로파일링을 수행하면 지연 로드가 적합한지 여부를 결정하는데 도움이 됩니다. 이에 관한 더 자세한 정보는 Demystifying Entity Framework Strategies: Loading Related Data 기사 및 Using the Entity Framework to Reduce Network Latency to SQL Azure 기사를 참고하시기 바랍니다.

직렬화 수행 전에 지연 로드 비활성시키기

만약 지연 로드가 활성화된 상태 그대로 직렬화를 수행한다면, 경우에 따라서는 처음 의도했던 것보다 심각하게 많은 데이터를 질의하게 되는 결과를 얻을 수도 있습니다. 일반적으로 직렬화는 특정 형식의 인스턴스가 갖고 있는 각각의 속성에 접근하는 방식으로 동작합니다. 이런 속성에 대한 접근은 지연 로드를 유발하게 되고, 그로 인해서 지연 로드된 엔터티들이 다시 직렬화됩니다. 그리고 직렬화 프로세스는 이렇게 지연 로드된 엔터티들의 각 속성들에 다시 접근하게 되어, 잠재적으로 지연 로드와 직렬화를 한층 더 유발하게 됩니다. 이런 폭주적인 연쇄 반응을 피하기 위해서는 엔터티를 직렬화하기 전에 지연 로드를 비활성화시켜야 합니다.

이후에 고급 시나리오 자습서 파트에서 다시 살펴보겠지만, 직렬화는 Entity Framework가 사용하는 프록시 클래스들 때문에 복잡해질 수도 있습니다.

직렬화 문제를 피하는 방법 중 한 가지는 Using Web API with Entity Framework 자습서에서 설명하고 있는 것처럼 엔터티 개체 대신 데이터 전송 개체(DTOs, Data Transfer Objects)를 직렬화 하는 것입니다.

데이터 전송 개체를 사용하지 않는 경우, 지연 로드를 비활성화하고 프록시 문제를 피하기 위해 프록시 생성도 비활성화시킬 수 있습니다.

다음은 지연 로드를 비활성화시킬 수 있는 몇 가지 방법입니다:

  • 엔터티의 속성을 선언할 때, 특정 탐색 속성의 선언에서 virtual 키워드를 생략합니다.
  • 다음과 같이 컨텍스트 클래스의 생성자에 LazyLoadingEnabled 속성을 false로 설정하는 코드를 추가해서, 모든 탐색 속성의 지연 로드를 비활성화시킵니다:
    this.Configuration.LazyLoadingEnabled = false;

학과명이 출력되는 강의(Courses) 페이지 만들기

지난 자습서에서 살펴봤던 것처럼 Course 엔터티에는 강의가 개설된 학과에 대한 Department 엔터티가 담겨 있는 탐색 속성이 존재합니다. 따라서 강의가 개설된 학과의 이름을 강의 목록에 출력하기 위해서는 이 Course.Department 탐색 속성에 담긴 Department 엔터티의 Name 속성을 가져와야 합니다.

먼저, 본 자습서 시리즈의 가장 첫 번째 파트에서 Student 컨트롤러를 생성하면서 Entity Framework를 사용하며 뷰가 포함된 MVC 5 컨트롤러(MVC 5 Controller with views, using Entity Framework) 스캐폴더를 이용했던 것과 동일한 요령으로, 다음 그림에서 볼 수 있는 것과 같은 옵션을 사용하여 CourseController라는 이름으로 Course 엔터티 형식에 대한 컨트롤러와 뷰들을 생성합니다. (여기서 컨트롤러의 이름이 CoursesController가 아니라는 점에 주의하십시오.)

그런 다음, Controllers\CourseController.cs  클래스 파일을 열고 Index 메서드를 찾아봅니다:

public ActionResult Index()
{
    var courses = db.Courses.Include(c => c.Department);
    return View(courses.ToList());
}

이 메서드의 코드를 살펴보면 자동 스캐폴딩이 Include 메서드를 사용해서 Department 탐색 속성에 즉시 로드를 지정했음을 확인할 수 있습니다.

계속해서 이번에는 Views\Course\Index.cshtml  파일을 열고 자동으로 생성된 템플릿 코드를 다음 코드로 대체합니다. 예제 코드에 변경된 부분들이 강조되어 있습니다:

@model IEnumerable<ContosoUniversity.Models.Course>

@{
    ViewBag.Title = "Courses";
}

<h2>Courses</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.CourseID)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Credits)
        </th>
        <th>
            Department
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.CourseID)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Title)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Credits)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Department.Name)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.CourseID }) |
            @Html.ActionLink("Details", "Details", new { id=item.CourseID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.CourseID })
        </td>
    </tr>
}

</table>

이 과정을 마치고 나면 스캐폴드로 생성된 코드에서 다음과 같은 부분들을 변경하게 됩니다:

  • 페이지 머리글이 Index에서 Courses로 변경됩니다.
  • CourseID 속성 값을 보여주는 Number 컬럼이 추가됩니다. 기본 키 값은 대부분 일반 사용자에게는 큰 의미가 없기 때문에, 기본적으로 기본 키는 스캐폴드 되지 않습니다. 그러나 본문의 예제에서는 기본 키 값도 중요하므로 목록에 출력합니다.
  • Department 컬럼이 우측으로 옮겨지고 컬럼 머리글도 변경됩니다. 스캐폴더가 컬럼 머리글에 출력할 속성으로 Department 엔터티의 Name 속성을 정확하게 선택하기는 하지만, 이 페이지에서는 Name보다는 Department가 컬럼 머리글로 더 어울립니다.

이 예제 코드에서 Department 컬럼의 경우, 스캐폴드 된 코드가 Department 탐색 속성에 로드된 Department 엔터티의 Name 속성을 출력하고 있는 부분을 유의해서 살펴보시기 바랍니다:

<td>
    @Html.DisplayFor(modelItem => item.Department.Name)
</td>

이제 페이지를 실행하고 (Contoso University 홈 페이지에서 Courses 메뉴를 선택합니다) 학과명이 출력되는 목록을 확인합니다.

강의 및 수강 정보를 보여주는 강사(Instructors) 페이지 만들기

계속해서 이번 절에서는 Instructor 엔터티에 대한 컨트롤러와 뷰를 생성해서 강사 페이지를 만들어 보겠습니다:

이번 페이지에서는 다음과 같은 방법들을 이용해서 관련 데이터를 읽고 출력합니다:

  • 강사 목록에는 OfficeAssignment 엔터티에서 가져온 관련 데이터가 출력됩니다. Instructor 엔터티와 OfficeAssignment 엔터티는 일대영 또는 일(One-to-Zero-or-One)의 관계를 갖고 있습니다. OfficeAssignment 엔터티에 대해서는 즉시 로드를 사용할 것입니다. 이미 설명했던 것처럼, 조회된 기본 테이블의 모든 로우들에 대한 관련 데이터가 필요한 경우에는 대부분 즉시 로드가 더 효율적입니다. 이번 경우에는 조회된 모든 강사들의 사무실 배정 정보를 출력하고자 합니다.
  • 강사들의 목록에서 사용자가 강사를 선택하면 관련된 Course 엔터티들이 출력됩니다. Instructor 엔터티와 Course 엔터티는 다대다(many-to-many) 관계를 갖고 있습니다. Course 엔터티들과 그에 관련된 Department 엔터티들에 대해서는 즉시 로드를 사용할 것입니다. 물론 이 경우에는 선택된 강사에 대한 강의 정보들만 필요하기 때문에 지연 로드가 더 효율적일 수도 있습니다. 그러나 이번 예제는 대상 엔터티 자체가 탐색 속성에 담겨져 있는 경우, 그 엔터티들의 탐색 속성들을 대상으로 즉시 로드를 지정하는 방법을 살펴보기 위한 목적을 갖고 있습니다.
  • 다시 사용자가 강의 목록에서 강의를 선택하면 Enrollments 엔터티 집합에서 가져온 관련 데이터들이 출력됩니다. Course 엔터티와 Enrollment 엔터티는 일대다(One-to-Many) 관계를 갖고 있습니다. Enrollment 엔터티들과 그에 관련된 Student 엔터티들에 대해서는 명시적 로드를 사용할 것입니다. (지연 로드가 활성화되어 있기 때문에 사실상 명시적 로드는 불필요하지만, 명시적 로드의 실제 사용 방법을 살펴보기 위한 것입니다.)

Instructor Index 뷰를 위한 뷰 모델 생성하기

강사 페이지에서는 세 가지 테이블들의 데이터를 보여주게 됩니다. 따라서, 그 중 한 가지씩 테이블의 데이터를 각각 담을, 세 개의 속성들이 존재하는 뷰 모델을 만들어 보겠습니다.

먼저 ViewModels 폴더에 InstructorIndexData.cs 클래스 파일을 만든 다음, 자동으로 생성된 코드를 다음 코드로 대체합니다:

using System.Collections.Generic;
using ContosoUniversity.Models;

namespace ContosoUniversity.ViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}

Instructor 컨트롤러 및 뷰 생성하기

다음 그림과 같은 옵션으로 EF 읽기/쓰기 동작이 함께 구현된 InstructorController 컨트롤러를 생성합니다. (이번에도 컨트롤러의 이름이 InstructorsController가 아니라는 점에 주의하십시오.)

우선 Controllers\InstructorController.cs  파일을 열고 ViewModels 네임스페이스에 대한 using 구문을 추가합니다:

using ContosoUniversity.ViewModels;

현재 Index 메서드에 자동으로 스캐폴드 된 코드에서는 OfficeAssignment 탐색 속성에 대해서만 즉시 로드를 지정하고 있습니다:

public ActionResult Index()
{
    var instructors = db.Instructors.Include(i => i.OfficeAssignment);
    return View(instructors.ToList());
}

Index 메서드의 코드를, 추가적인 관련 데이터들까지 로드해서 이를 뷰 모델에 담는 다음의 코드로 대체합니다:

public ActionResult Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses.Select(c => c.Department))
        .OrderBy(i => i.LastName);

    if (id != null)
    {
        ViewBag.InstructorID = id.Value;
        viewModel.Courses = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single().Courses;
    }

    if (courseID != null)
    {
        ViewBag.CourseID = courseID.Value;
        viewModel.Enrollments = viewModel.Courses.Where(
            x => x.CourseID == courseID).Single().Enrollments;
    }

    return View(viewModel);
}

이 메서드는 사용자가 선택한 강사와 강의의 ID 값들을 지정하는 선택적 라우트 데이터(id)와 질의 문자열 매개변수(courseID)를 전달받은 다음, 필요한 모든 데이터들을 조회하여 뷰에 전달합니다. 이 메서드로 전달되는 매개변수들은 사용자가 페이지에서 Select 하이퍼링크를 클릭함으로써 선택됩니다.

이 메서드의 코드는 먼저 방금 작성한 뷰 모델의 인스턴스를 생성한 다음 여기에 강사들의 목록을 조회하여 담는 작업으로 시작됩니다. 그리고 이때 Instructor.OfficeAssignment 탐색 속성과 Instructor.Courses 탐색 속성에 즉시 로드를 지정합니다.

var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
    .Include(i => i.OfficeAssignment)
    .Include(i => i.Courses.Select(c => c.Department))
    .OrderBy(i => i.LastName);

위의 구문에서 두 번째 Include 메서드는 관련된 Course 엔터티들을 로드하고, 로드된 각각의 Course 엔터티마다 Course.Department 탐색 속성에 대해 즉시 로드를 수행합니다.

.Include(i => i.Courses.Select(c => c.Department))

이미 설명했던 것처럼, 지금은 즉시 로드가 반드시 필요한 상황은 아니지만 성능 향상을 위해 즉시 로드를 사용하고 있습니다. 이 뷰는 언제나 OfficeAssignment 엔터티를 필요로 하므로, 한 번의 질의를 통해서 모든 데이터를 가져오는 것이 더 효율적입니다. 반면 Course 엔터티들은 웹 페이지에서 강사가 선택된 경우에만 필요하므로, 선택된 강의 정보가 페이지에 출력되는 경우가 출력되지 않는 경우보다 빈번한 상황에서만 지연 로드보다 즉시 로드가 더 적합합니다.

계속해서 만약 강사의 ID가 선택된 상황이라면, 뷰 모델에 담겨진 강사들의 목록으로부터 사용자가 선택한 강사가 조회됩니다. 그런 다음, 강사의 Courses 탐색 속성에서 가져온 Course 엔터티들이 뷰 모델의 Courses 속성에 로드됩니다.

if (id != null)
{
    ViewBag.InstructorID = id.Value;
    viewModel.Courses = viewModel.Instructors.Where(i => i.ID == id.Value).Single().Courses;
}

여기에 사용된 Where 메서드는 컬렉션을 반환하지만, 지금과 같은 경우에는 메서드에 전달된 조건으로 인해서 단일 Instructor 엔터티만 반환됩니다. 뒤이어 바로 호출되는 Single 메서드는 이 컬렉션을 단일 Instructor 엔터티로 변환해서 엔터티의 Courses 속성에 접근할 수 있게 만들어줍니다.

컬렉션에 오직 하나의 항목이 존재하는 경우에만 Single 메서드를 사용할 수 있습니다. 만약 전달된 컬렉션이 비어 있거나 하나 이상의 항목이 존재하면 Single 메서드가 예외를 던집니다. 또는 컬렉션이 비어 있으면 기본 값을 (이 경우에는 null을) 반환하는 SingleOrDefault 메서드를 대신 사용할 수도 있습니다. 그러나 이 메서드도 이번 예제에서는 여전히 예외가 발생할 수 있으며 (null 참조에서 Courses 속성을 찾으려고 시도할 경우), 그 경우 발생하는 예외 메시지도 문제의 원인을 명학하게 알려주지 못합니다. 그리고 Where 메서드를 따로 호출하는 대신, 다음과 같이 Single 메서드를 호출하면서 Where 조건을 함께 전달할 수도 있습니다:

.Single(i => i.ID == id.Value)

다음과 같이 호출하는 대신 말입니다:

.Where(I => i.ID == id.Value).Single()

마지막으로 강의가 선택되었다면 뷰 모델에 담겨진 강의들의 목록에서 선택된 강의가 조회됩니다. 그런 다음, 강의의 Enrollments 탐색 속성에서 가져온 Enrollment 엔터티들이 뷰 모델의 Enrollments 속성에 로드됩니다.

if (courseID != null)
{
    ViewBag.CourseID = courseID.Value;
    viewModel.Enrollments = viewModel.Courses.Where(
        x => x.CourseID == courseID).Single().Enrollments;
}

Instructor Index 뷰 수정하기

계속해서 이번에는 Views\Instructor\Index.cshtml  파일을 열고 자동으로 생성된 템플릿 코드를 다음 코드로 대체합니다. 예제 코드에 변경된 부분들이 강조되어 있습니다:

@model ContosoUniversity.ViewModels.InstructorIndexData

@{
    ViewBag.Title = "Instructors";
}

<h2>Instructors</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>Last Name</th>
        <th>First Name</th>
        <th>Hire Date</th>
        <th>Office</th>
        <th></th>
    </tr>

    @foreach (var item in Model.Instructors)
    {
        string selectedRow = "";
        if (item.ID == ViewBag.InstructorID)
        {
            selectedRow = "success";
        }
        <tr class="@selectedRow">
            <td>
                @Html.DisplayFor(modelItem => item.LastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FirstMidName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.HireDate)
            </td>
            <td>
                @if (item.OfficeAssignment != null)
                {
                    @item.OfficeAssignment.Location
                }
            </td>
            <td>
                @Html.ActionLink("Select", "Index", new { id = item.ID }) |
                @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>

이 과정을 마치고 나면 기존 코드에서 다음과 같은 부분들을 변경하게 됩니다:

  • 모델 클래스가 InstructorIndexData로 변경됩니다.
  • 페이지 제목이 Index에서 Instructors로 변경됩니다.
  • item.OfficeAssignment 탐색 속성이 null이 아닌 경우에만 item.OfficeAssignment.Location 속성을 출력하는 Office 컬럼이 추가됩니다. (일대영 또는 일(One-to-Zero-or-One) 관계이기 때문에, 관련된 OfficeAssignment 엔터티가 존재하지 않을 수도 있기 때문입니다.)
    <td> 
        @if (item.OfficeAssignment != null) 
        { 
            @item.OfficeAssignment.Location  
        } 
    </td> 
  • 선택된 강사의 tr 요소에 동적으로 class="success"를 추가하는 코드가 추가됩니다. 이 코드는 Bootstrap 클래스를 이용해서 선택된 로우의 배경 색상을 설정합니다.
    string selectedRow = "";
    if (item.InstructorID == ViewBag.InstructorID)
    {
        selectedRow = "success";
    }
    <tr class="@selectedRow" valign="top">
  • 각각의 로우마다 기존의 다른 링크들 바로 앞에 Select라는 이름의 새로운 ActionLink가 추가됩니다. 이 링크는 선택된 강사의 ID를 Index 메서드로 전송하는 역할을 수행합니다.

다시 응용 프로그램을 실행하고 Instructors 메뉴를 선택합니다. 그러면 관련된 OfficeAssignment 엔터티들의 Location 속성이 출력되거나, 관련된 OfficeAssignment 엔터티가 존재하지 않을 경우 빈 테이블 셀이 출력되는 것을 페이지에서 확인하실 수 있습니다.

다시 Views\Instructor\Index.cshtml  파일의 닫는 table 요소 뒤에 (즉 파일의 맨 끝에) 다음 코드를 추가합니다. 이 코드는 강사가 선택된 경우 그 강사와 관련된 강의들의 목록을 출력하는 코드입니다.

@if (Model.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == ViewBag.CourseID)
            {
                selectedRow = "success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}

이 코드는 뷰 모델의 Courses 속성을 읽어서 강의 목록을 출력합니다. 그리고 선택된 강의의 ID를 Index 액션 메서드로 전달하는 Select 하이퍼링크도 제공해줍니다.

응용 프로그램을 실행하고 Instructors 메뉴를 선택합니다. 그러면 이번에는 선택한 강사에게 배정된 강의들과 각 강의가 배정된 학과명이 출력되는 강의 목록 그리드를 확인할 수 있습니다.

방금 추가한 코드 블럭 바로 뒤에, 다음 코드를 추가합니다. 이 코드는 강의가 선택된 경우, 그 강의를 수강하고 있는 학생들의 목록을 출력합니다.

@if (Model.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}

이 코드는 뷰 모델의 Enrollments 속성을 읽어서 강의를 수강하고 있는 학생들의 목록을 출력합니다.

다시 응용 프로그램을 실행하고 Instructors 메뉴를 선택합니다. 그런 다음 강의를 선택해서 수강 중인 학생들과 그 학생들의 학점 목록을 살펴봅니다.

명시적 로드 추가하기

이제 다시 InstructorController.cs 파일을 열고 Index 메서드에서 선택된 강의의 수강 목록을 가져오는 부분을 살펴봅니다:

if (courseID != null)
{
    ViewBag.CourseID = courseID.Value;
    viewModel.Enrollments = viewModel.Courses.Where(
        x => x.CourseID == courseID).Single().Enrollments;
}

본문의 예제에서는 먼저 강사들의 목록을 조회하면서 Courses 탐색 속성과 각 강의들의 Department 속성에 대해 즉시 로드를 지정하고 있습니다. 그런 다음, Courses 컬렉션을 뷰 모델에 담고, 그 뒤에야 비로소 이 컬렉션 중 한 엔터티의 Enrollments 탐색 속성에 접근합니다. 이때 Course.Enrollments 탐색 속성에는 즉시 로드를 지정하지 않았기 때문에 이 속성의 데이터는 페이지에서 지연 로드를 통해서 나타나게 됩니다.

만약 이 코드를 다른 방식으로 변경하지 않은 상태에서 지연 로드를 비활성화시킨다면, 선택된 강의에 실제로 얼마나 많은 수강 정보가 존재하는지는 상관 없이 Enrollments 속성이 무조건 null을 반환하게 됩니다. 이런 경우, Enrollments 속성을 로드하기 위해서는 즉시 로드나 명시적 로드 중 한 가지 방식을 지정해야만 합니다. 즉시 로드를 지정하는 방법에 대해서는 앞에서 이미 살펴봤습니다. 따라서 이번에는 명시적 로드의 실제 사례를 살펴보기 위해서 Enrollments 속성을 명시적으로 로드하는 다음의 코드로 Index 메서드의 코드를 대체합니다. 예제 코드에 변경된 부분들이 강조되어 있습니다.

public ActionResult Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();

    viewModel.Instructors = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses.Select(c => c.Department))
        .OrderBy(i => i.LastName);

    if (id != null)
    {
        ViewBag.InstructorID = id.Value;
        viewModel.Courses = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single().Courses;
    }
    
    if (courseID != null)
    {
        ViewBag.CourseID = courseID.Value;
        // Lazy loading
        //viewModel.Enrollments = viewModel.Courses.Where(
        //    x => x.CourseID == courseID).Single().Enrollments;

        // Explicit loading
        var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
        db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
            db.Entry(enrollment).Reference(x => x.Student).Load();
        }

        viewModel.Enrollments = selectedCourse.Enrollments;
    }

    return View(viewModel);
}

변경된 코드는 먼저 선택된 Course 엔터티를 가져온 다음, 새로 추가된 코드를 통해서 해당 강의의 Enrollments 탐색 속성을 명시적으로 로드합니다:

db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();

그리고 다시 각 Enrollment 엔터티의 관련 Student 엔터티를 명시적으로 로드합니다:

db.Entry(enrollment).Reference(x => x.Student).Load();

이 코드에서 컬렉션 속성을 로드할 때는 Collection 메서드를 사용하고, 단일 엔터티를 담고 있는 속성을 로드할 때는 Reference 메서드를 사용한다는 점에 유의하시기 바랍니다.

이제 다시 응용 프로그램을 실행하고 Instructors 메뉴를 선택해보면, 데이터를 조회하는 방법이 변경되었음에도 불구하고 페이지에 출력되는 내용에는 별다른 부분이 없다는 점을 확인할 수 있습니다.

요약

본문에서는 지연 로드, 즉시 로드, 그리고 명시적 로드의 세 가지 방법을 모두 사용해서 탐색 속성에 관련 데이터를 로드해봤습니다. 다음 자습서에서는 관련 데이터를 갱신하는 방법을 살펴보도록 하겠습니다.

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

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

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