ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Core Data Context 이해와 활용 - 심화
    Apple🍎/CoreData 2024. 1. 15. 16:54

    그냥 Store에 저장하지 뭐하러 Context에다가 임시로 저장을 하냐?

    데이터를 저장하거나 변경할 때 Store에 바로 하지 않고 Context에 먼저 임시로 해본 뒤에 save를 호출해야 해당 내용들이 Store에 반영된다.
    뭐하러 Context를 Scratch Pad로 사용을 할까? 그냥 바로 Store에다가 꽂아버리면 안되는 이유가 뭘까?

    • Core Data는 그냥 데이터만 저장하는 게 아니다. 앱의 로직과 맞물려서 돌아가다가 의도한 로직에 맞지 않으면 해당 내용은 Store에 반영하면 안된다. 따라서 앱이 돌아가면서 데이터의 변경이 일어날 때 이것들을 Context에 임시로 저장해 놓았다가 모든 로직에 오류가 없는 것을 확인한 이후에서야 Store에 반영한다.

     

    Multiple Main Contexts

    • 하나의 Main Context는 기본으로 있고 필요에 따라 Context를 추가로 만들어서 같은 Store에 연결해서 사용할 수 있다.
    • 예를 들어 2개이상의 Context에서 같은 엔티티의 값에 대해 동시에 변경을 하려하면 데이터 충돌이 일어날 수 있다.

    • Core Data는 위와 같은 상황에 대해 디폴트로 Optimistic Locking을 채택한다.

    Context 동작 방식 Remind

    • Context는 SandBox(일정 지역에서 일어난 변화는 해당 지역에만 적용된다.)의 형태로 Store로부터 고립되어 있다.
    • 다시말해서 save를 호출하기 전까지는 Context 내부의 변화는 Context 내부에만 한정된다.

     

    Optimistic Locking

    • 하나의 문서 파일을 두개의 워드 창에 띄워 놓고 편집을 하는 상황을 가정해보자.
    • 이런 상황이면 하나의 엔티티를 여러 곳에서 접근하여 변경할 수 있어 데이터 충돌이 일어날 수 있다. Core Data에서는 이를 어떻게 해결 할까?

    • 각 Context는 Persistent Store로부터 엔티티를 fetch해올때 스냅샷을 떠놓는다.

    • 유저가 엔티티를 수정하고 이를 저장하기 위해 save를 호출하면
    • Context는 변경된 엔티티를 persistent store에 저장하기 전에 먼저
    • 한번더 Persistent Store로부터 해당 엔티티를 조회해서 가져와
    • 이전에 스냅샷과 비교한다.
    • 만약 조회한 엔티티가 이전 스냅샷과 동일한 경우, 엔티티의 변경이 일어나지 않았음을 의미하므로 정상적으로 변경한 엔티티를 저장한다.

     

    • 만약에 변경된 엔티티를 저장하기 전 persistent store로부터 한번더 가져온 엔티티가 이전에 떠 놓은 스냅샷과 다른 경우
    • 해당 엔티티를 편집하는 동안 다른 곳에서 이미 해당 엔티티를 변경 시킨 것이다.
    • 따라서 이 경우 데이터간의 충돌이 일어날 수 있기 때문에 save를 취소한다.

     

     

    Context 내부에서의 엔티티 변화를 persistent store에 반영하는 과정을 Merge(병합)이라고 하며 위와 같이 하나의 엔티티에 대해 여러 Context에서 변경을 시도하는 경우 Merge Conflict Error가 발생한다.

    Merge Conflict Errors

    // 기본 main context 가져오기
    let context = delegate.persistentContainer.viewContext
    
    // main context 하나 더 만들기
    let context2 = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    // 기본 main Context하고 같은 persistent store에 연결하기 
    context2.persistentStoreCoordinator = delegate.persistentContainer.persistentStoreCoordinator
    
    // NSFetchRequest에 Predicate 설정하기
    let predicate = NSPredicate(format: "name = %@", "Speak to me")
    let fetchRequest = NSFetchRequest<Song>(entityName: "Song")
    fetchRequest.predicate = predicate
    
    
       do {
        // 각 context에서 똑같은 엔티티 fetch해서 가져오기 
                let songFromContext1 = try context.fetch(fetchRequest).first!
                let songFromContext2 = try context2.fetch(fetchRequest).first!
    
        // 각 context에서 fetch해온 엔티티의 값 변경하고 
                songFromContext1.duration = 60.0
                songFromContext2.duration = 40.0
    
        // 각 context에서 save 호출해서 변경된 값 persistent store에 반영 시도 
                try context.save()
                try context2.save()
            } catch let error {
                NSLog((error as NSError).userInfo.debugDescription)
            }
    

    위와 같이 하나의 엔티티에 대해 서로 다른 context가 동시에 값의 변경을 시도하게 되면 다음과 같이 Merge Conflict Error가 발생한다.

    ["NSExceptionOmitCallstacks": 1, "conflictList": <__NSArrayM 0x600002850db0>(
    NSMergeConflict (0x600003356800) for NSManagedObject (0x60000057c640) with objectID '0xfa2faad45a7af888 <x-coredata://E8CEB6C1-4881-497F-92D9-4CC2F1AE64B8/Song/p1>' with oldVersion = 10 and newVersion = 11 and old object snapshot = {
        album = "<null>";
        composer = "<null>";
        duration = 40;
        imageURL = "<null>";
        lyrics = "<null>";
        name = "Speak to me";
    } and new cached row = {
        album = "<null>";
        composer = "<null>";
        duration = 60;
        imageURL = "<null>";
        lyrics = "<null>";
        name = "Speak to me";
    }
    )
    ]

    userInfo를 통해서 엔티티의 스냅샷과 현재 디스크에 저장되어 있는 엔티티가 서로 일치하지 않음을 알 수 있다.

    Merge Conflict Policy

    • 낙관적 락의 경우, 데이터간의 충돌이 발생했을 때 개발자에게 충돌을 보고하고 어떤 버전의 엔티티를 저장할지 선택하게 한다.
    • 충돌 때마다 버전을 수동으로 선택하기 보다 어떤 엔티티를 저장할 것인지 미리 지정해놓을 수 있다.
    • NSManagedContextmergePolicy를 이용해 충돌시 선택할 버전을 지정해 놓을 수 있다.

    NSErrorMergePolicy

    • NSErrorMergePolicy는 충돌 해결을 위한 정책이다.
    • 스냅샷과 persistent store의 버전 사이에 충돌이 발생시 디폴트는 error를 던지는 것이다.
    • mergeByPropertyStoreTrump : 현재 저장하려는 변경된 엔티티를 우선시한다.
      • persistent store에 있는 엔티티의 값과 다른 경우, 새로 저장하는 엔티티의 값으로 덮어쓰기 한다.
      • 완전한 덮어쓰기가 아니라 충돌하는 값에 대해서만 덮어쓰기
    context.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
    • mergeByPropertyObjectTrump : persistent store에 저장되어 있는 버전을 우선시한다.
      • 위의 경우와 반대로 persistent store에 저장된 값을 그대로 유지하는 것을 선택한다.
    context.mergePolicy = NSMergePolicy. mergeByPropertyObjectTrump
    • overwrite : 현재 저장하려는 변경된 엔티티로 덮어쓰기
      • persistent store에 있는 엔티티 위에다가 아예 새로운 엔티티로 덮어써버린다.
    context.mergePolicy = NSMergePolicy.overwrite
    • rollback : 현재 저장하려는 변경사항 모두 취소하기
      • context에서 이루어진 모든 변경사항 버리기
    context.mergePolicy = NSMergePolicy.rollback

    Nested Managed Object Contexts

    Child Contexts

    • 일반적으로 context에서 일어난 변경사항을 반영하기 위해 save를 호출하면 context 내 변경사항들은 persistent store로 push 된다.
    • Child Contexts에서 save를 호출하면 Child Contexts에서 일어난 변경사항들은 각자의 Parent context로 push 된다.
    • Child Context는 Persistent Store와 직접 연결되어 있지 않으며 모든 변경사항은 Parent context를 통해야한다.

    Child Contexts 만들기

    • Child Context는 일반 Context와 다르지 않다. -> Context를 생성하는 방법은 동일하다.
    • Child Context도 또다른 Contexts들의 Parent가 될 수도 있다.
    // 기본 main context를 parent 로 받고
    let parent = delegate.persistentContainer.viewContext
    // 새로운 context를 만든다음 
    let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    // persistentStoreCoordinator에 연결하는 대신 
    // parent Context 지정해주기
    childContext.parent = parent

    Parent와 Child Contexts 싱크 맞추기

    • Parent와 Child Contexts들은 양방향으로 변화를 반영할 수 있다.

    • Child Context에서 save : Child Context에서 일어난 변화를 Parent Context에 반영
    • NSManagedContextautomaticallyMergesChangesFromParent 속성을 가지고 있어 해당 속성을 true로 설정할 경우 Parent Context에서 일어난 변화가 Child Context들에게 자동으로 반영된다.
    // 기본 Main Context를 parent로 지정
    let parentContext = delegate.persistentContainer.viewContext
    
    // 새로운 Context 2개 만든 다음, automaticallyMergesChangesFromParent 속성 true
    let childContext1 = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    childContext1.parent = parentContext
            childContext1.automaticallyMergesChangesFromParent = true
    let childContext2 = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
            childContext2.parent = parentContext
            childContext2.automaticallyMergesChangesFromParent = true
    
    // Parent Context에 새로운 엔티티 생성해서 넣은 뒤에 엔티티 값 변경
    let songInMainContext = NSEntityDescription.insertNewObject(forEntityName: "Song", into: parentContext) as! Song
    songInMainContext.name = "Breathe"
    
    // Parent Context의 변경 내용 자동으로 Child Contexts들에 반영되어 
    // Parent Context에서 새로 생성한 엔티티 Child Contexts들에서도 꺼내 볼 수 있음 
    let childSong1 = childContext1.object(with: songInMainContext.objectID) as! Song
    let childSong2 = childContext2.object(with: songInMainContext.objectID) as! Song
    
    // Child Context에서 엔티티의 값 변경 후 save하면 
    // 해당 변경 내용 Parent Context에 반영 
    childSong1.name = "On the Run"
    try childContext1.save()
    
    // refreshAllObjects 호출해 Parent Context에 변경사항 업데이트해주기 
    childContext2.refreshAllObjects()

     

    Unleash Core Data: Fetching Data, Migrating, and Maintaining Persistent Stores

    Create apps with rich capabilities to receive, process, and intelligently store data that work across multiple devices in the Apple ecosystem. This book will show you how to organize your … - Selection from Unleash Core Data: Fetching Data, Migrating, a

    www.oreilly.com

     

    댓글

Designed by Tistory.