파트 9: Entity Framework와 비동기 코드 및 저장 프로시저

등록일시: 2016-06-10 08:00,  수정일시: 2016-09-02 09:15
조회수: 7,572
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
이번 파트에서는 비동기 코드의 구현 방법과 저장 프로시저의 사용법을 알아보고 지금까지 개발한 응용 프로그램을 다시 Azure에 배포해봅니다.

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

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

지금까지 본 자습서 시리즈에서는 동기 프로그래밍 모델을 사용해서 데이터를 읽고 갱신하는 방법들을 살펴봤습니다. 이번 파트에서는 비동기 프로그래밍 모델의 구현 방법을 알아봅니다. 비동기 코드를 사용하면 서버의 리소스를 보다 효율적으로 사용할 수 있기 때문에 결과적으로 응용 프로그램의 성능이 향상됩니다.

그리고 본문에서는 저장 프로시저를 이용해서 엔터티에 대한 입력, 갱신, 그리고 삭제 작업을 수행하는 방법도 살펴봅니다.

마지막으로 본 자습서 시리즈의 다섯 번째 파트에서 응용 프로그램을 Azure에 처음 배포한 이후로 데이터베이스에 적용한 모든 변경사항들이 반영된 응용 프로그램을 Azure에 다시 배포해보겠습니다.

다음 그림들은 본문을 통해서 작업하게 될 페이지들 중 일부를 보여줍니다.

비동기 코드를 굳이 고려하는 이유

웹 서버가 사용 가능한 스레드의 개수에는 제한이 존재하기 때문에, 부하가 많은 경우에는 이미 모든 스레드를 사용하고 있을 수도 있습니다. 이런 상황이 발생하면 스레드가 해제될 때까지 서버는 새로운 요청을 처리할 수 없습니다. 만약 동기 코드를 사용하고 있다면, 많은 스레드가 사실상 아무런 작업도 수행하지 않으면서, I/O 작업이 완료될 때까지 단지 대기만 하면서 묶여 있을 수도 있습니다. 그러나 비동기 코드를 사용하면, 특정 프로세스가 I/O 작업이 완료되기를 기다리는 동안, 서버가 다른 요청들을 처리할 수 있도록 해당 프로세스의 스레드가 해제됩니다. 결과적으로 비동기 코드를 사용하면 서버의 리소스를 보다 효율적으로 사용할 수 있으므로 서버가 더 많은 트래픽을 지연 없이 처리할 수 있게 됩니다.

기존 버전의 .NET에서는 비동기 코드를 작성하거나 테스트하는 작업이 매우 복잡했으며, 오류가 발생하기도 쉬웠고, 디버그 하기도 어려웠습니다. 그러나 .NET 4.5부터는 특별히 적용하지 말아야 할 이유가 없는한 비동기 코드를 작성하는 일이 보편적이 될만큼, 비동기 코드의 작성과 테스트, 그리고 디버깅이 대단히 쉬워졌습니다. 비동기 코드를 사용하면 어느 정도 오버헤드가 발생하기는 하지만, 트래픽이 적은 상황에서 감수해야 할 성능 저하는 무시할 수 있는 수준인 반면, 트래픽이 높은 상황에서의 잠재적인 성능 향상은 상당한 수준입니다.

비동기 프로그래밍에 대한 보다 자세한 정보는 Use .NET 4.5's async support to avoid blocking calls 문서를 참고하시기 바랍니다.

Department 컨트롤러 생성하기

지금까지 본 자습서 시리즈에서 컨트롤러를 생성해왔던 방법과 동일한 방법으로 Department 컨트롤러를 생성합니다. 다만 이번에는 비동기 컨트롤러 동작 사용(Use async controller actions) 체크 상자를 선택해야 합니다.

다음에 강조된 부분들은 Index 메서드를 비동기 코드로 만들어주는, 이 메서드의 동기 코드와 비교했을 때 추가된 부분들을 보여줍니다:

public async Task<ActionResult> Index()
{
    var departments = db.Departments.Include(d => d.Administrator);
    return View(await departments.ToListAsync());
}

이 예제 코드에는 Entity Framework 데이터베이스 질의를 비동기로 실행하기 위한 네 가지 변경사항들이 적용되어 있습니다:

  • 메서드에 async 키워드가 지정됐습니다. 이 키워드는 컴파일러에게 메서드 본문 중 일부분에 대한 콜백을 생성하고, 반환되는 Task<ActionResult> 개체를 자동으로 생성하도록 지시합니다.
  • 반환 형식이 ActionResult에서 Task<ActionResult>로 변경됐습니다. 여기서 Task<T> 형식은 T 형식의 결과에 대해서 진행중인 작업을 나타냅니다.
  • 웹 서비스 호출에 await 키워드가 지정됐습니다. 컴파일러는 이 키워드를 발견하면 내부적으로 메서드를 두 부분으로 분할합니다. 먼저 첫 번째 부분은 비동기적으로 시작되는 작업과 함께 끝이 납니다. 그리고 두 번째 부분은 작업이 완료됐을 때 호출되는 콜백 메서드에 담겨집니다.
  • ToList 확장 메서드의 비동기 버전인 ToListAsync 메서드가 호출됐습니다.

그런데 이 메서드에서 departments.ToList 구문은 비동기 메서드를 호출하도록 변경된 반면, departments = db.Departments 구문은 그대로인 이유는 무엇 때문일까요? 그것은 데이터베이스로 전송되는 쿼리나 명령이 만들어지는 구문만 비동기적으로 실행되기 때문입니다. 다시 말해서 departments = db.Departments 구문도 쿼리를 설정하기는 하지만, 그 쿼리는 ToList 메서드가 호출될 때까지는 실행되지 않습니다. 그래서 ToList 메서드만 비동기적으로 실행되도록 변경된 것입니다.

동일한 이유로 Details 메서드와 HttpGet Edit 메서드 및 Delete 메서드에서 사용되는 Find 메서드 역시 데이터베이스로 전송되는 쿼리를 만들어내므로 비동기적으로 실행되도록 변경됩니다:

public async Task<ActionResult> Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Department department = await db.Departments.FindAsync(id);
    if (department == null)
    {
        return HttpNotFound();
    }
    return View(department);
}

반면 Create 메서드, HttpPost Edit 메서드, 그리고 DeleteConfirmed 메서드의 경우에는 다름 아닌 SaveChanges 메서드 호출이 데이터베이스에서 실행되는 명령을 만들어내는 구문입니다. db.Departments.Add(department) 같은 구문들은 단지 메모리에 위치한 엔터티들만 변경할 뿐입니다.

public async Task<ActionResult> Create(Department department)
{
    if (ModelState.IsValid)
    {
        db.Departments.Add(department);
        await db.SaveChangesAsync();
        return RedirectToAction("Index");
    }

계속해서 이번에는 Views\Department\Index.cshtml  파일을 열고, 자동으로 생성된 템플릿 코드를 다음 코드로 대체합니다:

@model IEnumerable<ContosoUniversity.Models.Department>
@{
    ViewBag.Title = "Departments";
}

<h2>Departments</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Budget)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.StartDate)
        </th>
        <th>
            Administrator
        </th>
        <th></th>
    </tr>
@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Budget)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.StartDate)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Administrator.FullName)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.DepartmentID }) |
            @Html.ActionLink("Details", "Details", new { id=item.DepartmentID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.DepartmentID })
        </td>
    </tr>
}
</table>

이 코드는 페이지 머리글을 Index에서 Departments로 변경하고, 관리자 이름 컬럼을 우측으로 옮기고 관리자의 전체 이름을 제공합니다.

그리고 지난 파트에서 강의 관련 뷰들의 학과 이름 필드명을 "Department"로 변경했던 것과 동일한 방식으로, Create, Delete, Details, 그리고 Edit 뷰에서 InstructorID 필드의 이름을 "Administrator"로 변경합니다.

다시 말해서 Create 뷰와 Edit 뷰에는 다음 코드를 적용합니다:

<label class="control-label col-md-2" for="InstructorID">Administrator</label>

그리고 Delete 뷰와 Details 뷰에는 다음 코드를 적용합니다:

<dt>
    Administrator
</dt>

이제 응용 프로그램을 실행한 다음, Departments 메뉴를 선택하고 테스트를 해봅니다.

그러면 모든 기능들이 지금까지의 다른 컨트롤러들과 동일하게 동작하는 것을 확인할 수 있습니다. 그러나 이 컨트롤러의 모든 SQL 쿼리들은 비동기적으로 실행되고 있습니다.

비동기 프로그래밍에서 Entity Framework를 사용할 경우, 몇 가지 주의해야 할 점들이 있습니다:

  • 비동기 코드는 스레드로부터 안전하지 않습니다. 다시 말해서, 동일한 컨텍스트의 인스턴스를 이용해서 병렬로 다수의 작업을 실행하려고 시도하지 마십시오.
  • 만약 비동기 코드가 제공해주는 성능상의 이점을 제대로 활용하려면, 사용 중인 라이브러리 패키지들 중에서 데이터베이스로 전송되는 쿼리를 생성하는 Entity Framework의 메서드를 호출하는 모든 패키지들도 (페이징 등) 비동기 프로그래밍을 사용해야 합니다.

저장 프로시저를 이용해서 입력, 수정, 삭제하기

일부 개발자나 DBA들은 저장 프로시저를 이용해서 데이터베이스에 접근하는 방식을 더 선호합니다. 이전 버전의 Entity Framework에서는 직접 원본 SQL 쿼리를 실행하는 방식으로 저장 프로시저를 이용해서 데이터를 조회할 수는 있었지만, EF에서 저장 프로시저로 갱신 작업을 수행할 수는 없었습니다. 그러나 EF 6에서는 손쉽게 Code First가 저장 프로시저를 이용하도록 구성할 수 있습니다.

  1. 먼저 DAL\SchoolContext.cs 파일의 OnModelCreating 메서드에 다음에 강조된 코드를 추가합니다.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Entity<Course>()
            .HasMany(c => c.Instructors).WithMany(i => i.Courses)
            .Map(t => t.MapLeftKey("CourseID")
                .MapRightKey("InstructorID")
                .ToTable("CourseInstructor"));
        modelBuilder.Entity<Department>().MapToStoredProcedures();
    }

    이 코드는 Entity Framework에게 저장 프로시저를 이용해서 Department 엔터티에 대한 입력, 수정, 그리고 삭제 작업을 수행하도록 지시합니다.

  2. 패키지 관리자 콘솔에 다음 명령을 입력합니다:

    add-migration DepartmentSP

    그리고 Migrations\<timestamp>_DepartmentSP.cs  파일을 열어서 입력, 수정, 그리고 삭제 저장 프로시저를 생성하는 Up 메서드의 코드를 살펴봅니다:

    public override void Up()
    {
        CreateStoredProcedure(
            "dbo.Department_Insert",
            p => new
                {
                    Name = p.String(maxLength: 50),
                    Budget = p.Decimal(precision: 19, scale: 4, storeType: "money"),
                    StartDate = p.DateTime(),
                    InstructorID = p.Int(),
                },
            body:
                @"INSERT [dbo].[Department]([Name], [Budget], [StartDate], [InstructorID])
                  VALUES (@Name, @Budget, @StartDate, @InstructorID)
                  
                  DECLARE @DepartmentID int
                  SELECT @DepartmentID = [DepartmentID]
                  FROM [dbo].[Department]
                  WHERE @@ROWCOUNT > 0 AND [DepartmentID] = scope_identity()
                  
                  SELECT t0.[DepartmentID]
                  FROM [dbo].[Department] AS t0
                  WHERE @@ROWCOUNT > 0 AND t0.[DepartmentID] = @DepartmentID"
        );
        
        CreateStoredProcedure(
            "dbo.Department_Update",
            p => new
                {
                    DepartmentID = p.Int(),
                    Name = p.String(maxLength: 50),
                    Budget = p.Decimal(precision: 19, scale: 4, storeType: "money"),
                    StartDate = p.DateTime(),
                    InstructorID = p.Int(),
                },
            body:
                @"UPDATE [dbo].[Department]
                  SET [Name] = @Name, [Budget] = @Budget, [StartDate] = @StartDate, [InstructorID] = @InstructorID
                  WHERE ([DepartmentID] = @DepartmentID)"
        );
        
        CreateStoredProcedure(
            "dbo.Department_Delete",
            p => new
                {
                    DepartmentID = p.Int(),
                },
            body:
                @"DELETE [dbo].[Department]
                  WHERE ([DepartmentID] = @DepartmentID)"
        );
        
    }
  3. 다시 패키지 관리자 콘솔에 다음 명령을 입력합니다:

    update-database

  4. 그리고 디버그 모드로 응용 프로그램을 실행한 다음, Departments 메뉴를 선택하고 Create New 링크를 클릭합니다.

  5. 새로운 학과의 데이터를 입력하고 Create 버튼을 클릭합니다.

  6. 이때 Visual Studio에서 출력(Output) 창의 로그를 살펴보면, 저장 프로시저를 이용해서 새로운 Department 로우가 입력된 것을 확인할 수 있습니다.

지금 살펴본 것처럼, Code First는 기본 저장 프로시저의 이름을 자동으로 생성합니다. 그러나 기존 데이터베이스를 사용하는 경우, 이미 데이터베이스에 정의되어 있는 저장 프로시저를 사용하려면 저장 프로시저의 이름을 직접 지정해줘야 할 수도 있습니다. 그 방법에 관한 보다 자세한 정보는 Entity Framework Code First Insert/Update/Delete Stored Procedures 문서를 참고하시기 바랍니다.

만약 생성된 저장 프로시저가 수행하는 작업을 변경하고 싶다면, 마이그레이션 파일에서 저장 프로시저를 생성하는 Up 메서드에 스캐폴드 된 코드를 편집하면 됩니다. 이 방법을 사용하면 해당 마이그레이션이 실행되거나, 배포가 끝난 뒤 운영환경에서 자동으로 마이그레이션이 실행될 때 운영 데이터베이스에 변경사항이 반영될 것입니다.

마지막으로 이전의 마이그레이션을 통해서 생성된 기존 저장 프로시저를 변경하고 싶다면, 먼저 Add-Migration 명령을 이용해서 빈 마이그레이션을 생성한 다음, 직접 AlterStoredProcedure 메서드를 호출하는 코드를 작성하면 됩니다.

Azure에 배포하기

이번 절을 따라해보려면 본 자습서 시리즈 중 마이그레이션과 배포에 관한 파트에서 선택사항으로 설명했었던 Azure에 배포하기 절을 완료했어야만 합니다. 만약 여러분의 로컬 프로젝트에 데이터베이스를 삭제하는 방법으로 해결한 마이그레이션 오류가 존재한다면, 이번 절은 그냥 넘어갑니다.

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

  2. 게시(Publish) 버튼을 누릅니다.

    그러면 Visual Studio가 응용 프로그램을 Azure에 배포하고, 배포가 완료되면 기본 브라우저에서 Azure에서 실행되는 응용 프로그램이 열립니다.

  3. 응용 프로그램이 정상적으로 동작하는지 테스트 해보십시오.

    데이터베이스에 접근하는 페이지를 최초로 실행할 때, Entity Framework가 현재 데이터 모델에 맞춰 데이터베이스를 최신 상태로 갱신하기 위해서 필요한 모든 마이그레이션의 Up 메서드들을 실행합니다. 이제 본문에서 추가한 Department 페이지를 비롯해서 마지막으로 배포한 이후에 추가한 모든 웹 페이지들을 온라인 환경에서 사용할 수 있습니다.

요약

지금까지 비동기적으로 실행되는 코드를 작성함으로써 서버의 효율을 개선하는 방법과, 저장 프로시저를 이용해서 입력, 수정, 그리고 삭제 작업을 수행하는 방법을 살펴봤습니다. 다음 단계에서는 여러 명의 사용자들이 동일한 시점에 동일한 레코드를 수정하려고 시도할 때 발생할 가능성이 있는 데이터 유실을 방지할 수 있는 방법을 살펴보도록 하겠습니다.

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

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