iOS Core Data HeavyWeight 마이그레이션 실패기

당신을 위한 나의 실패기

Lee Di
DelightRoom
12 min readFeb 2, 2024

--

그림을 그리다가 마음에 들지 않거나 예상대로 흘러가지 않을 때 어떻게 하는지 보여드릴게요.
그냥 이렇게 덧그리기만 하면 돼요. 이렇게 하면 완전히 아름다운 나무가 다시 탄생하죠. 우리는 실수를 하지 않아요. 단지 행복한 사고가 일어났을 뿐이죠!
-밥 로스

안녕하세요 리디입니다. 🧑🏻‍💻

당신을 위한 나의 실패

딜라이트룸에는 ‘당신을 위한 나의 실패’ 라는 문화가 있습니다. 실패를 통해 레슨을 얻고 이를 공유함으로 다른 사람들의 비용을 아껴주자라는 의미입니다. 오늘은 저의 실패를 공유함으로 여러분들이 소비할 시간을 아껴드리고자 합니다.

최근 알라미 DB구조에 대한 대규모 업데이트를 진행 중에 있습니다.
그리고 해당 작업의 저의 첫 플랜은 다음과 같았습니다.

  1. Realm과 CoreData 로 나눠진 DB를 CoreData로 합친다.
  2. CoreData 데이터를 V3로 마이그레이션을 진행한다.

이 과정 중 특히 2번에서 많은 시도가 있었고, 덕분에 좋은 레슨도 얻을 수 있었습니다. 오늘은 이 과정에서의 레슨을 공유하고자 합니다.

먼저 제가 Core Data를 선택한 이유는 다음과 같았습니다.

  1. FirstParty Framework에 대한 신뢰.
  2. 이후 SwiftData로 넘어가기에 용이함.

이번에 모두 CoreData로 넘어가고 나면 이후에는 문제없이 SwiftData까지 잘 넘어갈 수 있을 것이라는 기대가 컸습니다.

Core Data Migration

먼저 CoreData의 Migration 방식에는 크게 자동(Automatically) 이라고 이야기하는 LightWeight 방식 그리고 수동(Manual) 이라고 이야기하는 HeavyWeight 방식이 있다는 것은 다들 알고 계실 것입니다.

일반적으로 우리는 LightWeight 방식을 사용하게 됩니다. Row를 추가하거나, entity 혹은 property의 이름을 바꾸거나 하는 가벼운 구조의 변경에서는 말이죠.

developer.apple.com에 따르면 다음과 같은 경우에 대하여 LightWeight 방식을 이야기하고 있습니다.

- 속성 추가
-속성 제거
-선택사항이 아닌 속성이 선택사항이 됨
-선택사항 속성이 선택사항이 아니며 기본값을 정의함
-엔터티 또는 속성 이름 바꾸기

예를 들면 다음과 같습니다.

공통으로 사용 할 것이라고 생각하여 ALColorEntity로 정의한 이름을 차량에만 사용하게 되어 ALCarColorEntity로 변경 한다거나, Entity내부의 RGB값을 rgb로 property명을 변경하는 경우에는 LightWeight로 충분합니다.(LightWeight 에 대한 자료는 구글링을 통해 금방 확인이 가능하니, 더이상의 설명은 생략하도록 하겠습니다.)

하지만 우리의 DB는 이런 light migration만을 필요로 하지 않습니다. 새로운 Column에 기본 값을 추가해준다거나, 마이그레이션 중 연산을 통해 다른 값으로 변경해줘야하는 경우도 있습니다.

이 경우 우리는 HeavyWeight방식을 통한 마이그레이션이 필요합니다. HeavyWeight 마이그레이션 방식을 찾아보면 몇가지 방법들이 나오지만, 개인적으로는 이 강의가 제일 좋았습니다..

친절하지 않은 애플놈들 대신 친절한 raywenderlich 강의는 마음을 편안하게 만들어주죠.

요약해보면 다음과 같습니다.

1. 새로운 모델을 추가해주고 Model Version을 변경해준다.
2. Source와 Destination을 설정한 MappingModel을 만들어준다.
3.세뷰 규칙을 정한 객체를 만들고 NSEntityMigrationPolicy 를 채택합니다.
4.그리고 이 객체를 CustomPolicy에 등록시켜준다.

그러면 App의 Launch타임에 HeavyWeight migration이 동작하게 되는 것이죠.

참 쉽죠?

오늘의 주제는 HeavyWeight 하는 방법이 아니기 때문에 강의로 설명을 대체합니다

하지만 우리는 이러한 마이그레이션을 1회만 하지는 않을 것입니다.
V1 → V2 → V3 시간이 지남에 따라 버젼이 늘어나겠죠. 그리고 여기에서부터 모든 문제가 시작하게 되죠…

Realm Migration

알라미는 그동안 대부분의 DB를 Realm을 통해 관리하고 있었습니다. 최근 추가된 수면 관련 데이터들만 CoreData를 통해 관리하고 있었죠.

Realm에서의 Migration 방식을 살펴보자면 다음과 같습니다.

1. Realm.Configuration 에 새로운 schemaVersion을 등록해준다.
2. migrationBlock 내부에 세부 마이그레이션 로직을 넣어둔다.
3. Realm을 초기화하기 전 Configuration에 넣어준다.
4. Realm은 migrationBlock 을 실행하며 마이그레이션을 진행한다.

migrationBlock에서 우리는 마이그레이션 수행에 사용되는 Migration 객체와 oldSchemaVersion을 통해 쉽게 마이그레이션을 동작시킬 수 있습니다.

Realm에서는 migration schema가 v1 → v2 → v3로 업데이트가 되도 문제가 되지 않습니다. 순차적으로 마이그레이션을 타도록 코드를 짠다면 우리는 직전 모델에 대해서만 대응을 하면 되니까요!

AlarmEntity migration시 사용되는 코드.. schemaVersion이 상당하다..!

Core Data Migrating multiple versions

그렇다면 CoreData는 이런 여러가지 버젼에 대한 Migration에 대하여 어떻게 동작하고 있을까요?

CoreData는 기본적으로 저장소에 사용된 모델과 번들의 현재 모델간의 불일치를 감지합니다. 그리고 불일치할 경우 자동으로 마이그레이션 시도합니다. 때문에 CoreData는 version에 대한 순차적 마이그레이션을 지원하지 않습니다.

즉, V1 모델을 갖고 있는 유저와 V2의 모델을 갖고 있는 유저가 최신인 V3로 업데이트 할 경우에는 다음과 같이 동작하게 됩니다.

하아…

사실 여기까지의 동작을 파악하는 데에도 꽤 오랜 시간이 걸렸습니다.
당연히 버젼을 순차적으로 기억할 것이라 생각했기 때문이죠.

다시 돌아와서 이로인해 발생하는 문제점은 명확합니다. Version이 올라갈 수록 관리비용이 급격하게 증가한다는 점이죠. Version 4가 나올 경우, [V1 → V4, V2 → V4, V3 → V4] 3개의 mapper가 필요합니다. 즉, version N의 경우 N-1 개의 Mapping Model이 필요하게 되는 것이죠.

시간이 흐르고 우리가 V10까지 만들었다고 가정을 해봅시다.

우리는 기억도 안나는 과거의 V1에서부터 V10으로의 마이그레이션을 고민해야합니다. 생각만해도 끔찍하죠..

딱 이 심정…

마이그레이션을 최소화 시키라는 애플의 큰 그림일까요..
하지만 저는 그런 그림 싫은데요…

물론 갓플에서도 이러한 문제점을 알았는지 WWDC 2023에서 Staged 방식을 통해 CoreData Migration의 문제점을 해결할 수 있도록 해주었습니다.

https://developer.apple.com/videos/play/wwdc2023/10186

너무나 매력적이죠!

단,
iOS 17에서부터 된다는 것만 제외하면요…. ㅂㄷㅂㄷ..

iOS17 이면 SwiftData를 썼겠지.. 이 사과놈아..

Core Data progressive migration

하지만 좌절하긴 이르죠. 우리의 선배님들이 이런 문제에서 고통만 받고 계실 리 없죠.

이러한 마이그레이션의 문제점을 해결하기 위한 키워드로 Core Data progressive migration이라는 키워드로 검색해본 결과 몇가지 유용한 아티클들을 찾게 됩니다.

PROGRESSIVE CORE DATA MIGRATIONS
by.williamboles

Core Data Progressive Migrations
by.kean

그리고 이를 응용한 Framework도 찾게 되었죠.

(🫢 오이오이.. 믿고있었다구..)

내용을 살펴보면 다들 비슷한 방식으로 순차적 마이그레이션을 진행하고 있습니다.

이해가 잘 안되실 경우, 블로그 가서 코드 한번 보면 이해가 쏙쏙 됩니다.

자세한 설명은 블로그를 찬찬히 읽어보시고, 내용을 더 요약해보자면 다음과 같습니다.

1. 버젼의 순서를 수동으로 등록 한다.
2. NSManagedObjectModel API를 통해 SourceModel의 현재 버젼을 파악한다.
3. 순차적으로 MigrationStep을 따라간다.
4. 마이그레이션 끝 🥳

오! 쉽습니다. 실제로 동작시켜보니 잘 동작하는 것도 확인할 수 있습니다.

해치웠나..!?

이렇게 행복하게 마이그레이션 글쓰기를 종료..
하고 싶었지만.. 코어데이터는 그렇게 호락호락하지 않습니다..

Product에 적용하는 것은 또 다른 이야기이죠..

이제 문제는 알 수 없는 곳에서 발생했습니다.

특정 commit 이전에서 현재 버젼으로 마이그레이션을 시도할 경우 SourceModel의 현재 버젼을 파악할 수 없는 상황이 발생했기 때문입니다.

* 문제가 되었던 API

metadata 오픈 해봐도 값들이 전부 잘 들어있어 원인 파악이 되지 않았고,
version을 강제하기도 하니 맵핑과정에서 크래시가 났습니다…
configuration에 대한 내부 로직을 알 수 없으니 안정감이 떨어졌습니다.

후..!
정말이지.. 왜 Realm을 사용하는지 갑자기 이해가 되기 시작했습니다..

선녀 같은 Realm의 마이그레이션..

급작스러운 글의 마무리이긴 하지만,
여기까지의 시도를 마지막으로 코어데이터 마이그레이션에 대한 저의 도전은 막을 내렸습니다.

이유들은 위의 원인만 있었던 것은 아니었습니다.

  1. 여전히 안정성에 대한 불안함
    - 코어데이터에 대한 이해도가 여전히 낮았고, 위의 이슈를 해결한다고 해도 안정성에 대한 불안감이 사라지지 않을 것 같았습니다.
  2. 소비되는 비용이 너무 커지고 있음
    - 우리의 목표는 유저에게 원하는 가치를 안전하고, 빠르게 전달하는 것이었습니다. 하지만 이대로 작업하다가는 안전함과 빠름 어떤 것도 달성하기 힘들다고 판단했기 때문입니다.
  3. Model과 MappingModel 의 수정을 코드로 할 수 없어서 변경에 대한 확인이 어려움.
    - Storyboard를 쓰지 않음과 마찬가지로 변화를 코드로 확인할 수 없고, 이 부분이 쌓였을 때 안정성을 해칠 수 있다고 판단했기 때문입니다.

CoreData에 대하여 어느 정도 다이브 해보고 나니 오히려 필요한 부분이 명확하게 그려졌습니다.

저는 결국 CoreData의 모델들을 모두 Realm 으로 이관시키기로 결정하였습니다. 다행히 우리는 interface를 통해 Repository에 접근을 하고 있었기 때문에 변경 비용이 크게 들어가진 않았습니다.

그렇게 우리 앱은 조금 더 안정적인 상태로 QA를 진행 중에 있습니다.

마무리

정리해보면 이번에 제가 얻은 레슨은 크게 2가지 입니다.

첫번째로, CoreData의 마이그레이션 로직에 대한 이해도를 높일 수 있었습니다.

  • 지금껏 서버와 통신이 중요한 앱들을 만들다보니, Local DB에 대해서 이렇게 만져볼 기회가 없었습니다. 이번 기회를 통해 Core Data와 Realm의 동작들에 대한 이해도를 높일 수 있었고, 덕분에 앞으로의 방향성에 대한 확신을 얻을 수 있었습니다.

두번째는, FirstParty Framework 이라도 검증을 통해 안정성을 체크해야 한다라는 점이었습니다.

  • 만약 POC의 결과만 믿고 앱을 배포 했다면, 특정 버젼 이전의 유저들은 크래시를 경험하게 되었을 것이고, 이는 우리 앱에 치명적인 결과를 낳을 수 있기 때문입니다. 결국 모두 도구라고 생각하고 검증에 대한 필요성을 한번 더 느끼게 되었습니다.

하지만 그렇다고 FirstParty Framework를 포기하지는 않았습니다.

한번 깔끔하게 털어내고, 잘 닦아내고 이해도를 높인 후 다음에는 Realm에서 SwiftData 로 다시 돌아올 예정입니다. 물론 이 경우에도 여러가지 실험을 통해 안전함과 유용함을 검증 해야겠지요.

끝까지 읽어주셔서 감사합니다!

⏰ 딜라이트룸에서 알라미와 함께 아침을 바꿀 분들을 모십니다

딜라이트룸의 다양한 채널들을 팔로우하고 빠르게 소식을 받아보세요!

--

--