Entity Framework

C# 닷넷 엔티티 프레임워크(Entity Framework) 복잡한 관계 매핑과 쿼리 최적화

zorimo 2025. 1. 28. 23:13

 

지난 포스팅에서는 N:N 관계 매핑과 중간 테이블을 활용한 데이터 관리 방법을 학습했습니다.

 

이번 글에서는 다단계 관계상속 관계를 활용한 복잡한 관계 매핑과,

이를 효과적으로 관리하기 위한 최적화 전략을 살펴보겠습니다.

특히, 회사 조직 구조결제 시스템을 예제로 다루며 실습을 통해 이해를 도울 예정입니다.


1. 복잡한 관계 매핑이란?

복잡한 관계는 다음과 같은 특성을 가집니다

다단계 관계

- 데이터가 계층 구조를 가지며 상호 연관됩니다.

- 예: 직원(Employee) → 팀(Team) → 부서(Department).

 

상속 관계

- 공통 속성을 부모 클래스에서 정의하고, 각 자식 클래스에서 고유 속성을 추가합니다.

- 예: 결제시스템(Payment System) → 신용카드(CreditCard Payment), 계좌이체(BankTransfer Payment).


2. 다단계 관계: 회사 조직구조

2.1 시나리오

직원(Employee)팀(Team)에 소속됩니다.

팀(Team)은 하나의 부서(Department)에 속합니다.


2.2 모델 작성

Employee 엔티티

namespace EFCoreDemo.Model.Company

public class Employee
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public int TeamId { get; set; }

    public Team Team { get; set; } = default!;
}
 

Team 엔티티

namespace EFCoreDemo.Model.Company

public class Team
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public int DepartmentId { get; set; }

    public Department Department { get; set; } = default!;

    public List<Employee> Employees { get; set; } = [];
}
 

Deparment 엔티티

namespace EFCoreDemo.Model.Company

{
    public class Department
    {
        public int Id { get; set; }

        public string Name { get; set; } = string.Empty;

        public List<Team> Teams { get; set; } = [];
    }
}
 

2.3 관계 매핑 코드

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    (...)

    modelBuilder.Entity<Employee>()
                .HasOne(e => e.Team)
                .WithMany(t => t.Employees)
                .HasForeignKey(e => e.TeamId);

    modelBuilder.Entity<Team>()
                .HasOne(t => t.Department)
                .WithMany(d => d.Teams)
                .HasForeignKey(t => t.DepartmentId);
}
 

2.4 DbContext 업데이트

public class EFCoreDemoDbContext : DbContext {
     (...)

     public DbSet<Employee> Employees { get; set; } = default!;

     public DbSet<Team> Teams { get; set; } = default!;

     public DbSet<Department> Departments { get; set; } = default!

     (...)
}
 

2.5 마이그레이션 생성 및 적용

dotnet ef migrations add AddMultiLevelRelationship
dotnet ef database update

3. 상속 관계: 결제 시스템

3.1 시나리오

Payment는 공통 속성을 가진 부모 클래스입니다.

CreditCardPaymentBankTransferPayment는 각각 신용카드와 계좌이체 결제를 처리합니다.


3.2 모델 작성

Payment 엔티티

namespace EFCoreDemo.Model.Payment

{
    public abstract class Payment
    {
        public int Id { get; set; }

        public decimal Amount { get; set; }

        public DateTime PaymentDate { get; set; }
    }
}
 

CreditCardPayment 엔티티

namespace EFCoreDemo.Model.Payment

{
    public class CreditCardPayment : Payment
    {
        public string CreditCardNumber { get; set; } = string.Empty;

        public string CardHolderName { get; set; } = string.Empty;

        public string ExpiryDate { get; set; } = string.Empty;

        public string SecurityCode { get; set; } = string.Empty;
    }
}
 

 

BankTransferPayment 엔티티

namespace EFCoreDemo.Model.Payment

{
    public class BankTransferPayment : Payment
    {
        public string AccountNumber { get; set; } = string.Empty;

        public string AccountHolderName { get; set; } = string.Empty;

        public string BankName { get; set; } = string.Empty;

        public string IFSC { get; set; } = string.Empty;
    }
}
 

3.3 관계 매핑 코드

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    (...)

    modelBuilder.Entity<Payment>()
                .HasDiscriminator<string>("PaymentType")
                .HasValue<CreditCardPayment>("CreditCard")
                .HasValue<BankTransferPayment>("BankTransfer");
}
 
HasDiscriminator<T>
Discriminator는 TPH (Table-Per-Hierarchy) 전략에서 하위 클래스를 구분하기 위해 사용하는 컬럼입니다.
테이블에 추가된 Discriminator 컬럼은 각 행이 어떤 자식 클래스의 데이터인지 식별합니다.

HasDiscriminator<string>("PaymentType")
PaymentType이라는 컬럼이 Payment 테이블에 추가됩니다.
이 컬럼은 문자열 타입(string)이며, 데이터의 유형(예: CreditCard, BankTransfer)을 나타냅니다.

HasValue<T>(value)
각 하위 클래스와 매핑되는 값을 설정합니다.
예를 들어, CreditCardPayment는 PaymentType이 "CreditCard"인 행과 매핑됩니다.BankTransferPayment는 PaymentType이 "BankTransfer"인 행과 매핑됩니다.
 

 

3.4 DbContext 업데이트

public class EFCoreDemoDbContext : DbContext
{
    (...)

    public DbSet<Payment> Payments { get; set; } = default!;

    public DbSet<CreditCardPayment> CreditCardPayments { get; set; } = default!;

    public DbSet<BankTransferPayment> BankTransferPayments { get; set; } = default!;

    (...)
}
 

3.5 마이그레이션 및 적용

dotnet ef migrations add AddPaymentRelationship
dotnet ef database update
 

4. 데이터 추가 및 확인

4.1 회사 데이터

static void SeedDatabase_Company()
{
    using var context = new EFCoreDemoDbContext();

    if (context.Employees.Any())
    {
        context.Employees.RemoveRange(context.Employees);
        context.Teams.RemoveRange(context.Teams);
        context.Departments.RemoveRange(context.Departments);
        context.SaveChanges();
        Console.WriteLine("기존 회사 데이터가 삭제되었습니다.");
    }

    var itDepartment = new Department { Name = "IT" };
    var hrDepartment = new Department { Name = "HR" };
    var salesDepartment = new Department { Name = "Sales" };

    var devTeam = new Team { Name = "Development", Department = itDepartment };
    var qaTeam = new Team { Name = "Quality Assurance", Department = itDepartment };
    var recruitmentTeam = new Team { Name = "Recruitment", Department = hrDepartment };
    var salesTeam = new Team { Name = "Field Sales", Department = salesDepartment };

    var employees = new List<Employee>
    {
        new() { Name = "Kim", Team = devTeam },
        new() { Name = "Lee", Team = qaTeam },
        new() { Name = "Park", Team = recruitmentTeam },
        new() { Name = "Choi", Team = salesTeam }
    };

    context.Employees.AddRange(employees);
    context.SaveChanges();
    Console.WriteLine("새로운 회사 데이터가 성공적으로 추가되었습니다.");
}
 

4.2 데이터 확인

static void ReadAllEmployees()
{
    using var context = new EFCoreDemoDbContext();

    var employees = context.Employees
                            .Include(e => e.Team)            // Employee와 연결된 Team 로드
                            .ThenInclude(t => t.Department)  // Team과 연결된 Department 로드
                            .ToList();

    Console.WriteLine("회사 직원 목록");
    foreach (var employee in employees)
    {
        Console.WriteLine($"Employee: {employee.Name}, Team: {employee.Team.Name}, Department: {employee.Team.Department.Name} \n");
    }
}
 

4.3 결제 데이터

static void SeedDatabase_Payments()
{
    using var context = new EFCoreDemoDbContext();

    if (context.Set<Payment>().Any())
    {
        context.Set<Payment>().RemoveRange(context.Set<Payment>());
        context.SaveChanges();
        Console.WriteLine("기존 결제 데이터가 삭제되었습니다.");
    }

    var payments = new List<Payment>
    {
        new CreditCardPayment
        {
            Amount = 30000,
            PaymentDate = DateTime.Now,
            CreditCardNumber = "1234-5678-9012-3456",
            CardHolderName = "Kim",
            ExpiryDate = "12/25",
            SecurityCode = "123"
        },
        new CreditCardPayment
        {
            Amount = 500000,
            PaymentDate = DateTime.Now,
            CreditCardNumber = "9876-5432-1012-3456",
            CardHolderName = "Lee",
            ExpiryDate = "11/26",
            SecurityCode = "456"
        },
        new BankTransferPayment
        {
            Amount = 2000,
            PaymentDate = DateTime.Now,
            AccountNumber = "123456789",
            AccountHolderName = "Park",
            BankName = "Kakao Bank",
            IFSC = "BOFA123456"
        },
        new BankTransferPayment
        {
            Amount = 800,
            PaymentDate = DateTime.Now,
            AccountNumber = "987654321",
            AccountHolderName = "Choi",
            BankName = "Citibank Korea Inc.",
            IFSC = "CHASE987654"
        }
    };

    context.AddRange(payments);
    context.SaveChanges();
    Console.WriteLine("새로운 결제 데이터가 성공적으로 추가되었습니다.");
}
 

4.4 데이터 확인

static void ReadAllPayments()
{
    using var context = new EFCoreDemoDbContext();

    var payments = context.Set<Payment>().ToList();

    Console.WriteLine("결제 데이터 목록");
    foreach (var payment in payments)
    {
        Console.WriteLine($"Amount: {payment.Amount}, Date: {payment.PaymentDate}, Type: {payment.GetType().Name}");

        switch (payment)
        {
            case CreditCardPayment cc:
                Console.WriteLine($"  Card Number: {cc.CreditCardNumber}, Card Holder: {cc.CardHolderName}, Expiry: {cc.ExpiryDate} \n");
                break;

            case BankTransferPayment bt:
                Console.WriteLine($"  Bank: {bt.BankName}, Account: {bt.AccountNumber}, IFSC: {bt.IFSC} \n");
                break;
        }
    }
}
 

5. 마치며

이번 글에서는 다단계 관계와 상속 관계를 사용해 복잡한 관계 매핑을 구현했습니다.

이를 통해 데이터베이스의 계층 구조를 설계하고, TPH 전략을 사용한 상속 매핑을 학습했습니다.

 

다음 포스팅에서는 Lazy, Eager, Explicit Loading의 차이점을 알아보고, 성능 최적화를 위한 로딩 전략을 살펴보겠습니다.