[C#/EF6] 서로 다른 컨텍스트가 업데이트된 레코드를 읽어야 할 때

여러개의 컨텍스트 사용

필연

우선 이 글은 커넥션 풀링과는 아주 관계가 없진 않으나, 관계가 없다고 생각한 채 읽으셔도 됨을 알려드립니다.
엔티티 프레임워크를 사용해 개발하다 보면 어쩔 수 없이 동일한 컨텍스트 클래스의 인스턴스를 여러 곳에서 생성해 사용하게 됩니다.
처음에 저는 단순한 프로젝트이니 그냥 컨텍스트 클래스를 싱글톤 패턴으로 만들어, 여러 사용처에서 호출해 사용하면 되지 않을까? 라는 어리석은 생각을 했으나, 아무리 단순한 프로젝트라도 그곳에 이벤트가 있고, 병렬 처리가 있다면, 그리고 그 곳에서 DB에 접근해야 한다면(즉, 컨텍스트 인스턴스 호출을 해야 한다면), 어쩔 수 없습니다. 싱글톤은 답이 아닙니다.
문제는 이렇게 할 경우에 여러 동일한 컨텍스트 클래스의 인스턴스에서 동일한 Entity의 정보를 읽고 쓸 수 있다는 것입니다.
예를 들어 동일한 컨텍스트 클래스의 서로 다른 인스턴스를 호출해 사용하는 A, B 클래스가 있다고 가정해봅시다.
아래 가정은 시간 순서로 쓰여졌습니다.
  1. B 인스턴스에서 Road 엔티티를 DB에서 컨텍스트로 읽어드렸습니다. (DB => In memory, FindAsync 사용)
  2. B 인스턴스에서 Road 엔티티의 (ICollection)Cars.Count 프로퍼티를 읽어 변수 prevCarCount에 저장했습니다.
  3. B 인스턴스에서 유틸리티 클래스인 A 인스턴스의 AddCarOnRoad를 호출했습니다.
  4. A 인스턴스에서 Road 엔티티의 (ICollection)Cars 프로퍼티를 수정했습니다. (새 컨텍스트 인스턴스에서 작업)
  5. B 인스턴스에서 Road 엔티티의 (ICollection)Cars.Count 프로퍼티를 읽어 변수 curCarCount에 저장했습니다.
  6. B 인스턴스에서 prevCarCount와 curCarCount가 같으면 throw, 아니면 반환합니다.
위와 같은 상황에서, 어떤 한 로직을 빠뜨리면 3. 에서 CarRoad에 추가해 성공적으로 SaveChanges가 호출되더라도 6. 에서 성공적으로 반환하지 못하고 예외가 throw됩니다. 즉, DB에 변경된 사항이 반영되지 않는다는 얘기입니다.

DbContext의 인스턴스별 데이터 관리

그럼 도대체 왜 이런 일이 생길까요? 답은 당연합니다. AB에서 사용하는 DbContext가 서로 다른 인스턴스이기 때문입니다. 클래스는 같지만, 인스턴스는 다르죠. 쉽게 코드로 설명하면 다음과 같습니다.
public class A
{
    private SampleContext _context = new SampleContext(); // A 인스턴스에서 사용할 컨텍스트 인스턴스
}

public class B
{
    private SampleContext _context = new SampleContext() // B 인스턴스에서 사용할 컨텍스트 인스턴스
}
초기화가 A._contextB._context 두 곳 모두에서 각각 일어나기 때문에 두 컨텍스트의 인스턴스는 같을래야 같을 수가 없는 것이죠.
그럼 여기서 생각해봅시다. 과연 서로 다른 두 컨텍스트의 인스턴스가 엔티티 정보를 공유할까요? 물론 그렇게 만들 수도 있겠지만, 현실적으로 불가능한 이야기입니다. 엔티티 프레임워크 사용자가 얼마나 많은 엔티티를 사용하게 될지 어떻게 알고 해당 멤버들을 공유 가능하게 만들까요? 불가능하죠.

DbEntityEntry.State = EntityState.Detached 사용

바로 이 부분에서 필연적인 문제가 발생하는 것입니다.
다시 위 필연 섹션의 로직을 봐 보세요. 분명 A 인스턴스와 B 인스턴스에서 독자적으로 컨텍스트 인스턴스를 생성해 사용했으며, A 인스턴스의 컨텍스트에서 추가된 Car정보를 B 인스턴스의 컨텍스트가 알 방법이 없습니다. 물론 다시 말씀드리지만 그렇게 만들 수는 있습니다만 그렇게 하지 않는 것이 더 낫기 때문에 이와 같이 배포된 것입니다.
B 인스턴스에서 prevCarCount에 값을 할당할 때엔 B 인스턴스의 컨텍스트 인스턴스가 사용됐으며, 이미 이 시점에서 DB에 쿼리를 날려 레코드를 가져와 내부 DbSet 프로퍼티들에 저장이 끝난 상태입니다.
그리고 중간에 A 인스턴스에서 DB의 Cars 테이블에 레코드를 하나 추가하죠.
그러나 B 인스턴스의 컨텍스트 입장에서 보자면, 자신의 내부 프로퍼티에 변경사항은 전혀, 하나도 없습니다. A 인스턴스의 컨텍스트가 변경한 것은 A 인스턴스의 컨텍스트의 프로퍼티와 DB(외부)의 정보이지, B 인스턴스의 컨텍스트를 건드린 것이 아니기 때문입니다.
때문에, 여기서 우리는 B인스턴스의 컨텍스트에게 DB에 변경사항이 있으며 그 정보를 다시 읽어와야 한다고 알려줘야 합니다.
가장 간단한 방법으로는, 컨텍스트를 파괴하고 다시 생성하는 것입니다. 그러나 이는 이렇게도 할 수 있다일 뿐, 절대 좋은 예시가 아니니 실전에서 사용하지 마세요.
// class B
// ...
_context.Dispose();

_context = new SampleContext();
// ...
위 방법이 좋지 못한 방법인 이유는, 고작 한 개, 아니 몇 개가 됐던, 엔티티 변경을 감지하겠다고 커넥션 자체를 닫고 다시 여는 비효율적인 방법이기 때문입니다.
이미 있는 커넥션을 재활용 하는것과 커넥션 자체를 닫았다가 다시 여는 것. 둘 중 어느 것이 더 효율적인지는 누구라도 알 수 있습니다.
그럼 이제 재활용을 해 봅시다. B 인스턴스에서 A 인스턴스의 AddCarOnRoad를 호출한 다음 부분의 코드입니다.
// class B
// ...
// instanceA.AddCarOnRoad(...);
_context.Entry(road).State = EntityState.Detached; // ChangeTracker에서 road 엔트리를 분리함. 즉, 더이상 컨텍스트가 해당 엔트리를 추적하지 않음

road = _context.Roads.Find(id); // DB에서 신선한 Road 엔티티를 읽어옴
curCarCount = road.Cars.Count;
// ...
4번째 줄에서 컨텍스트와 기존 A 인스턴스가 받아온 Road 엔트리의 추적을 끊습니다. 이렇게 함으로써 6번째 줄에서 Find를 호출해도 더이상 기존 컨텍스트의 메모리에서 값을 반환하지 않고 DB에 요청을 날려 직접 값을 받아오게 됩니다.

댓글

이 블로그의 인기 게시물

C# 남아도는 메모리에도 불구하고 OutOfMemoryException이 발생한다면?

USB를 뒤는 괜찮은데 앞에 꽂으면 인식이 힘들다?

MySQL 데이터 타입과 Java 데이터 타입 비교/매칭