From 1f772648525a904458cf7a9078c644b7428b04d1 Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Mon, 13 Jul 2026 03:15:38 +0900 Subject: [PATCH] fix(startup): survive store open failures instead of crashing before splash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 일기 기능의 스키마 변경(모델 4종 추가) 후 기존 데이터가 있는 기기에서 첫 실행 시 마이그레이션이 필요한데, 이때 위젯 프로세스와의 스토어 경합이나 마이그레이션 실패가 발생하면 makeContainer()가 곧장 fatalError로 죽어 스플래시도 못 띄우는 문제 방어: - 일시적 경합 대비 최대 3회 재시도(점증 대기) - 그래도 실패하면 스토어를 타임스탬프 백업으로 보존 후 새로 생성해 앱은 반드시 켜지게 - 검증: 스토어 파일 고의 손상 → 백업 생성 + 정상 부팅 확인 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7 --- myApp/HaruDanim/Shared/DataStore.swift | 36 ++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/myApp/HaruDanim/Shared/DataStore.swift b/myApp/HaruDanim/Shared/DataStore.swift index 926ebc7..d3d5397 100644 --- a/myApp/HaruDanim/Shared/DataStore.swift +++ b/myApp/HaruDanim/Shared/DataStore.swift @@ -64,12 +64,44 @@ nonisolated enum DataStore { /// 앱·위젯·인텐트가 공유하는 컨테이너 생성. /// CloudKit 연동은 메인 앱 프로세스에서만 — 확장은 같은 파일을 로컬 전용으로 연다. - /// 앱 본체(DataStore.shared)는 스토어 없이는 동작할 수 없어, 생성 실패 시 크래시한다. + /// + /// 실패해도 곧장 크래시하지 않는다: + /// 1) 위젯 등 다른 프로세스와의 일시적 스토어 경합(특히 스키마 마이그레이션이 필요한 + /// 업데이트 직후 첫 실행)일 수 있어 짧게 재시도하고, + /// 2) 그래도 실패하면(영구적 마이그레이션/파일 손상) 기존 스토어를 백업 이름으로 보존한 뒤 + /// 새 스토어로 시작한다 — 예전에는 여기서 바로 fatalError라 스플래시도 못 띄우고 + /// 앱이 무한히 죽는 상태가 될 수 있었다. 백업 파일(HaruDanim.store.backup-*)은 남는다. static func makeContainer() -> ModelContainer { do { return try makeContainerThrowing() } catch { - fatalError("[DataStore] 컨테이너 생성 실패: \(error)") + print("[DataStore] 컨테이너 생성 실패, 재시도: \(error)") + for attempt in 1...3 { + Thread.sleep(forTimeInterval: 0.4 * Double(attempt)) + if let container = try? makeContainerThrowing() { + return container + } + } + print("[DataStore] 재시도 실패 — 스토어를 백업하고 새로 시작: \(error)") + backupBrokenStore() + do { + return try makeContainerThrowing() + } catch { + fatalError("[DataStore] 컨테이너 생성 실패(백업 후 재생성도 실패): \(error)") + } + } + } + + /// 열 수 없게 된 스토어 파일(-wal/-shm 포함)을 타임스탬프 백업 이름으로 옮긴다. + /// 삭제하지 않으므로 필요 시 파일을 되돌려 데이터를 복구할 수 있다. + private static func backupBrokenStore() { + let fileManager = FileManager.default + let stamp = Int(Date.now.timeIntervalSince1970) + for suffix in ["", "-wal", "-shm"] { + let source = URL(fileURLWithPath: storeURL.path + suffix) + guard fileManager.fileExists(atPath: source.path) else { continue } + let destination = URL(fileURLWithPath: storeURL.path + ".backup-\(stamp)" + suffix) + try? fileManager.moveItem(at: source, to: destination) } }