Realm alex leffelman migrating cover

iOS에서 Realm으로 Migration 하는 법

2015년도 말, Remind는 Alex의 지휘 하에 Core Data에서 Realm으로 migration했습니다. Realm으로 앱 개발을 시작하고 운영하는 것은 좋은 선택이지만, 이미 Core Data를 사용하는 앱이 상용화된 상황이라면 어떨까요? Realm으로 교체하기 전에는 두 종류의 데이터베이스를 동시에 유지해야 할테고 복잡한 문제가 야기되겠죠. Alex가 공유하는 Realm으로의 migration을 선택한 이유와, 실수를 포함한 경험담을 통해 Realm으로 migration을 문제 없이 진행하는 법을 배워 볼까요?


소개 (0:00)

저는 Remind의 iOS 개발자 Alex입니다. 저희는 Realm의 장점을 익히 알고, 다른 애플리케이션의 프로토타입에서 Realm을 사용하고 있었지만, 몇몇 팀원은 아직 Core Data를 사용해 만들어진 애플리케이션을 작업하는 상황이었습니다. 이런 경우 Realm으로 교체하고 싶어도 어쩌면 길어질지도 모르는 migration 기간과 배포 시의 위험성을 마주친 후 결국 포기할 수도 있을 겁니다.

따라서 이 강연에서는 Core Data를 안전하게 대체하기 위해 점진적으로 migration 하는 방법을 알려드릴 예정입니다.

두 데이터베이스 병행하기 (2:42)

Remind에서 Realm과 Core Data 두 데이터베이스를 병행해서 교체해나간 방법부터 시작하겠습니다. 저희는 Core Data 스택을 유지한채 새 UI 패턴과 새 객체 매핑 코드, 더 나은 스레딩 모델을 위해 Realm 구현을 해나갔으며, 이 작업은 만일의 경우를 대비해 서버 쪽의 종료 스위치를 두고 만들어졌습니다.

우리의 시작점 (3:06)

제가 Remind에 합류하기 이전에 개발된 저희 iOS 앱은 원래 오리지널 Core Data로 구성돼 있었습니다. 입사 후 제 첫 프로젝트는 RestKit이라는 라이브러리를 통합하는 것이었습니다. 또한 Mogenerator라는 툴을 통해 모든 NSManagedObject 서브클래스를 생성해서 데이터 모델 파일을 읽고 모든 프로퍼티 접근자와 변환자 및 관계를 생성했습니다. 따라서 데이터 모델이 변경돼도 객체 클래스가 재생성될 필요가 없어졌습니다.

이런 개발 뉴스를 더 만나보세요

여기까지의 가장 큰 위험요소는 객체 매핑 레이어에 있었습니다. 데이터베이스로 들어가는 데이터는 데이터 타입을 엄밀하게 따지죠. 하지만 페이로드에서 들어오는 JSON은 데이터 타입이 확정되지 않으며, 기대한 데이터를 모두 포함하리라는 보장이 없습니다. 숫자를 기대했는데 문자열이 담겨 있기도 하죠. 정수를 기대했지만 서비스 파이프라인 어디선가 더블로 바꿔버리는 경우도 생깁니다.

어느 레벨까지는 이런 경우들이 API 코드 상의 버그이므로 이론적으로 수정 가능하지만, 동시에 해묵은 버그가 존재하는 사이 새 버그가 생겨 나기도 하므로, 사용자가 이런 경우에 노출되지 않도록 하는 것이 클라이언트 개발자인 우리들이 해야할 일일 겁니다. RestKit으로 만든 매핑 정보는 우리에게 새롭고도 방대한 분량이었으므로, 앱을 배포할 경우 맞닥뜨릴 수 있는 여러 크래시에 대한 부담과 안정화를 위한 방법 마련에 대한 고민이 컸습니다. 전에도 말했듯 큰 문제가 아니긴 하지만 미지의 경우이기도 하거니와 이 레이어에서 발생하는 오류는 사용자에게 직결될 수 있기 때문입니다.

서버 쪽의 종료 스위치와 모니터링 (6:15)

다음으로 중요한 것은 원격으로 조작할 수 있는 비상 버튼이 필요하다는 것이었습니다. 뭔가 잘못되면 모든 잘못된 코드가 야기한 영향을 서버로부터 차단할 수 있어야 했죠. 저희는 사용자 요청에서 온 boolean 값 세트를 가지고 애플리케이션의 코드를 켜거나 끌 수 있도록 했습니다. 이는 실제 사용 중 잘못되는 것을 알아차릴 방법이 필요하다는 뜻이기도 합니다. 마치 Realm 코드에서 예측하지 못한 상황이 발생할 경우 핑을 보내는 이벤트 트래킹 시스템과도 같죠. 배포 후에 모니터가 가능한 크래시 리포트도 필요합니다. 점진적인 migration을 하는 이 모든 액션들의 요점은 물의 깊이를 잘 재고 안전하게 입수할 수 있도록 하는 것입니다. 만약 서버 쪽의 종료 스위치가 없다면 전체 migration을 한꺼번에 하는 것과 마찬가지로, 이후의 일을 운에 맡기는 것과 다름없죠.

데이터베이스 병행 구축 (7:06)

데이터베이스를 병행 구축하려면 프로젝트 내부의 데이터 모델 역시 병행할 수 있도록 만들어져야 합니다. Core Data에서 User라는 클래스가 있다면 Realm에는 Realm User라는 유저가 있어야 하죠. 중복을 해야하는 점이 맘에 들지 않겠지만 안타깝게도 점진적인 migration을 위해서는 어쩔 수 없이 해야하는 필요악입니다. 새 Realm 서브클래스에 프로퍼티들을 복사하기만 하면 되는 쉬운 일이지만, 이 모델들 안에서 병행한 커스텀 로직을 유지하고 모델에게 메시지를 보내는 실제 코드를 다루는 것은 까다롭습니다. 이에 대한 해결책은 얼마나 오래 데이터베이스들을 병행 유지할 것이냐에 따라 다르겠죠.

개인적으로는 migration에 자신감이 생길 때까지 짧은 구간을 반복한 이후에 Realm으로 깔끔하게 교체하는 것을 추천합니다. 이런 방식으로 두 가지 데이터베이스 모델을 한꺼번에 유지하면서 드는 고통스러운 시간을 줄이고 로직을 추가해도 안정적인 데이터베이스 모델을 만들 수 있습니다.

UI 업데이트를 통합하기 (10:39)

이 단계에서 비교적 안정적이고 트래픽이 낮은 시험용 애플리케이션을 뽑아서 애플리케이션 내의 표준적인 UI 상호작용을 재연하고자 했습니다. 앱에 리스트가 많다면 Realm 데이터베이스로부터 table view를 잘 불러올 수 있는지 확인하고 싶을 겁니다. 탐색 인터페이스 같은 곳에서 다이나믹 쿼리가 많이 일어난다면 현재 Realm의 제공 기능을 쉽게 활용해서 쿼리를 처리할 수 있을지 확인하거나 새 모델에 맞게 변경할 수 있을지 고민하겠죠.

저희는 Realm 데이터베이스로부터 받아온 것들을 보여주고 네트워크 레이어를 지나 응답을 받아서 이를 매핑하는 collection view부터 시작했습니다. 이 시점에서는 아직 애플리케이션의 나머지 부분이 Core Data와 연동되므로 병행 데이터베이스 문제가 아직 남아있는 상태로, Realm 모델을 다루는 인터페이스를 도입하기 시작해야 합니다. 이 때 두 가지 선택지가 있죠.

1) 다른 인터페이스를 위한 View Controller를 복제하기 2) 한 View Controller 내에 조건 스위치를 만들기

둘 모두 썩 좋은 방법은 아니지만 시험적인 기간에만 도입한다면 병행 문제를 처리하기 쉬우므로 나중에는 Realm으로 교체하기 위한 자신감을 얻을 만큼 UI 곳곳에서 다양한 경험을 쌓을 수 있을 겁니다.

정보 처리 상호 운용 (12:49)


@interface RLMObject (CoreDataMatching)
+ (NSString*)coreDataEntityName;
- (NSPredicate*)coreDataMatchingPredicate;
- (id)coreDataEntity: (NSManagedObjectContext*)context;
@end

@interface NSManagedObject (RealmMatching)
+ (NSString*)realmEntityName;
- (NSPredicate*)realmMatchingPredicate;
- (id)realmEntity:(RLMRealm*)realm;
@end

@implementation RLMObject (CoreDataMatching)
- (NSPredicate*)coreDataMatchingPredicate {
  NSString* primaryKey = [[self class] primaryKey];
  id primaryValue = [self valueForKeyPath:primaryKey];
  return [NSPredicate predicateWithFormat:@"%K == %@", primaryKey, primaryValue];
}

@implementation NSManagedObject (RealmMatching)
- (__kindof RLMObject)realmEntity:(RLMRealm*)realm {
  NSString *entityName = [[self class] realmEntityName];
  Class entityClass = NSClassFromString(entityName);

  NSPredicate *predicate = [self realmMatchingPredicate];
  RLMResults *results = [entityClass objectsInRealm:realm withPredicate:predicate];
  return [results firstObject];
}

한번 Realm 모델을 적용한 시험용 앱을 확보했다면 Core Data를 이용하는 앱의 나머지 부분 역시 Realm 모델로 Realm에 데이터를 공급할 수 있어야 합니다. Core Data 모델 리스트를 다루는 Master View Controller가 있고 이 중 하나를 보여주는 Detail View Controller가 있다고 생각해 보죠. Core Data 모델에 접근하면 Detailed View Controller에 전달할 수 있도록 동일 부분의 Realm 엔티티를 가져와야 합니다. 즉, 네이밍 스키마를 만들어야 하죠.

UserRealm User가, GroupRealm Group이 됩니다. 모든 엔티티는 서버에서 부여한 ID로 고유하게 식별할 수 있으므로 다른 데이터베이스의 동일 부분 객체를 찾는 술부를 생성하는 것은 어렵지 않습니다. 위 코드에서는 현재 Realm 모델과 일치하는 Core Data 모델을 찾기 위한 술부를 생성하고 있습니다.

작업 마무리하기 (15:25)

시험 기간을 마무리하는 시점에서 아마 Realm의 멋진 기능에 반해 도입을 결심하게 될 겁니다. migration 기간 동안 고려해야 할 사항을 정리해 봤습니다.

저희 Remind의 팀의 프로덕트 매니저인 Zach는 개발자 출신이 아니었는데, 저는 최종 migration 과정에서 두 가지 큰 잘못을 저질러서 그를 실망시켰죠.

첫 번째 잘못은 migration을 완성하는데 얼마나 많은 시간이 들지를 과소평가한 점이었습니다. 아래 내용을 잘 고려했어야 했습니다.

  • 여러 애플리케이션에서 많이 사용하는 Core Data 기능 중 몇 가지는 Realm으로 빠르게 교체할 수 없습니다. 예를 들어 아직 중첩 키패스 정렬을 지원하지 않습니다.

  • Core Data와는 달리 캐스캐이딩 객체 삭제를 라이프 사이클 내부에서 할 수 없습니다.

  • 주로 변경 가능한 프로퍼티에 저장하는 딕셔너리처럼 구조 정립이 되지 않은 데이터를 Realm에 저장하기 위해서는 어떤 식으로든 구조화하거나 시리얼라이즈해야합니다. 단, 맨 처음 언급한 것처럼 데이터의 구조가 복잡하지 않아야 합니다.

두 번째 잘못은 migration에 두 배의 시간을 들인 점입니다. 저는 시험용 앱에서 사용한 접근법인 종료 스위치 이전에 두는 방식을 사용하면서, 모든 레퍼런스에 영향을 주는 인터페이스 하나를 업데이트 했습니다. 마치 램프 하나를 갈려다 자동차 전체를 수리해야 하는 문제처럼, 인터페이스에 연관된 모든 단계마다 계속 번져가는 문제를 수정해야 했습니다. 나중에는 어디서 문제가 시작됐는지도 가물가물할 정도가 됐죠. 하지만 결국은 모든 Core Data 모델을 분리해내고, 줄줄이 이어진 레퍼런스를 처리하고, Core Data를 다루는 모든 코드를 없앨 수 있는 시작점을 찾을 수 있었고, 여러분도 그럴 겁니다.

요약 (22:57)

정리하자면 Core Data 패턴을 깊게 적용해서 사용하고 있는 애플리케이션이라도, 앞서 말한 시험 기간을 두면서 점진적으로 migration을 한다면 의구심없이 안전한 교체가 가능하다는 점입니다.

모든 것을 종료 스위치 이전에 두고, Realm에서 UI 패턴을 테스트하면서, 각 배포 이후의 결과를 모니터링하고, Core Data를 완전히 버릴 준비가 되면, 제 실수를 반면교사 삼아 손쉽게 migration을 하길 빕니다. 자신의 코드를 기반으로 얼마나 시간이 걸릴지 잘 측정해서 실행하세요. 제가 말씀드린 반복 과정을 통해 예측 불가능한 장애를 잘 넘어선다면, 결과적으로는 앱의 구조를 근본적으로 바꾸는 변화를 마주하게 될 겁니다.

Q&A (24:00)

Q: migration을 한 보람이 있나요?

Core Data에 비해 Realm의 인터페이스가 얼마나 나은지 생각한다면 정말 migration을 할만 합니다. Core Data 사용을 위해 쓰던 RestKit 때문에 기존의 저희 앱은 많이 불안정했고, Core Data에서의 오류는 예측하기도 어려웠죠. 뭘 잘못했는지 알아낼 방도도 없었기 때문에 일정 비율의 Core Data crash를 항상 겪곤 했습니다. 이때문에 Realm으로 교체하기로 했고요. crash 비율과 안정성 개선을 모색한 방법이 Realm으로의 교체였습니다.

Q: migration 과정은 얼마나 진행됐나요?

전환을 할 준비는 마쳤으며, 현재 Core Data로부터 받는 미세한 notification과 관련된 두세가지 문제만 남겨둔 것만 빼면 Realm으로의 교체가 코 앞에 있습니다. 이 문제를 해결하면 거의 끝난 셈이죠.

다음: iOS를 위한 Realm #2: iOS 검색 컨트롤러를 Swift로 구축하기

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.

Alex Leffelman

Alex is a Software Engineer at Remind. When he joined in November 2013, he doubled the size of the iOS team. His previous work includes two years developing Zynga Poker’s iOS app, as well as a year designing note tracks for several Guitar Hero titles at Neversoft Entertainment. A proud native Wisconsinite, Alex enjoys all things beer, cheese, and Badgers.

4 design patterns for a RESTless mobile integration »

close