SwiftData 성능은 스토리지 문제입니다
WWDC 2026 SwiftData 그룹 랩에는 프레임워크 아래 계층을 직접 책임지는 사람들이 배치되어 있었습니다. Apple 플랫폼 전반의 SQLite를 유지보수하는 엔지니어와 Core Data 및 SwiftData 담당 매니저도 그중 한 명이었습니다. 그들의 답변을 관통하는 주제는, 대부분의 개발자가 성능에 접근하는 방식에 대한 유용한 교정이었습니다. SwiftData가 앱에 들어오는 순간 비용이 큰 것은 Swift 코드가 아니라 I/O이며, 성능 향상은 동시성을 더하는 데서가 아니라 더 적게 읽고 스토리지 엔진을 이해하는 데서 온다는 것입니다. 이후 내용 대부분은 Apple과 SQLite의 문서에 근거합니다. 어떤 주장이 문서화된 사실이 아니라 랩에서의 엔지니어링적 추론인 경우에는 그렇게 표시했습니다.
WWDC 2026 SwiftData 그룹 랩.
TL;DR
- SwiftData의 SQLite 스토어는 기본적으로 write-ahead logging(WAL)을 사용합니다. 즉, 여러 개의 reader가 단일 writer와 동시에 실행됩니다. 이것은 reader/writer 락이 아니며, 랩에서는 개발자들이 흔히 이 구분을 잘못 이해한다고 지적했습니다.23
- 객체를 구체화하지 않고 읽기:
fetchCount(_:)는 일치 개수를 반환하고fetchIdentifiers(_:)는[PersistentIdentifier]를 반환하며, 둘 다 모델을 하이드레이션하지 않습니다. 이를 history 관찰과 짝지어 새로 고침이 정말 필요한지 판단하세요.45 @Model객체는Sendable이 아니며, 억지로Sendable로 만들어서는 안 됩니다. 액터 경계를 넘으려면Sendable인PersistentIdentifier와 추출한 값들을 함께 넘긴 다음, 도착 측 컨텍스트에서 다시 fetch하세요.6- SwiftData에는 Core Data의 SQL로 푸시되는 집계 쿼리(sum, average, min, max)에 해당하는 기능이 없습니다. 우회로는 공존입니다. 같은 스토어 파일을 대상으로 Core Data 스택을 돌려 집계를 계산하게 하는 것입니다.8
- 랩의 성능에 대한 메시지: 데이터베이스가 느리다고 단정하기 전에 프로파일링으로 SwiftUI가 왜 다시 fetch했는지 알아내세요. 뷰의 과도한 무효화는 그렇지 않은데도 I/O 문제처럼 보이기 때문입니다.110
WAL: 동시 reader, 단일 writer, 락이 아님
랩에서 나온 가장 유용한 교정은 스토리지 계층의 동시성에 관한 것입니다. SwiftData는 Core Data SQLite 스토어 위에 구축되어 있으며, 그 스토어는 iOS 7부터 write-ahead logging을 기본값으로 사용해 왔습니다.3 SQLite 문서가 표현하듯 WAL 하에서는 “WAL은 reader가 writer를 막지 않고 writer가 reader를 막지 않으므로 더 많은 동시성을 제공합니다. 읽기와 쓰기는 동시에 진행될 수 있습니다.”2 여전히 한 번에 정확히 하나의 writer만 존재하지만, 모든 데이터베이스 접근을 직렬화하는 뮤텍스라는 멘탈 모델은 틀렸습니다. 여러분의 읽기는 쓰기 뒤에서 기다릴 필요가 없습니다.
랩의 설명을 녹취에서 풀어 옮기자면, 사람들은 단일 writer 규칙을 reader/writer 락처럼 취급하고 존재하지도 않는 제약을 중심으로 아키텍처를 설계한다는 것이었습니다.1 정확한 모델은 WAL 모델입니다. 전역 배제가 아니라, 다수의 동시 읽기와 직렬화된 쓰기를 염두에 두고 설계하세요.
더 적게 읽기: 하이드레이션 없이 세고 식별하기
I/O가 비용이라면, 가장 레버리지가 큰 행동은 필요하지 않은 객체를 로드하지 않는 것입니다. SwiftData는 이를 위한 두 가지 프리미티브를 제공하며, 둘 다 API에서 확인됩니다.
ModelContext의 fetchCount(_:)는 FetchDescriptor를 받아 일치하는 모델 중 어느 것도 인스턴스화하지 않고 그 개수를 Int로 반환합니다.4 배지나 섹션 헤더에 개수가 필요할 때, 이것은 fetch해서 .count를 호출하는 것보다 엄밀하게 더 저렴합니다.
fetchIdentifiers(_:)는 descriptor에 대해 역시 모델을 구체화하지 않고 [PersistentIdentifier]를 반환하며, fetchIdentifiers(_:batchSize:) 오버로드는 작업을 배치 처리합니다.5 랩이 제안한 사용법을 풀어 옮기면, 이를 history 관찰과 짝짓는 것입니다. 변경이 들어오면 영향받은 식별자를 fetch하고, 무언가를 다시 로드할지 결정하기 전에 뷰가 실제로 표시하는 것과 비교하세요.1 history와 관찰 API 자체는 SwiftData의 iOS 27 관찰과 history에서 다룹니다. fetchIdentifiers는 이들을 효율적으로 만들어 주는 가벼운 읽기입니다. SwiftUI 바깥에서 손이 가야 할 관찰 타입은 ResultsObserver입니다. 2027년 릴리스를 위해 도입된 Swift Observation 기반 관찰자로, sectionBy:를 통한 키패스 섹셔닝을 포함해 @Query와 동일한 프리미티브를 지원합니다.9
Sendable 경계는 실재하며, 모델 그래프는 그 경계를 넘지 않습니다
SwiftData 모델은 컨텍스트 내부의 그래프에 연결된 참조 타입이며 Sendable이 아닙니다. 랩은 이들을 제정신으로 Sendable로 강제할 수 없다고 직설적으로 말했습니다. 그래프가 스레드 안전하지 않고, 다른 액터에서 그것을 부분적으로 하이드레이션하면 문제로 이어지기 때문입니다.1 지원되는 패턴은 Sendable이자 Hashable이며 Codable인 PersistentIdentifier를 경계를 넘어 옮기는 정체성으로 사용합니다.6 필요한 값을 구조체로 추출하고, PersistentIdentifier를 붙여 다른 액터에 넘긴 다음, 살아 있는 객체가 필요하면 도착 측 컨텍스트에서 모델을 다시 fetch하세요.
기억해 둘 만한 정밀한 한 가지: Apple은 디코딩된 PersistentIdentifier와 기본 스토어가 생성한 것이 항상 동등하게 간주되는 것은 아니라고 언급합니다. 따라서 디코딩된 사본이 살아 있는 것과 같다고 가정하기보다는, 식별자를 안정적인 크로스 컨텍스트 핸들로 취급하세요.6
같은 “그래프가 아니라 정체성” 원칙은 프로세스 전반에 걸쳐 나타납니다. 위젯이나 확장과 공유하기 위해 스토어를 앱 그룹으로 옮길 때, 기본 구성은 기존 스토어를 앱 그룹 컨테이너로 복사해 줍니다. 사용자 지정 스토어 URL을 쓰면 위치를 직접 관리합니다.7 어느 쪽이든 프로세스들은 살아 있는 객체를 서로 주고받는 것이 아니라 스토어와 그 식별자를 통해 협력합니다.
집계 함수의 공백, 그리고 Core Data 우회로
랩이 짚은 실제 한계: SwiftData에는 Core Data의 NSExpression 기반 집계 쿼리에 해당하는 기능이 없습니다. sum, average, min, max를 SQLite로 푸시해 데이터베이스가 행을 로드하지 않고 계산하게 하는 그 쿼리들 말입니다.8 SwiftData에서는 행을 fetch한 다음 메모리에서 reduce하게 되는데, 큰 테이블에서는 이것이 목적을 무색하게 합니다. min이나 max의 경우 정렬 descriptor와 fetch limit 1로 fetch할 수 있습니다. 진정한 집계가 필요하다면, 랩은 공존을 가리켰습니다.
공존은 Apple이 WWDC 2023에서 설명한 대로 “완전히 분리된 두 개의 영속성 스택, 즉 하나의 Core Data 스택과 하나의 SwiftData 스택이 같은 영속 스토어와 대화하는 것”입니다.8 두 스택 모두 같은 스토어 URL을 가리키며, SwiftData가 persistent history tracking을 자동으로 활성화하기 때문에 Core Data 측에서도 NSPersistentHistoryTrackingKey를 활성화해야 합니다. 그렇지 않으면 스토어가 읽기 전용으로 열립니다.8 그것이 갖춰지면, SwiftData가 소유한 바로 그 파일을 대상으로 Core Data를 통해 SQL로 푸시되는 집계를 실행할 수 있습니다. 대부분의 앱에 필요한 것보다 더 많은 장치이지만, 정말로 데이터베이스 측 집계가 필요할 때 문서화된 경로입니다.
데이터베이스만이 아니라 무효화를 프로파일링하세요
랩의 가장 실용적인 성능 지침을 풀어 옮기면, SwiftData 앱의 겉보기 I/O 비용은 종종 변장한 SwiftUI 무효화 문제라는 것이었습니다. 너무 자주 무효화되는 뷰는 다시 fetch하고, 프로파일러는 그 재fetch를 데이터베이스 시간으로 보여 주지만, 진짜 잘못은 애초에 그 뷰가 새로 고침되지 말았어야 했다는 데 있습니다.1 해법은 어떤 SwiftUI 성능 문제에도 도움이 되는 뷰 격리 원칙과 같으며, SwiftUI 성능과 상호 운용에서 다룹니다. 큰 뷰를 의존성이 좁은 작은 뷰들로 쪼개고, 이미 fetch한 모델을 아래로 전달해 쿼리가 다시 실행되지 않게 하세요.
도구도 이런 진단을 뒷받침합니다. Instruments는 SwiftUI instrument를 Hangs and Hitches instrument와 함께 묶은 SwiftUI 템플릿, Reads and Writes instrument가 실제 디스크 트래픽을 보여 주는(시뮬레이터가 아닌 기기 전용) File Activity 템플릿, 그리고 fault, fetch, save를 보고하는 Data Persistence instrument가 포함된 Core Data 템플릿을 제공합니다.10 SwiftUI 뷰와 영속성 뷰를 함께 실행하면, 재fetch가 진짜 읽기였는지 아니면 과도한 무효화로 촉발된 중복 읽기였는지 알 수 있습니다.
랩이 벤치마킹에 관해 제기한 주의를 풀어 옮기면, 아래로 끝까지 캐시가 있다는 것입니다. SQLite 페이지 캐시, OS 파일 캐시, 그리고 스토리지 컨트롤러까지요. 그래서 “빠른” 실행이 실제 개선이 아니라 캐시 히트일 수 있습니다. 현실적으로 큰 데이터셋을 대상으로 측정하고, File Activity instrument로 실제 I/O가 일어났는지 확인하세요.1
동시성 추가에 관하여
랩의 가장 강한 의견이자, 문서화된 사실이 아니라 엔지니어링적 추론으로 받아들여야 할 부분은, 성능 해법으로 동시성에 손을 뻗는 것에 대한 경고였습니다. 엔지니어들은 SwiftData의 커넥션 풀링이 의도적으로 제한되어 있다고 설명했고, 소수의 동시 작업을 넘어서면 스토리지 하드웨어의 한계에 부딪히므로 컨텍스트를 더 늘려도 더 많은 메모리와 더 많은 I/O를 대가로 수익이 점점 줄어든다고 주장했습니다.1 Apple은 특정 동시성 한계를 문서화하지 않으므로, 이 글을 포함해 누구에게서도 확정된 수치를 받아들이지 마세요. 옹호할 수 있는 결론은 방향성에 있습니다. 플래시 스토리지 기기에서 동시 writer를 쌓아 올리는 것은 더 빨라지는 신뢰할 만한 방법이 아니며, WAL 모델은 이미 동시 읽기를 공짜로 제공한다는 것입니다.
여기서 가져갈 것
랩은 SwiftData 성능을 스토리지 엔진을 중심으로 재구성합니다. 검증된 레버는 구체적입니다. 락을 두려워하는 대신 WAL의 동시 읽기에 기대고, fetchCount와 fetchIdentifiers를 사용해 객체 하이드레이션을 피하며, 모델 그래프가 아니라 PersistentIdentifier를 액터 사이로 옮기고, 진짜 집계가 필요할 때 Core Data 공존에 손을 뻗으세요. 프로파일링의 원칙은 데이터베이스를 최적화하기 전에 I/O 비용이 실재하는지 확인하는 것입니다. 범인은 종종 그러지 말았어야 했는데 새로 고침된 뷰이기 때문입니다.
FAQ
SwiftData는 쓰기 동안 데이터베이스를 잠그나요?
reader/writer 락이라는 의미에서는 아닙니다. 스토어는 SQLite write-ahead logging을 사용하며, 이는 여러 reader가 단일 writer와 동시에 실행되게 합니다. 읽기는 writer를 막지 않고 writer는 읽기를 막지 않습니다.23 한 번에 하나의 writer만 있지만, 읽기는 그와 나란히 진행됩니다.
레코드를 로드하지 않고 세거나 확인하려면 어떻게 하나요?
일치 개수에는 ModelContext.fetchCount(_:)를, [PersistentIdentifier] 값에는 ModelContext.fetchIdentifiers(_:)를 사용하세요. 둘 다 모델 객체를 구체화하지 않습니다.45 fetchIdentifiers를 history 관찰과 결합해, 다시 로드하기 전에 변경이 실제로 뷰가 보여 주는 것에 영향을 주는지 판단하세요.
SwiftData 객체를 다른 액터에 어떻게 넘기나요?
객체를 넘기지 않습니다. @Model 타입은 Sendable이 아닙니다. Sendable인 PersistentIdentifier와 추출한 값들을 넘긴 다음, 도착 측 컨텍스트에서 다시 fetch하세요.6 살아 있는 모델 그래프를 경계 너머로 건네지 마세요.
SwiftData는 데이터베이스에서 sum/average/min/max를 할 수 있나요?
아니요. SwiftData에는 Core Data의 NSExpression SQL 푸시 집계에 해당하는 기능이 없습니다.8 min/max의 경우 정렬과 fetch limit 1로 fetch하세요. 진짜 집계의 경우 같은 스토어 파일을 대상으로 Core Data 스택을 실행(공존)하며, 이는 스토어 URL을 일치시키고 Core Data 측에서 persistent history tracking을 활성화하는 것을 요구합니다.8
이 블로그의 SwiftData 라인은 스키마 원칙과 마이그레이션 가이드에서 스키마와 마이그레이션 원칙을, 관찰과 history에서 iOS 27 관찰과 history API를 다룹니다. 이 글은 성능과 스토리지 계층을 더합니다. 전체 시리즈 허브는 Apple Ecosystem Series입니다.
References
-
Apple, WWDC 2026 session 8017, SwiftData Group Lab. Paraphrased from a locally transcribed recording; Apple publishes no official captions for the labs, so the wording here is a paraphrase, not a quotation, and exact phrasing is unverified. Source for the reader/writer-lock misconception framing, the
fetchIdentifiers-plus-history refresh-gating suggestion, the@Modelnon-Sendable transfer guidance, the view-invalidation-masquerading-as-I/O point, the “caches all the way down” benchmarking caution, and the connection-pool/concurrency-ceiling position (which is the lab’s engineering reasoning, not documented behavior; no specific concurrency number is asserted here because Apple does not document one). ↩↩↩↩↩↩↩ -
SQLite, Write-Ahead Logging. Source for the WAL concurrency model: “WAL provides more concurrency as readers do not block writers and a writer does not block readers,” with a single writer at a time. ↩↩↩
-
Apple, Technical Q&A QA1809: Setting the SQLite journaling mode for a Core Data store. Source for write-ahead logging being the default journaling mode for Core Data SQLite stores since iOS 7 and OS X Mavericks; SwiftData is built on the Core Data SQLite store. ↩↩↩
-
Apple,
ModelContext.fetchCount(_:). Signaturefunc fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel; returns the number of models matching the descriptor without instantiating them. ↩↩↩ -
Apple,
ModelContext.fetchIdentifiers(_:)andfetchIdentifiers(_:batchSize:). Returns[PersistentIdentifier]for a fetch descriptor without materializing the models, with a batched overload. ↩↩↩ -
Apple,
PersistentIdentifier. The aggregate identity of a SwiftData model; it isSendable,Hashable, andCodable, making it the type to move across actor boundaries. Apple notes a decodedPersistentIdentifierand one created by the default store are not always considered equivalent, so treat it as a stable cross-context handle. ↩↩↩↩ -
Apple, Adopting SwiftData for a Core Data app. Source for the app-group behavior: when an app evolves to use an app group container, SwiftData copies the existing store into the app group container under the default configuration; with a custom store URL you manage the location yourself. ↩
-
Apple, WWDC 2023 session 10189, Migrate to SwiftData, and
NSExpression. Source for coexistence (“two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store”), the requirement that both use the same store URL and that the Core Data stack enableNSPersistentHistoryTrackingKeyor the store opens read-only, and for Core Data’sNSExpression-based SQL aggregates that SwiftData does not provide an equivalent to. ↩↩↩↩↩↩ -
Apple, WWDC 2026 session 274, What’s new in SwiftData. Source for
ResultsObserver, the Swift Observation-based observation type that supports the same primitives as@Queryincluding key-path sectioning viasectionBy:, shipping in the 2027 platform releases. ↩ -
Apple, WWDC 2025 session 306, Optimize SwiftUI performance with Instruments, and the Instruments File Activity and Core Data templates. Source for the SwiftUI Instruments template (bundling the SwiftUI instrument and the Hangs and Hitches instruments), the File Activity template’s Reads and Writes instrument (device only), and the Data Persistence instrument reporting faults, fetches, and saves. ↩↩
