본문 바로가기
Entity Framework Core

.NET Core MVC] EF Core - 7. DB 동시성 제어(1) tracking property

by Fastlane 2023. 8. 29.
728x90
반응형

동시성 충돌(Concurrency conflicts)

동시성 충돌은 한 사용자가 수정하기 위해 entity를 조회하고, db에 수정사항을 저장하기 전에 다른 사용자가 동일한 entity를 수정하면 발생한다. 이러한 충돌을 감지하지 못하면, 마지막 저장하는 사람이 다른 사람의 수정사항을 덮어쓸 수 있다. 많은 applications에서 이러한 risk는 감당할만하다. 사용자가 적거나, 수정사항이 적거나, 몇몇 수정사항을 덮어써도 문제되지 않는 경우에는, 동시성 제어를 위한 프로그래밍의 비용이 이로인한 이익보다 더 클 수 있다. 이러한 경우에는, 동시성 제어를 위한 구성을 하지 않아도 된다. 

 

Pessimistic concurrency (locking)

어플리케이션이 동시성 시나리오에서 사고로 데이터가 손실되는 것을 막기 위해서는, database locks을 사용할 수 있다. 이를 pessimistic concurrency라고 부른다. 예를들어, database로부터 row를 읽기 전에, read-only 또는 update access를 위한 lock을 요청한다. update access를 위한 lock을 하면, 다른 사용자는 read-only 또는 update access를 위한 lock을 할 수 없다. read-only access를 위한 lock을 하면, 다른 사용자는 read-only를 위한 lock은 할 수 있지만, update를 위한 lock은 할 수 없다. 

 

locks을 관리하는 것은 단점이 있다. 프로그래밍하기 복잡하다. 상당한 db 관리 자원이 필요하며, 사용자가 늘어날 수록 품질문제가 생길 수 있다. 이러한 이유로, 모든 db 관리 시스템이 pessimistic concurrency를 지원하지 않는다. EF Core는 built-in 지원은 없지만, 이번 POST에서 어떻게 구현하는지 보여준다. 

 

Optimistic Concurrency

pessimistic concurrency의 대안으로 optimistic concurrency가 있다. optimistic concurrency는 동시성 충돌을 허용하고, 적절히 대응하는 것을 말한다.

 

예를들면, Jane이 Department 수정화면에 접속해서 English department의 Budget을 $350,000.00에서 $0.00으로 수정했다. Jane이 저장버튼을 클릭하기 전에, John이 똑같은 화면에서 Start Date를 2007-9-1을 2013-9-1로 수정했다. John이 아직 budget이 $350,000.00로 보이는 수정화면에서 저장버튼을 클릭하면, 동시성 충돌을 어떻게 처리할지에 따라 다음 일이 결정된다. 

 

  • 사용자가 수정한 property를 track하고 해당 컬럼만 database에서 수정한다. 위 예에는 2명의 사용자가 다른 properties를 수정했기 때문에 데이터가 손실되지 않는다. 누군가 English department를 조회하면, Jane과 John의 수정사항을 모두 볼 수 있다. 이 방법은 데이터 손실을 줄여준다. 하지만 같은 property에 대한 수정 데이터의 손실은 막을 수 없다. 웹앱에서 이 방법은 실용적이지 않다. entity의 원래 property value와 새로운 value를 track하기 위해 많은 양의 state를 관리해야 하기 떄문이다. 앱 퍼포먼스에 영향을 준다. 
  • John의 수정사항이 Jane의 수정사항을 덮어쓰도록 허용한다. 누군가 English department를 조회하면 2013-9-1과 $350,000.00값을 볼 수 있다. 동시성 제어를 위한 코딩을 하지 않으면, 이는 자동으로 생겨난다. 
  • John의 수정사항이 database update하는 것을 막는다. 일반적으로, data의 현재 상황을 알려주는 에러 메시지를 표시하고, John이 여전히 수정하길 원하면 재적용하는 것을 허용한다. 이를 Store Wins 시나리오라고 한다. 이번 POST에서는 Store Wins 시나리오를 구현할 것이다. 이 방법은 alert없이는 어떠한 변경사항도 덮어쓰기 처리 되지 않는다. 

동시성 충돌 감지

EF가 일으키는 DbConcurrencyException을 제어하므로 충돌을 처리할 수 있다. 언제 이런 exceptions이 발생되는지 알기 위해, EF는 충돌을 감지할 수 있어야 한다. 그러므로, db와 data model을 적절히 구성해야 한다. 충돌감지를 하기 위한 몇가지 옵션은 아래와 같다. 

  • database table에 언제 row가 변경되었는지 결정하는데 사용할 tracking column이 포함된다. SQL Update, Delete 명령어의 Where 조건문에 해당 column을 포함하도록 EF를 구성한다. tracking column의  data type은 rowversion이다. rowversion 값은 row가 업데이트 될때마다 증가될 sequential number이다. Update, Delete 명령어의 Where 조건문은 tracking column의 원래 값을 포함한다. row가 다른 사용자의 수정사항에 의해 update되면, rowversion 컬럼의 값은 원래 값과 다르다. 따라서 Update와 Delete 구문이 해당 row를 찾을 수 없게된다. EF가 Update, Delete 구문에 의해 영향받은 row가 0임을 확인하면, 동시성 충돌로 여긴다.
  • Update, Delete 명령어의 Where 조건문에 테이블의 모든 column의 원래 값을 포함하도록 EF를 구성한다. 첫번째 옵션처럼, row가 처음 조회된 이후 변경이 있으면, Where 조건문은 update를 하기위한 row를 반환하지 않고 EF는 동시성 충돌로 여긴다. database table이 많으 컬럼을 갖으면, 이 접근은 Where 조건문이 커지게 되므로 state 관리양도 커진다. 퍼포먼스에 악영향을 주므로, 이 방법은 추천되지 않으며, 이 POST에서 다루지 않는다. 이 방식을 구현하고 싶으면, entity의 모든 non-primary key properties에 ConcurrencyCheck attribute를 추가하면 된다. 이렇게 하면 EF가 Update, Delete 구문에 모든 컬럼을 추가한다. 

이번 POST에서는 Department entity에 rowversion tracking property를 추가하고, controller와 view를 생성해서 정상동작하는지 테스트해보자. 

 

tracking property 추가 

namespace WEB.Models;

public class Department
{
    public int DepartmentID { get; set; }
    
    [StringLength(50, MinimumLength = 3)]
    public string Name { get; set; }
    
    [DataType(DataType.Currency)]
    [Column(TypeName = "money")]
    public decimal Budget { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Start Date")]
    public DateTime StartDate { get; set; }

    public int? InstructorID { get; set; }
    
    [Timestamp]
    public byte[] RowVersion { get; set; }

    public Instructor Administrator { get; set; }
    public ICollection<Course> Courses { get; set; }
}

Timestamp attribute는 Update, Delete의 Where 조건절에 포함되는 것을 표시한다. rowversion의 .NET type은 byte array이다. 

 

fluent API를 사용하는 것을 선호하면, IsConcurrentyToken 함수를 사용할 수 있다. 

modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsConcurrencyToken();

property를 추가했으므로, migration을 해야한다. 

dotnet ef migrations add RowVersion
dotnet ef database update

컬럼이 추가된 것을 확인할 수 있다. 

 

728x90
반응형

댓글