Entity를 이용한 DB와 관련된 유용한기능을 살펴보고

ORM은 결국 느릴것이다란 오해를 타파해보겠습니다.

src : http://git.webnori.com/projects/OPPJ/repos/accountapi/browse/accountapi_test/AccountControlerTest.cs

낙관성동시성제어

DB에 특정 엔티디 동시 편집 방지하기 위해, 일반적으로 다음과 같이 트랜잭션에 Lock/UnLock 을 사용할수가 있습니다.

트랜잭션을 이용한 락기법

  • 로킹(Locking)기법: 트랜잭션들이 동일한 데이터 항목에 대해 임의적인 병행 접근을 하지 못하도록 제어하는 것

  • 트랜잭션 T가 데이터 항목 X에 대해 Read(X) or Write(X)연산을 수행하려면 반드시 lock(X) 연산을 해주어야 함

  • 트랜잭션 T가 실행한 lock(X)에 대해서는 해당 트랜잭션이 종료되기 전에 반드시 unlock(x)연산을 해주어야 함

  • 트랜잭션 T는 다른 트랜잭션에 의해 이미 lock이 걸려 있는 X에 대해 다시 lock(X)를 수행시키지 못한다.

  • 트랜잭션 T가 X에 lock을 걸지 않았다면, unlock(X)를 수행시키지 못한다.

참고: http://mangkyu.tistory.com/30

전통적인 멀티스레드에서 동시성처리를 위한 Lock/UnLock모델과 크게 다르지 않으며 ,단점으로는 DB저장소에서도 데드락이

발생하여 심각한 성능및 장애 문제로 이어질수 있다란 점입니다.


자그럼 .net core entity 에서는 동시성 접근 문제를 얼마나 단순화 시키고 성능업을 할수 있는지 살펴보겠습니다.

Entity설계

    [Table("test_blog")]
    public class Blog
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int BlogId { get; set; }

        public string Url { get; set; }

        [Timestamp]
        public byte[] Timestamp { get; set; }
    }

    [Table("test_person")]
    public class Person
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int PersonId { get; set; }

        // ENGTIP - LastName:성 FirstName:이름
        [ConcurrencyCheck]
        public string LastName { get; set; }

        public string FirstName { get; set; }
    }
  • Timestamp : Update가 이루어질때마다 자동증가합니다, 수정시 TimeStamp유효 체크를 하기때문에, 동시요청중 하나만 성공합니다.
  • ConcurrencyCheck : 특정필드를 대상으로 동시에 수정하는것을 방지합니다.


그럼 TestCode를 살펴보겠습니다.

TestCode

        [Fact]
        public void ConcurrencyTest()
        {
            ResetTestDB();
            Person person1 = new Person();
            person1.LastName = "PS";
            person1.FirstName = "MON";
            _testContext.Persons.Add(person1);
            _testContext.SaveChanges();  //Context를 Save하는것으로 실제 DB저장소에 객체가 저장이 됩니다.

            Person edit1 = _testContext2.Persons.FirstOrDefault(e => e.FirstName == "MON");
            Assert.Equal("PS", edit1.LastName);

            for(int i = 0; i < 10; i++)
            {
                String editName = "PS" + i;
                String editName2 = "XS" + i;
                person1.LastName = editName;  //다음 Save시 SQL의 Update문이 발생합니다. 
                edit1.LastName = editName2;
                _testContext.SaveChangesAsync();  //비동기로작동
                _testContext2.SaveChanges();  //동기적으로 작동
            }            
        }

동시 수행을 위해 트랜잭션에 관련된 DBContext 2개를 생성하여 이용하였습니다.

동시편집 방지는 LastName(성)에 지정을 하였으며...,  동시에 편집하는 시나리오 연출을위해

각각의 트랜잭션은 다른 LastName으로 저장시도를 할것입니다. Loop를 작동시켜 동기적/비동기적저장을

각각 수행하는것은 동시성 충돌문제를 일으키는 좋은방법중에 하나입니다.


위 코드는, 테스트시 Entity(Table)을 자동 셋팅해주며  데이터 추가 Update까지 포함되어 작동이 됩니다.

여기서 ORM의 장점을 알수가 있습니다. 위와 동일한 기능을 하는 동시성 테스트를 SQL문으로 작성하려고 한다면

아마 시도하지 않을것입니다. 하지만 동시성 충돌 문제를 임의로 일으킬수 있는방법이 있고  유연하게 대처하는

설계를 하는것은 중요한 내용일것입니다.


 동시성 편집 방지 확인

error

Message: Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException : Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.

두번째 동시 Update는 , 첫번째 Update가 끝날때까지 기다리는것이 Lock/UnLock모델의 기본 동시성 처리 방식입니다.

여기서 낙관성 동시제어는.., 두번째 동시 Update시도에 에러를 발생하게 하는것입니다.  Lock모델과 비교하여 장단점이 있지만

대규모 분산 동시성 처리에 사용되는 전략중 하나입니다.

데드락이 걸려 전체 DB성능 문제를 해결하는것보다, 어플리케이션에서 ConcurrencyException을 개별로 처리하는것이 훨씬 단순한

해법이 될것입니다.


그럼 이것이 어떻게 자동적으로 이루어지는것일까요? 그 메카니즘은 간단합니다.

Entity를 이용하면서 동시성처리에 제약 어노테이션을 걸었을뿐이지만, 다음과같은 SQL문이 자동적으로 작동이됩니다.


SQL문을 통해 살펴본 낙관성 동시제어

-Update 1
UPDATE person
     SET firstname = 'MON',
         lastname = 'XS1'
     WHERE personid=1 and lastname="PS"

-Update 2
UPDATE person
     SET firstname = 'MON',
         lastname = 'XS2'
     WHERE personid=1 and lastname="PS"

둘다 lastname을 변경하려고 시도하였고 Update1이 먼저 수행되어 이미 변경되었다고 가정해봅시다.

두번째 업데이트는..,  personid가 1이고 lastname이 'PS'인것을 찾을수 없기때문에 실패가 나게 됩니다.

즉 동시에 이루어지는 변경중 처음시도 되는 처리를 보장하는것이 낙관성 동시제어의 기본 골격이라고 할수가 있습니다.

물론 이러한 논리적인 기능사용을 위해 ORM이아니어도 됩니다.

위와같은 논리적인 SQL문처리가 자동적으로 이루어지고 불필요한 반복 SQL문 작성을 하지 않는것이 ORM의 컨셉이라고 할수 있습니다. 

Lazy Loading

전통적인 SQL-Mapper방식에서 다음을 생각해봅시다. 외래키로 설정된 테이블의 데이터를 참조하기 위해

join문을 사용하였고 , 비슷한 쿼리문에서 상황에따라 외부 데이터를 참조할때도 있고 아닐때도 있습니다.  하지만 우리의 로직은

이러한 분기를 위해 join문을 패턴별로 여러벌 만들기 어려울수도 있기때문에 공통 sql문 모듈로 항상 join을 선택하는 시나리오를 선택하게 됩니다. 

실제로 10가지 각각다른 조인이 발생하는 쿼리를 작성하고 이것을 맵핑처리하는 코드를 작성하려면 적어도 10가지 이상의 파일을 작성해야하는 것은 명백합니다.

그리고 Base가 되는 쿼리를 조금이라도 바꾸면 10가지 쿼리를 다시 모두 바꿔야하는 일이 발생합니다. 


ORM에서는 이러한 문제를 Lazy Loading에서 제공하는 기능을 사용하여 코드량을 비약적으로 줄일수가 있습니다.

물론 Lazy Loading은 기본적으로 사용되지 않을 불필요한 요소를 Load하지 않게다는게 기본 컨셉입니다. 

Entity 구조

  • TokenHistorie : 로그인 토큰 리스트
  • User : 사용자 정보
User myinfo= _context.TokenHistories.Include( p=>p.User)  // Join 작동될지 여부가 Include에 의해서 선택가능하다란것은 환상적인 아이디어입니다.
    .First(p => p.AuthToken.Equals(accessToken))
    .User;

토큰히스토리를 통해 해당 테이블내에서만 데이터가 필요할수도 있지만

토큰 히스토리가 외래키에의해 참조하는 사용자정보를 필요로 할수도 있습니다.

이 경운 join문이 필요하는 케이스가 되며, ORM에서는 단순하게 Include로 명시를 하게되면

Join문이 작동되어 사용자정보를 즉시 참조가능하게 됩니다. 이러한 것을 명시적 로드라고 불릅니다.

  • 즉시 로드는 관련 데이터가 초기 쿼리의 일부로 데이터베이스에서 로드됨을 의미합니다.
  • 명시적 로드는 관련 데이터가 나중에 데이터베이스에서 명시적으로 로드됨을 의미합니다.
  • 지연 로드는 탐색 속성에 액세스할 때 관련 데이터가 데이터베이스에서 투명하게 로드됨을 의미합니다.


다음은 블로그 컨텐츠를 조회하는 예이며, 게시물이 있을때에만  게시물사용자를 포함하라는

선택적 쿼리를 발생할수가 있습니다.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
        .ToList();
}


더많은 패턴 참고 : https://docs.microsoft.com/ko-kr/ef/core/querying/related-data


Json Refernce Loop Ignore

1 VS N의 Entity를 Json으로 표현하려했을시 다음에러가 발생

JSON.NET Error Self referencing loop detected for type


Entity Sample : 1 VS N

[Table("user")]
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }

public Profile Profile { get; set; }

public List<SocialInfo> SocialInfos { get; set; }

}

[Table("socaialinfo")]

public class SocialInfo
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int SocialId { get; set; }

[ForeignKey("UserForeignKey")]
public User User { get; set; }

public String SocialProviderName { get; set; }

public String SocialUserNo { get; set; }

public String NickName { get; set; }

}


위와같은 Entity가 있으며 , 사용자의 정보를 포함하여, 가입된 소셜을 모두 Json 보고자 한다고 가정해봅시다.

User user = _context.SocialInfos.Include( p=>p.User).First(p => p.UserID.Equals("1234"));  //ORM에서 Include문은 SQL문의 Join효과를 가집니다.
return user;

사용자는 소셜정보를 포함하고, 소셜정보역시 사용자를 포함하게 됩니다. 이것을 Tree구조인 Json으로 나타내고자한다면

참조간 Loop가 끊이지 않는 무한 참조가 발생하며,  JAVA ORM에서도 동일하게 발생하는 문제입니다.

  • 웹결과는 분리하여, Entity의 값이 채워지게 정의한다.
  • Entity를 그대로 Json으로 활용한다.

일반적으로 첫번째 방식을 사용하면 Json의 무한참조와 같은 에러가 발생하지가 않습니다. 정의 분리전략은 별개의 문제이며

Entity를 Json결과로 바로 사용하면  불필요한 정의없이 웹결과로 사용할수가 있습니다.   ( 보안필드는 별도 처리 )

하지만 이경우 1 VS N관계가 있는 엔티티에서 Json에서 참조를 끊지 못하는 상황이 발생하며, Json에서 이것을 그대로 사용하려면

상호참조를 무시라는 전역 옵션 사용이 가능합니다.  DB Entity의 관계도를 OOP로 표현하려하다보니 어쩔수없이 발생하게 되는 문제입니다.


다음과 같은 간략한 코드로, ORM Entity 상호참조에서 발생하는 Json문제를 해결할수가 있습니다.

public void ConfigureServices( IServiceCollection services )
{
    services.AddMvc()
        .AddJsonOptions(
            options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
    );
}









  • No labels