[Swift] SwiftData 상속과 스키마 마이그레이션 알아보기

2026. 3. 18. 15:12·Apple/Swift
728x90
반응형

안녕하세요! 피피아노입니다 🎵

 

WWDC 세션 SwiftData: 상속과 스키마 마이그레이션 자세히 알아보기 세션을 보고 내용을 정리해보려고 합니다.

 

이번 세션의 핵심은 iOS 26에서 새롭게 도입된 클래스 상속 지원과 더 효율적인 데이터 관리 및 마이그레이션 전략입니다.

 

SwiftData의 클래스 상속(iOS 26 신기능)

이제 SwiftData 모델에서도 클래스 상속을 활용해 계층 구조를 설계할 수 있습니다.

 

상속이 필요한 이유에 대해서 먼저 간단하게 살펴보자면,

 

우리가 여행 앱을 만든다고 가정을 해보겠습니다.

 

우리에게는 '개인 여행'과 '출장' 데이터가 있습니다. 둘 다 '장소'와 '날짜'라는 공통점을 가지고 있지만, '목적'이나 '비용' 같은 고유한 데이터도 필요합니다.

 

상속이 생기기 전에는 PersonalTrip(개인 여행)과 BusinessTrip(출장)을 각각 만들어서 관리했습니다.

 

만약 전체 리스트를 보여주고 싶다면 두 가지를 모두 열어서 데이터를 합치고 정렬해야 했습니다.

 

하지만 이제 상속이 가능하기 때문에 Trip이라는 부모 클래스를 두고 자식 클래스가 이것을 확장하면 됩니다.

상속 도입 전

코드로도 한번 살펴보겠습니다.

protocol TripProtocol {
    var destination: String { get set }
    var startDate: Date { get set }
}

@Model class PersonalTrip: TripProtocol {
    var destination: String
    var startDate: Date
    var reason: String
}

@Model class BusinessTrip: TripProtocol {
    var destination: String
    var startDate: Date
    var perDiem: Int
}

 

기존에는 이렇게 상속 대신 '이런 속성을 가져야 한다'라는 규칙(Protocol)만 정해두고, 각각의 클래스가 그 규칙을 따르게 만들었습니다.

 

물론 이 방식도 좋은 방식이지만, destination과 startDate라는 변수와 초기화 로직을 모든 클래스에 중복해서 작성해야 했습니다.

 

그리고 @Query를 날릴 때 [TripProtocol] 타입으로 한 번에 가져오는 게 불가능했습니다. 그렇기 때문에 [PersonalTrip]과 [BusinessTrip]을 각각 따로 Fetch해서 수동으로 합쳐야 했습니다.

 

또 다른 방법으로는 

 

단일 모델 & 열거형 방식(데이터 구조가 단순할 때)을 쓰는 방법이 있었습니다.

클래스를 나누지 않고, 하나의 Trip이라는 클래스 안에 '타입'을 구분하는 변수를 두는 방식입니다.

 

이것도 코드로 보면 아래처럼 작성할 수 있습니다.

@Model class Trip {
    var destination: String
    var startDate: Date
    var type: TripType // .personal 또는 .business
    
    // 두 자식의 속성을 모두 다 가지고 있어야 함 (Optional)
    var reason: String?
    var perDiem: Int?
}

하지만 이 방식도 한계점이 있는데 PersonalTrip일 때는 perDiem이 필요 없는데도 모델이 그 변수를 가지고 있어야 합니다.

 

그리고 데이터가 복잡해질수록 모델 클래스가 너무 무거워지고, 어떤 데이터가 필수인지 헷갈리게 됩니다.

 

상속 도입 후

@Model class Trip {
    var destination: String
    var startDate: Date
}

@Model class PersonalTrip: Trip {
    var reason: String
}

@Model class BusinessTrip: Trip {
    var perDiem: Int
}

상속을 도입하고 나서는 이제 공통점은 Trip이라는 부모 클래스에 맡기고, 자식 클래스는 자기 특색만 챙기면 됩니다.

 

이렇게 코드를 작성하면 단일 쿼리가 되기 때문에 메인 화면에서 그냥 부모 클래스 하나만 부르면 끝납니다. 그리고 또 다른 이점으로는 리스트에서 이름을 보여줄 때는 trip.destination으로 공통 접근하고, 상세 페이지로 갈 때만 필요한 데이터만 골라 쓰면 됩니다.

 

스키마 마이그레이션

앱의 버전이 올라가면 모델에 새로운 속성이 추가되거나, 기존 속성의 이름이 바뀌기도 합니다. SwiftData는 이를 단계별로 관리할 수 있는 VersionedSchema와 SchemaMigrationPlan을 제공합니다.

 

SwiftData에서 마이그레이션은 크게 3가지로 구성됩니다.

  • VersionedSchema(버전별 스키마): 각 앱 릴리스마다 모델의 상태를 정의함
  • MigrationPlan(마이그레이션 계획): 스키마 버전의 순서와 버전 간 이동 시 실행할 단계를 정의함
  • ModelContainer 설정: 앱 시작 시 마이그레이션 계획을 컨테이너에 적용하여 실제 데이터를 변환함

 

기존의 단일 모델 Trip이 있었는데, 앱이 업데이트되면서 BusinessTrip과 PersonalTrip으로 상속 구조가 생겼을 때 마이그레이션 계획을 어떻게 짜는지 WWDC 세션에 나온 예시를 가져와서 설명을 해보겠습니다.

1. 버전별 스키마 정의

마이그레이션의 첫 단계는 각 버전의 데이터 모델을 독립된 개체로 분리하는 것입니다. VersionedSchema 프로토콜을 사용하여 특정 시점의 모델 상태를 박제합니다.

@available(iOS 26, *)
enum SampleTripsSchemaV4: VersionedSchema {
    // 1. 버전 식별자: 이전 버전과 구분하기 위한 고유 번호를 지정합니다.
    static var versionIdentifier = Schema.Version(4, 0, 0)

    // 2. 모델 리스트: 이 버전에 포함된 모든 데이터 모델 타입을 배열로 정의합니다.
    // 상속 구조가 도입되었다면 부모(Trip)와 자식(BusinessTrip, PersonalTrip)을 모두 명시해야 합니다.
    static var models: [any PersistentModel.Type] { 
        [Trip.self, BusinessTrip.self, PersonalTrip.self, BucketListItem.self, LivingAccommodation.self] 
    }

    // 3. 모델 정의: 해당 버전에서 사용하는 실제 데이터 구조를 내부에 선언합니다.
    @Model
    class Trip {
        @Attribute(.unique) var name: String
        var destination: String
        // 생략...
    }
}

 

2. 마이그레이션 계획

버전이 정의되었다면, 버전 사이를 이동하는 일종의 통로를 만들어줘야 합니다.

enum SampleTripsMigrationPlan: SchemaMigrationPlan {
    // 앱이 거쳐온 모든 설계도를 순서대로 나열합니다.
    static var schemas: [any VersionedSchema.Type] {
        [V1.self, V2.self, V3.self, V4.self]
    }

    // 각 버전 사이의 전환 방식을 정의합니다.
    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3, migrateV3toV4]
    }

    // Custom Migration: 단순 구조 변경이 아닌 '데이터 로직'이 필요할 때 사용합니다.
    static let migrateV2toV3 = MigrationStage.custom(
        fromVersion: SampleTripsSchemaV2.self,
        toVersion: SampleTripsSchemaV3.self,
        willMigrate: { context in
            // 1. 구버전 데이터를 fetch 합니다.
            let fetchDesc = FetchDescriptor<SampleTripsSchemaV2.Trip>()
            let trips = try? context.fetch(fetchDesc)
            
            // 2. 중복 제거 등 데이터를 가공하는 로직을 이곳에서 직접 코드로 작성합니다.
            // 3. 작업이 끝나면 저장하여 새 스키마에 반영합니다.
            try? context.save()
        }, 
        didMigrate: nil
    )
}

willMigrate 클로저는 데이터베이스의 구조가 실제로 바뀌기 직전에 실행됩니다. 여기서 데이터를 미리 정리해두어야 새 스키마의 제약 조건을 위반하지 않고 마이그레이션이 성공합니다.

 

3. 가져오기 최적화 

데이터 양이 많아질 때 전체 모델을 메모리에 올리는 것은 비효율적입니다. FetchDescriptor를 정교하게 설정하여 필요한 리소스만 사용합니다.

var fetchDesc = FetchDescriptor<Trip>()
// 모델 전체 데이터가 아닌 'name' 속성의 데이터만 메모리에 로드하도록 제한합니다.
// 대량의 데이터를 순회하며 중복 체크를 할 때 메모리 점유율을 획기적으로 낮춥니다.
fetchDesc.propertiesToFetch = [\.name] 

let trips = try context.fetch(fetchDesc)

 

가져오기 한도 설정(fetchLimit)

// 정렬 조건에 맞는 최상위 데이터 1개만 가져옵니다.
// 위젯이나 '가장 최근 여행'을 보여주는 화면에서 불필요한 전체 조회를 막아줍니다.
fetchDesc.fetchLimit = 1
fetchDesc.sortBy = [SortDescriptor(\.startDate, order: .forward)]

 

4. 영구 기록 추적(Persistent History)

앱의 메인 프로세스 외부(예: Widget, App Group 공유 앱)에서 데이터가 변경되었을 때, 이를 효율적으로 감지하는 방법입니다.

// 1. 최신 트랜잭션 기록을 찾기 위한 설정입니다.
var historyDesc = HistoryDescriptor<DefaultHistoryTransaction>()
// 2. 가장 최근 기록이 맨 위로 오도록 역순 정렬합니다.
historyDesc.sortBy = [.init(\.transactionIdentifier, order: .reverse)]
// 3. 딱 하나만 가져와서 현재의 마지막 위치(Token)를 파악합니다.
historyDesc.fetchLimit = 1

let transactions = try context.fetchHistory(historyDesc)
if let lastToken = transactions.last?.token {
    // 4. 이 토큰 이후에 발생한 '특정 엔터티'의 변경 사항만 골라내는 조건문을 만듭니다.
    let tokenPredicate = #Predicate<DefaultHistoryTransaction> { $0.token > lastToken }
    let changesPredicate = #Predicate<DefaultHistoryTransaction> {
        $0.changes.contains { $0.changedPersistentIdentifier.entityName == "Trip" }
    }
}
728x90
반응형

'Apple > Swift' 카테고리의 다른 글

[Swift] Swift 6로 마이그레이션 하기  (3) 2026.03.04
[Swift] 싱글톤 패턴(Singleton Pattern) 알아보기  (4) 2026.02.23
[Swift] Swift 동시성 사용하기  (0) 2026.02.09
[Swift] 3D 스캔 앱을 로컬 서버와 연결하기  (2) 2025.12.30
[Swift] SwiftData 모델 구조 변경 시 런타임 에러 해결하기  (0) 2025.10.13
'Apple/Swift' 카테고리의 다른 글
  • [Swift] Swift 6로 마이그레이션 하기
  • [Swift] 싱글톤 패턴(Singleton Pattern) 알아보기
  • [Swift] Swift 동시성 사용하기
  • [Swift] 3D 스캔 앱을 로컬 서버와 연결하기
P_Piano
P_Piano
Apple 생태계 개발자가 되기 위한 학습과 경험의 기록
    반응형
    250x250
  • P_Piano
    피피아노의 개발 일지
    P_Piano
  • 전체
    오늘
    어제
    • 분류 전체보기 (226) N
      • Apple (145) N
        • iOS (25)
        • visionOS (5)
        • Swift (76)
        • UIKit (2)
        • SwiftUI (26) N
        • RxSwift (2)
        • Xcode (5)
        • Metal (2)
      • C언어 (5)
      • C++ (8)
      • Dart (1)
      • Python (3)
      • JavaScript (17)
      • Git (1)
      • CS (1)
        • 디자인 패턴 (6)
        • 네트워크 (20)
        • 운영체제 (8)
        • Database (5)
        • 자료구조 (0)
      • IT 지식 (2)
      • IT 뉴스 (4)
      • 출처 표기 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    오블완
    티스토리챌린지
    옵셔널
    Vision Pro
    네트워크
    데이터베이스
    코딩테스트
    운영체제
    이니셜라이저
    SWIFT
    프로퍼티 래퍼
    스위프트
    wwdc25
    Initializers
    Xcode
    프로세스
    함수
    클래스
    배열
    UIKit
    제어문
    디자인패턴
    swiftUI
    연산자
    ios
    Apple
    변수
    자바스크립트
    visionOS
    비동기
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
P_Piano
[Swift] SwiftData 상속과 스키마 마이그레이션 알아보기
상단으로

티스토리툴바