티스토리 뷰

Entity Framework Core

ADO.NET과 ORM 비교

kaki104 2021. 1. 12. 09:44
반응형

ADO.NET과 ORM에 대한 비교가 필요한 분들을 위해서 아래 포스팅을 번역과 정리를 했습니다. 이 외에도 추가로 좋은 포스트가 보이면 올리도록 하겠습니다. 우선은 아래 내용들을 읽어 보시고 검토를 하시면 좋을 것 같습니다.

 

ADO.NET vs an ORM (Dapper & EF) | The Machine Spirit (bensampica.com)

 

ADO.NET vs an ORM (Dapper & EF) | The Machine Spirit

A case for an ORM compared to traditional ADO.NET and/or raw SQL scripts.

www.bensampica.com

Introduction

현재 매일 Entity Framework Core를 사용합니다. 이 주제에 대한 StackOverflow에 대한 질문을 보았고 기존의 ADO.NET 및 SQL 스크립트를 사용하여 ORM(객체 관계형 매퍼)이없이 작업 한 여러 프로젝트에 대해서 많은 대화를 나누며, ORM을 사용하는 것과 비교해 보았습니다. 이 게시물에서는 Entity Framework를 주로 다루었지만, Dapper도 포함한 차이점에 대해서 이야기를 하도록 하겠습니다.

The Theoretical

보안 고려사항(Security Considerations)

Microsoft가 이야기하는 EF에 대한 포괄적인 보안 고려 사항 목록이 있습니다. 다음은 Dapper 또는 EF를 안전하게 사용하기 위해 조직에서 고려해야 할 몇 가지 사항입니다.

ORM Usage

  • 원시 SQL 쿼리 문자열 작성을 방지하여 외부 사용자 입력으로부터 SQL Injection을 방지합니다. Dapper와 Entity Framework는 모두 매개 변수가있는 쿼리를 작성하고 저장 프로시저에 삭제 된 매개 변수를 전달하는 메서드를 제공하며 특별한 경우를 제외하고 선호되어야 합니다.
  • 매우 큰 결과 세트를 피하십시오. 5 백만 개의 결합된 레코드를 메모리로 선택하는 큰 결과 집합으로 인해 응용 프로그램 /시스템이 중단 될 수 있습니다. 애플리케이션에 필요한 것만 쿼리합니다.
  • (EF 전용)-서비스 계정에는 데이터베이스에 적용된 [db_datareader] [db_datawriter] [db_ddladmin] 권한이 필요합니다.

성능 고려사항(Performance Considerations)

성능이 뛰어나고 강력하며 확장 가능한 엔터프라이즈 애플리케이션을 구축하고 유지하는 것이 ORM의 핵심입니다. 다음은 100,000 개 이상의 레코드와 평균 <10ms의 SQL 응답 시간이있는 Entity Framework를 사용하는 관계형 데이터베이스 구현의 라이브 예제입니다. 생성되는 쿼리와 속도에 대한 로그를 확인하십시오.

구체화(Materializing)

구체화를 이해하는 것은 ORM 사용의 중요한 부분입니다. 가능한 한 적은 수의 개체를 구체화하기 위해 쿼리를 작성하는 것은 고성능 애플리케이션을 유지하는 데 중요합니다. EF 및 Dapper에는 쿼리를 메모리로 구체화하는 일반적인 C#식이 있습니다. 두 가지 모두 이를 수행하는 몇 가지 고유 한 방법과 명시적인 호출없이 자동 구체화되는 제약 조건이 있습니다.

Generated SQL (EF Only)

Entity Framework는 특히 Entities SQL (EF 6+ 및 EF Core)이라는 SQL 언어를 작성한 다음 대상 데이터 원본 SQL (MSSQL, MYSQL 등)로 변환됩니다. 따라서 응용 프로그램을 변경하지 않고도 응용 프로그램 계층을 모든 유형의 데이터베이스에 이식 할 수 있지만 잠재적인 성능 문제가있는 쿼리를 쉽게 처리 할 수 ​​있으므로 보다 복잡한 개체에 대한 쿼리를 만들 때 약간의 고려가 필요합니다.

The Practical

보안 및 성능 고려 사항을 염두에두고 ORM이 실제로 빛을 발하고 응용 프로그램 개발 수명주기가 개선 된 몇 가지 범주는 다음과 같습니다.

 

직원을 유지하는 응용 프로그램을 고려하십시오. 애플리케이션은 다음 속성을 사용하여 dbo.Employeedomain.EmployeeType이라는 테이블이있는 데이터베이스에 연결합니다.

dbo.Employee domain.EmployeeType
EmployeeId (int) [PK] EmployeeTypeId (int) [PK]
EmployeeTypeId (int) [FK] Value [nvarchar]
Name (nvarchar) Active (bit)
Active (bit)  

Speeding Up New Development

새로운 쿼리를 실행하고 새로운 데이터베이스 테이블과 관련 애플리케이션 모델을 지속적으로 추가하는 데 걸리는 시간을 줄이는 것은 ORM의 가장 큰 이점 중 하나입니다.

Comparison: Select all columns from all employees

ORM (Entity Framework)

public IEnumerable<Employee> GetEmployees()
{
    return _entityFrameworkContext.Employees;
}

ORM (Dapper)

public IEnumerable<Employee> GetEmployees()
{
    var employeeProperties = typeof(Employee).GetProperties().Select(prop => prop.Name);

    var sqlQuery = new StringBuilder("SELECT ")
        .AppendJoin(", ", employeeProperties)
        .Append($" FROM [dbo].[{nameof(Employee)}]")
        .ToString();

    using (var databaseConnection = new SqlConnection(_applicationOptions.ConnectionString))
    {
        return await databaseConnection.QueryAsync<Employee>(
            sqlQuery,
            commandType: CommandType.Text);
    }
}

ADO.NET

SQL Script

USE [Employees]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[GetEmployees]
SELECT * FROM [dbo].[Employee]
GO
USE [Employees]
GRANT EXECUTE ON OBJECT::[dbo].[GetEmployees]
TO rl_Employee_ServiceAccount;

Application Layer

public List<Employee> GetEmployees())
{
    using (var connection = new SqlConnection())
    {
        using (SqlCommand cmd = new SqlCommand("[dbo].[GetEmployees]", connection))
        {
            cmd.CommandType = CommandType.StoredProcedure;
            connection.Open();
            SqlDataReader reader = cmd.ExecuteReader();
            return HydrateData(reader);

        }
    }
}
public List<Employee> HydrateData(SqlDataReader reader)
{
    var data = new List<Employee>();
    using (reader)
    {
        if(reader.HasRows)
        {
              while (reader.Read())
              {
                   data.Add(
                       new Employee()
                       {
                            EmployeeId = reader.GetInt32(0),
                            EmployeeTypeId = reader.GetInt32(1),
                            Name = reader.GetString(2),
                            Active= reader.GetBoolean(3),
                       }
                   );
              }
              reader.Close();
          }
      }
      return data;
}

표시된 것처럼 이것은 ORM에서는 매우 간단한 것을 ADO.NET에는 매우 장황합니다. 가독성이 떨어지고 저장 프로시저에서 애플리케이션으로의 데이터 흐름을 따라갈 수 있는 능력이 약화됩니다. SQL 디버깅은 훨씬 더 많은 시간이 소요되고 실수하기 쉽습니다.

 

이러한 자세한 정도의 차이는 쿼리가 더 복잡해질수록 길어집니다. 몇 가지 예 :

  • ADO.NET : 필터링 된 쿼리와 함께 카운트가 필요한 쿼리는 기존 SQL에서는 번거롭고 중복성이 필요하지만 EF에서는 단순히 .Count ()입니다. EF의 .Where() 절을 통해 응용 프로그램 내에서 선택적 매개 변수를 사용하는 쿼리 작성도 마찬가지입니다.
  • EF :
public Tuple<IEnumerable<Employee>, int> GetEmployeeByTypeAndStatus(int employeeTypeId, bool? isActive)
{
    // Get all records OR select records that match the optional parameter if it's given.
    var allRecords = _entityFrameworkContext.Employees
        .Where(employee => isActive == null || employee.IsActive == isActive);
    // Get the total count of the base query.
    var totalRecordsCount = allRecords.Count();

    // Filter the data by the type and return the result.
    var filteredData = allRecords
        .Where(employee => employee.EmployeeTypeId == employeeTypeId);
    return new { filteredData , totalRecordsCount };
}
  • ADO.NET : 레코드를 추가하거나 업데이트하려면 추가 또는 삽입을 분리하는 논리가있는 하나의 저장 프로시저 (거의 전체 문이 두 번 복제 됨) 또는 별도로 유지 관리되어야하는 두 개의 별도 저장 프로시저가 필요합니다. 둘 다 여전히 열 불일치의 영향을받습니다.
  • EF : _entityFrameworkContext.Employees.AddOrUpdate(employee)

이러한 쿼리가 존재하고 직원이 추가 요청을 받았을 때 필요한 작업의 차이를 상상해보십시오 (실수 가능성도 있음).

Maintenance Reduction

이전 예제에서 현재 아키텍처 파일은 인덱스별로 바인딩되지 않은 배열을 읽고 있으며 새로운 변경 사항에 매우 취약합니다. 열 유형이 변경되면 어떻게됩니까? 저장 프로시저가 내부에 새 열을 추가하고 인덱스를 엉망으로 만들면 어떻게됩니까? 또한 dbo.Employee 테이블과 상호 작용하고 새 정보가 필요한 모든 단일 저장프로시 저도 업데이트해야합니다.

 

ORM을 사용하면 모델에 속성을 추가하는 것만큼이나 간단하며, 새 속성은 필요한 전체 응용 프로그램의 모든 단일 쿼리에서 즉시 사용할 수 있습니다.

 

주의! 여기서는 Database table에 컬럼을 추가해 놓고, 그 컬럼을 이용하기 위해 모델에도 Alias 프로퍼티를 추가해서 사용하는 것입니다. 만약, Code-first를 이용하고, 히스토리를 이용하고 있다면, 새로 추가된 컬럼을 database에 반영을 시켜준 후 사용해야합니다.
public class Employee
{
    public int EmployeeId { get; set; }
    public int EmployeeTypeId { get; set; }
    public string Name { get; set; }
    public string Alias { get; set; } // New property added.
    public bool Active { get; set; }
}

직원 정보를 필요로하는 유사한 쿼리가 많이 있다고 상상해보십시오. 애플리케이션이 커짐에 따라 발생하는 데이터베이스 열 변경은 Employee 모델에서 속성을 추가/업데이트하기만 하면됩니다. 모든 단일 쿼리는 새 정보를 포함하도록 자동으로 업데이트됩니다. 하지만, .Select()를 이용해 특정 상점 정보를 선택하는 쿼리는 제대로 변경되지 않은 상태로 유지됩니다.

 

또한, ORM의 강력한 형식 모델을 사용하므로 인해 데이터베이스와 응용 프로그램 간의 결합 된 관계에서 인덱스, 열 형식 불일치 또는 저장 프로시저 결과 집합 불일치로 인한 사고가 발생하지 않습니다. 수정되는 파일이 적고 오류 가능성이 적으므로 변경 영향이 줄어 듭니다.

EF Only

글로벌 쿼리 필터링은 실행중인 쿼리에 추가되는 'middleware'쿼리 역할을합니다. 엔티티의 소프트 삭제를 허용하는 위의 직원 및 직원 유형 테이블을 고려하십시오. 전역 쿼리 필터는 필요에 따라 재정의 할 수있는 모든 쿼리에 .Where (employee => employee.IsActive)를 추가 할 수 있습니다. 이 두 가지 모두 개발자의 실수를 방지하여 유지 관리를 줄이고 개발 속도를 높입니다.

 

소유 한 엔티티는 상위 개체에 연결될 때만 생성되도록 설정할 수 있습니다. 자체 테이블이 필요하지 않은 일반적인 항목 그룹 속성을 줄이는 데 자주 사용됩니다. 아래 모델을 고려하십시오.

public class Employee {
    public int PersonId { get; set; }
    public Name Name { get; set; }
}

public class ExternalNewsletterSubscriber {
    public int ExternalNewsletterSubscriberId { get; set; }
    public Name Name { get; set; }
    public string EmailAddress { get; set; }
}

[Owned]
public class Name {
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
}

Name 클래스는 EmployeeExternalNewsletterSubscriber 모두에서 재사용되며 Name 속성은 데이터베이스의 적절한 소유자 테이블에 매핑됩니다. 여기에서 Owned 대해 자세히 알아보십시오.

Integration Testing

EF는 복잡한 비즈니스 로직에 대한 통합 테스트를 가능한 한 쉽게 작성할 수 있는 in-memory 데이터베이스 기능을 제공합니다. 일반적으로 테스트를 위해 데이터베이스가 필요할 때 SSDT 프로젝트에서 직접 구축됩니다. EF의 in-memory 데이터베이스를 사용하면 테스트가 실행되는 동안 각 통합 테스트에서 자체 원자 데이터베이스를 가질 수 있습니다. 모의 작업이나 기존 데이터 없이도 기존 애플리케이션 코드와 원활하게 작업 할 수 있습니다.

public class TestsDbContextFixture
{
    public readonly DbContext DbContext { get; set; }

    public TestsDbContextFixture()
    {
        var options = new DbContextOptionsBuilder<DbContext>()
            // Use the in-memory database with a unique name so each test has their own "local" database.
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        DbContext = new DbContext (options);
    }
}

public class IntegrationTests
{
      [Fact]
      public void Test_DbContext()
      {
          // Initialize the in-memory database
          var context = new TestsDbContextFixture().DbContext;
          // ... tests involving the database
      }
}

Some Notes

보안과 성능을 염두에두면 Entity Framework와 Dapper는 모두 응용 프로그램 개발 수명주기를 개선 할 수있는 엄청난 기회를 제공합니다. 새로운 개발 속도를 높이고 유지 관리 및 버그 수정 시간을 줄이며 강력한 통합 테스트를 제공함으로써 ORM은 기존 및 신규 개발의 애플리케이션 라이프 사이클에서 많이 고려되어야합니다.

Additional Resources

반응형
댓글