파트 11: Entity Framework를 이용한 데이터 모델 상속 구현하기

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

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

이전 파트에서는 동시성 예외를 처리하는 방법에 관해서 알아봤습니다. 이어서 본문에서는 데이터 모델에서 상속을 구현하는 방법을 살펴보겠습니다.

개체 지향 프로그래밍에서는 상속(Inheritance)이라는 개념을 통해서 간편하게 코드를 재사용할 수 있습니다. 본문에서는 LastName 속성같이 Instructor 클래스와 Student 클래스가 공통으로 사용하는 속성들을 갖는 Person 기본 클래스를 생성한 다음, 두 클래스가 이 새로운 클래스를 상속받도록 변경해보려고 합니다. 작업 중, 웹 페이지를 추가하거나 변경할 필요는 없지만, 일부 모델 클래스의 코드를 변경해야 하고, 그렇게 변경된 내용은 자동으로 데이터베이스에 반영될 것입니다.

데이터베이스 테이블과 상속간 매핑 방식 선택하기

현재 School 데이터 모델의 Instructor 클래스와 Student 클래스에는 몇 가지 같은 속성들이 존재합니다:

이렇게 Instructor 엔터티와 Student 엔터티가 공유하는 속성들의 중복 코드를 제거한다고 가정해보십시오. 또는 전달받은 이름이 강사의 이름인지, 학생의 이름인지 신경 쓰지 않고 서식을 설정하는 서비스를 구현하고 싶을 수도 있습니다. 그런 경우, 다음 그림처럼 두 엔터티에 의해 공유되는 속성들만 가진 Person 기본 클래스를 생성한 다음, Instructor 엔터티와 Student 엔터티가 이 기본 클래스를 상속받도록 구현할 수 있습니다:

그리고 데이터 모델의 이런 상속 구조를 데이터베이스에 표현할 때 선택할 수 있는 몇 가지 매핑 방식이 존재합니다. 먼저, 학생 및 강사에게 필요한 모든 정보를 갖고 있는 Person이라는 이름의 단일 테이블 하나만 생성하는 방식이 있습니다. 이 방식에서 어떤 컬럼은 강사에게만 적용되는 반면 (HireDate), 어떤 컬럼은 학생에게만 적용되고 (EnrollmentDate), 어떤 컬럼은 양쪽 모두에 적용됩니다 (LastName, FirstName). 그리고 일반적으로 각각의 로우에 저장된 정보가 두 형식 중 어떤 형식인지를 구분하기 위한 판별자(Discriminator) 컬럼을 테이블에 추가하는 경우가 많습니다. 가령, 강사는 판별자 컬럼에 "Instructor"를, 학생은 판별자 컬럼에 "Student"를 저장하는 식입니다.

이처럼 단일 데이터베이스 테이블로 엔터티 간의 상속 구조를 매핑하는 패턴을 계층당 하나의 테이블(TPH, Table-Per-Hierarchy) 상속이라고 합니다.

데이터베이스와 모델의 상속 구조를 보다 비슷한 형태로 만드는 다른 방식도 있습니다. 가령, 이름에 관한 필드들만 Person 테이블에 추가하고, Instructor 테이블과 Student 테이블을 각각의 날짜 필드와 더불어 분리하는 방식입니다.

이렇게 각 엔터티 클래스마다 데이터베이스 테이블을 생성하는 패턴을 형식당 하나의 테이블(TPT, Table Per Type) 상속이라고 합니다.

마지막으로 모든 비추상 형식을 각 테이블에 매핑하는 방식도 있습니다. 이 방식에서는 클래스에 존재하는 속성뿐만 아니라 상속된 속성들까지 대응 테이블의 컬럼에 모두 매핑합니다. 이런 패턴을 구체적 클래스당 테이블(TPC, Table-per-Concrete Class) 상속이라고 합니다. 만약 Person 클래스와 Student 클래스, 그리고 Instructor 클래스를 대상으로 앞에서 살펴본 구조와 동일한 TPC 상속을 구현한다면, Student 테이블과 Instructor 테이블은 상속을 구현하기 전과 후에 아무런 차이가 없습니다.

일반적으로 Entity Framework를 사용할 경우, TPC 상속 패턴 및 TPH 상속 패턴이 TPT 상속 패턴보다 더 좋은 성능을 제공해주는데, TPT 패턴의 경우 대부분 복잡한 조인 질의가 만들어질 수 있기 때문입니다.

본문에서는 이 중 TPH 상속을 구현하는 방법을 살펴보겠습니다. TPH 상속이 Entity Framework의 기본 상속 패턴이기 때문에, Person 클래스를 생성해서 Instructor 클래스와 Student 클래스가 Person 클래스를 상속 받도록 변경하고, 새로운 클래스를 DbContext 추가한 다음, 마이그레이션을 생성하기만 하면 됩니다. (다른 상속 패턴들을 구현 방법에 관한 더 자세한 정보는 MSDN의 Entity Framework 관련 문서 중, Mapping the Table-Per-Type (TPT) Inheritance 문서와 Mapping the Table-Per-Concrete Class (TPC) Inheritance 문서를 참고하시기 바랍니다.)

Person 클래스 생성하기

먼저 Models 폴더에 Person.cs 클래스를 생성하고 템플릿 코드를 다음 코드로 대체합니다:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public abstract class Person
    {
        public int ID { get; set; }

        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }

        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }
    }
}

Person 클래스를 상속받도록 Student 클래스와 Instructor 클래스 변경하기

그런 다음, Instructor.cs 파일에서 Instructor 클래스가 Person 클래스를 상속받도록 변경하고 키와 이름 관련 필드들을 제거합니다. 작업을 마치고 나면 다음 예제와 같은 코드가 만들어집니다:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public virtual ICollection<Course> Courses { get; set; }
        public virtual OfficeAssignment OfficeAssignment { get; set; }
    }
}

계속해서 Student.cs 파일도 비슷하게 처리합니다. 작업을 마치면 Student 클래스의 코드도 다음과 비슷해질 것입니다:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }

        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

모델에 Person 엔터티 형식 추가하기

마지막으로 SchoolContext.cs 파일을 열고 Person 엔터티 형식에 대한 DbSet 속성을 추가합니다:

public DbSet<Person> People { get; set; }

지금까지 수행한 작업이 Entity Framework를 이용해서 계층당 하나의 테이블 상속을 구성하는 데 필요한 작업의 전부입니다. 잠시 후 확인해보겠지만, 데이터베이스가 갱신되고 나면 Student 테이블과 Instructor 테이블 대신 Person 테이블이 만들어질 것입니다.

마이그레이션 파일 생성 및 수정하기

패키지 관리자 콘솔(Package Manager Console)에서 다음 명령을 입력합니다:

Add-Migration Inheritance

그런 다음, 다시 패키지 관리자 콘솔에서 Update-Database 명령을 실행합니다. 그러나 지금은 마이그레이션이 처리할 방법을 알 수 없는 기존 데이터들이 존재하기 때문에 명령이 실패합니다. 즉, 다음과 같은 오류 메시지가 발생할 것입니다:

Could not drop object 'dbo.Instructor' because it is referenced by a FOREIGN KEY constraint.

이 문제를 해결하려면 Migrations\<timestamp>_Inheritance.cs 파일을 열고 Up 메서드의 코드를 다음 코드로 대체해야 합니다:

public override void Up()
{
    // Drop foreign keys and indexes that point to tables we're going to drop.
    DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
    DropIndex("dbo.Enrollment", new[] { "StudentID" });

    RenameTable(name: "dbo.Instructor", newName: "Person");
    AddColumn("dbo.Person", "EnrollmentDate", c => c.DateTime());
    AddColumn("dbo.Person", "Discriminator", c => c.String(nullable: false, maxLength: 128, defaultValue: "Instructor"));
    AlterColumn("dbo.Person", "HireDate", c => c.DateTime());
    AddColumn("dbo.Person", "OldId", c => c.Int(nullable: true));

    // Copy existing Student data into new Person table.
    Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, ID AS OldId FROM dbo.Student");

    // Fix up existing relationships to match new PK's.
    Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");

    // Remove temporary key
    DropColumn("dbo.Person", "OldId");

    DropTable("dbo.Student");

    // Re-create foreign keys and indexes pointing to new table.
    AddForeignKey("dbo.Enrollment", "StudentID", "dbo.Person", "ID", cascadeDelete: true);
    CreateIndex("dbo.Enrollment", "StudentID");
}

이 코드는 다음과 같은 데이터베이스 갱신 작업을 수행합니다:

  • Student 테이블을 가리키는 외래 키 제약 조건과 인덱스를 제거합니다.
  • Instructor 테이블의 이름을 Person으로 변경하고 Student 테이블의 데이터를 수용하기 위한 다음과 같은 변경작업을 수행합니다:
    • 학생 데이터를 저장하기 위해 필요한 nullable EnrollmentDate 컬럼을 추가합니다.
    • 특정 로우가 학생 또는 강사 데이터를 담고 있는지를 구분하기 위한 판별자 컬럼을 추가합니다.
    • 학생 데이터 로우에는 채용 일자가 불필요하므로 HireDate 컬럼을 nullable로 변경합니다.
    • 학생 데이터를 가리키는 외래 키를 갱신하는 작업에 사용될 임시 필드를 추가합니다. 학생 데이터는 Person 테이블에 복사되고 나면 새로운 기본 키 값을 갖게 됩니다.
  • Student 테이블의 데이터를 Person 테이블에 복사합니다. 작업을 마치고 나면 학생 데이터에 새로운 기본 키 값들이 할당됩니다.
  • 학생 정보를 가리키는 외래 키 값들을 수정합니다.
  • 외래 키 제약 조건과 인덱스를 다시 생성해서, Person 테이블을 가리키도록 만듭니다.

(정수형 기본 키 대신 GUID 형식의 기본 키를 사용했다면, 학생 데이터의 기본 키 값을 변경할 필요가 없으므로 작업이 더 간단했을 것입니다.)

다시 update-database 명령을 실행합니다.

(운영 시스템에서는 이전 버전의 데이터베이스로 돌아갈 경우를 대비해서 이 작업과 대응하는 Down 메서드의 변경사항을 구현해 놓는 것이 바람직합니다. 본문에서는 Down 메서드는 다루지 않습니다.)

노트: 데이터를 마이그레이션하거나 스키마를 변경할 때, 여러 가지 다른 오류가 발생할 수도 있습니다. 만약 직접 해결하기 어려운 마이그레이션 오류가 발생한다면, Web.config 파일의 연결 문자열을 변경하거나 데이터베이스를 삭제하는 방식으로 본 자습서의 내용을 계속 진행할 수 있습니다. 가장 간단한 방법은 Web.config 파일에서 데이터베이스의 이름을 변경하는 것입니다. 가령, 다음 예제에서 볼 수 있는 것처럼 데이터베이스의 이름을 ContosoUniversity2로 변경합니다:

<add name="SchoolContext"
     connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=ContosoUniversity2;Integrated Security=SSPI;"
     providerName="System.Data.SqlClient" />

새 데이터베이스에는 마이그레이션 해야 할 데이터가 당연히 존재하지 않기 때문에, update-database 명령이 오류 없이 수행될 가능성이 훨씬 더 높습니다. 또는 데이터베이스를 삭제하는 구체적인 방법은 How to Drop a Database from Visual Studio 2012 포스트를 참고하시기 바랍니다. 그리고 자습서를 계속 따라 해보기 위해서 이 방법을 선택한 경우에는, 본문의 마지막 부분에서 설명하고 있는 배포 과정은 생략하거나, 또는 새로운 사이트와 데이터베이스를 생성하여 배포하시기 바랍니다. 기존에 배포했던 사이트에 다시 배포하고 업데이트를 실행하면, Entity Framework가 자동으로 마이그레이션을 실행할 때 다시 같은 오류가 발생하기 때문입니다. 마이그레이션 오류를 해결할 때 참고할 수 있는 가장 좋은 자료는 Entity Framework 포럼이나 StackOverflow.com 입니다.

테스트하기

응용 프로그램을 실행하고 여러 페이지들을 사용해보십시오. 모든 작업이 예전과 다름 없이 정상적으로 수행될 것입니다.

그리고 서버 탐색기(Server Explorer)에서 데이터 연결\SchoolContext(Data Connections\SchoolContext) 하위의 테이블(Tables) 노드를 확장해보면 Student 테이블과 Instructor 테이블이 Person 테이블로 대체된 것을 알 수 있습니다. 다시 Person 테이블 노드를 확장해보면 Student 테이블과 Instructor 테이블에서 사용되던 모든 컬럼들이 Person 테이블에 존재하는 것을 확인할 수 있습니다.

마우스 오른쪽 버튼으로 Person 테이블을 클릭한 다음, 테이블 데이터 표시(Show Table Data)를 선택해서 판별자 컬럼의 내용을 확인합니다.

다음 다이어그램은 새로운 School 데이터베이스의 구조를 보여주고 있습니다:

Azure에 배포하기

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

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


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


    그러면 Visual Studio가 응용 프로그램을 Azure에 배포하고, 배포가 완료되면 기본 브라우저에서 Azure에서 실행되는 응용 프로그램을 열어줍니다.
  3. 응용 프로그램이 정상적으로 동작하는지 테스트 해보십시오.

    데이터베이스에 접근하는 페이지를 최초로 실행할 때, Entity Framework가 현재 데이터 모델에 맞춰 데이터베이스를 최신 상태로 갱신하기 위해서 필요한 모든 마이그레이션의 Up 메서드들을 실행합니다.

요약

본문에서는 Person 클래스와 Student 클래스, 그리고 Instructor 클래스를 대상으로 계층당 하나의 테이블 상속을 구현해봤습니다. 이를 비롯한 다른 상속 구조들에 관한 더 많은 정보는 MSDN의 TPT Inheritance Pattern 문서와 TPH Inheritance Pattern 문서를 참고하시기 바랍니다. 다음 마지막 파트에서는 여러 가지 고급 Entity Framework 시나리오들에 대한 처리방법들을 살펴봅니다.

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

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