성능 향상과 메모리 사용량 최소화에 최적화된 Realm API를 소개합니다.

Realm Database series header

이 글은 Realm 모바일 데이터베이스에 대한 시리즈 글입니다. 여러 유명한 프로그래밍 언어에서 데이터베이스를 사용할 수 있는 기초적인 클래스들에 대해 알아봅니다. 혹시 시리즈 시작 글을 놓치셨다면 첫 번째 글을 먼저 보시길 추천합니다. 플랫폼 간 코드 공유를 다룬 세 번째 글Realm처럼 유연하고 견고한 SDK가 어떻게 좋은 개발을 돕는지에 대한 네 번째 글도 번역됐습니다. 그 다음 시리즈에 대한 한글 번역은 진행 중으로, 미리 보고 싶은 분들은 영문 원문인 작은 단위 알림을 기반으로 부분적인 UI 업데이트를 제공할 수 있게 하는 Realm SDK를 다룬 다섯 번째 글, 마지막으로 이같은 기능이 모여 어떻게 최신 모바일 애플리케이션을 만드는지에 대한 여섯 번째 글을 참고해 주세요.


Realm 모바일 데이터베이스는 모바일 디바이스 사용 목적에 맞게 설계된 모바일 데이터베이스로, 기초부터 다시 설계된 만큼 효율적입니다.

Realm 모바일 데이터베이스의 뛰어난 성능을 보이는 이유 몇 가지를 지난 글에서 알아봤었죠. 다음과 같습니다.

  • 서드 파티 스토리지 엔진을 사용하지 않습니다.
  • SQL과 같은 중간 쿼리 언어를 사용하지 않습니다.
  • 디스크에서 데이터를 읽거나 쓸 때 데이터를 상호 변환하지 않아도 됩니다.

앞서 말했듯, Realm 모바일 데이터베이스에는 두 가지 차별성이 있습니다. C++ 코어와, 여러분이 애플리케이션을 개발할 때 사용하는 언어로 된 SDK이죠.

더 좋은 성능과 강력한 코드를 만드는데 도움이 되는 Realm 모바일 데이터베이스의 SDK단에서는 어떤 일을 할까요? 이번 글에서 몇 가지 일반적인 패턴을 살펴보겠습니다.

가져온 Results Controller를 누가 사용할까요?

모바일 개발에서 흔하게 사용하는 패턴은 사용자의 연락처, 캘린터 항목, 트윗 목록 등 긴 리스트를 보여주는 겁니다.

“기존” 스토리지를 사용할 경우, 보통 디스크에서 항목들을 메모리 상의 배열로 복사하고, 모델을 복제하는 경우가 많습니다. 주의깊게 배열을 복사하지 않으면 세 배로 늘릴 수도 있죠.

정말 긴 리스트를 다루는 경우라면 모든 데이터를 메모리에 올리기보다는 더 빠르고 효과적인 방법을 사용하는게 좋을 겁니다. 아마 백그라운드에 레코드의 페이지를 디스크로부터 로드하거나 배치하는 코드를 두고 현재 배치만을 메모리로 복사하는 방법을 사용하겠죠. 그런 다음 사용자가 리스트를 얼마나 스크롤했는지 추적하면서 적절한 때 배치를 더 로드할 겁니다.

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

이런 상황에서 자주 사용되는 방법은 Apple 플랫폼의 NSFetchedResultsController를 사용하는 것입니다. 하지만 이는 데이터를 미리 가져오는 클래스로 SQLite 데이터베이스의 맨 위에서 실행되는 ORM의 최상단에서 동작합니다. 개발자와 개발자의 데이터간의 중개자가 아주 많은 형국이죠.

results controller 사용 앱, ORM 사용 앱, SQLite 사용 앱

반면, Realm 모바일 데이터베이스를 사용하면 Results 클래스를 사용해서 직접 디스크 상의 오브젝트를 쿼리하고 필요한 순간에 실제 사용하는 결과 세트만을 로드하게 됩니다.

어떤 장점이 있나 살펴볼까요?

먼저 오브젝트 쿼리를 정의해도 디스크로부터 어떤 오브젝트도 불러오지 않습니다. 아래 예제를 보시죠.

realm.objects(Repository.self)
  .filter("name contains[c] %@", searchTerm ?? "")
  .sorted(byProperty: "stars", ascending: false)

이 코드는 데이터베이스에 대한 일종의 뷰포트 를 정의합니다. 위 코드에서는 주어진 검색어를 포함한 이름을 가진 Repository 객체와 별 속성을 보려 합니다.

Realm은 실제로 결과 집합 내의 객체를 순회하기 시작하는 시점에 디스크에서 데이터를 가져오며, 해당 객체에 대한 쿼리를 다시 실행할 필요가 없습니다.

Results를 사용하면 실제로 필요한 데이터만 사용할 수 있도록 가져옵니다. 물론 데스크로부터 분리된 바이트를 읽을 수 있다는 것은 아닙니다.

그래서 UI 테이블뷰에 Results 클래스를 사용한다면, 현재 보이는 테이블의 행을 표시할 데이터만을 실제로 디스크에서 읽어오게 됩니다.

가장 좋은 점은 이런 데이터 로딩을 위해 별다른 노력이 필요하지 않다는 점입니다. 사용자가 몇 백, 몇 천의 행을 가진 테이블뷰를 스크롤하더라도, Results는 현재 시점에서 화면에 보이는 대여섯개의 행에 표시하는 데이터만 메모리에 유지합니다.

혁신적인 리스트 관리

이제 Realm 모바일 데이터베이스 API 중 List에 대해 알아보겠습니다. List 클래스는 객체간의 일대다 관계를 위해 사용합니다. 아래 예제를 보시죠.

class User: Object {
  dynamic var name: String = ""
}

class Repository: Object {
  dynamic var repository: String = ""
  let contributors = List<User>()
}

SQL 데이터베이스에서 이런 관계를 많이 다뤄봤을겁니다. SQL 데이터베이스이라면 usersrepositories, 두 테이블 사이를 연결하는 추가적인 테이블이 필요하죠. 특정 저장소에 기여하는 사람들을 알고 싶을 때마다 데이터베이스에 있는 모든 저장소와 사용자 간의 모든 관계를 탐색해서 원하는 것을 찾아야 합니다.

몇 개의 기능만 더해도 아래처럼 얽힌 관계의 테이블이 생겨나며, 보다 복잡한 SQL 데이터베이스라면 이런 관계는 더 얽히게 됩니다.

Tangled relational database tables

따라서 EasyAnimation이라는 저장소에 공헌한 두 명의 사용자가 있는 경우에 그들의 정보를 알고 싶다면, usersrepositories 내의 모든 레코드를 링크해서 찾아내야 합니다. 얼마나 많은 저장소와 사용자가 있는지에 따라 관계 인덱스는 수백, 수천을 넘어 수백만 개가 될 수도 있습니다. 게다가 이를 가져오고 싶을 때마다 실제로 가져올 두 개의 레코드를 매번 찾아내는 작업을 해야 하죠.

한눈에 봐도 그다지 효율적인 방법은 아닙니다. 그래서 대용량 데이터베이스를 다루는 팀은 성능 저하를 막기위해 팀 리더가 데이터베이스 쿼리의 JOIN절에 대한 권한을 따로 부여하기도 합니다.

반면 Realm 모바일 데이터베이스는 테이블을 join하지 않으며 SQL 쿼리도 사용하지 않습니다. Realm 모바일 데이터베이스는 메모리에 있는 다른 객체와 거의 동일합니다. 한 객체가 다른 객체를 가리킬 때 부모는 일반적으로 자식의 데이터를 복사하지 않고 단순히 포인터만 유지하죠.

따라서 Realm 모바일 데이터베이스를 사용하면 Repository 객체의 두 명의 공헌자를 가지고 오는 것은, 두 개의 다른 객체, 사용자를 직접적으로 가리키는 두 개의 포인터를 단순히 유지하는 것과 같습니다. 원하는 때 언제든지 사용할 수 있고, 별다른 조회나 관계, 인덱스를 사용할 필요가 없습니다.

Realm relationships

List는 일대다 관계를 정말로 효율적으로 만들어줍니다. Array와 거의 비슷하게 작동하며, 객체가 추가된 순서를 유지하고, 객체를 특정 인덱스에 추가, 제거하거나 이동하는 메서드도 제공합니다. 또한 List를 생성하는데는 비용이 거의 들지 않습니다. 객체 자체는 다른 객체에 대한 직접적인 인덱스의 목록만 저장하며, 인덱스의 저장 비용이 매우 저렴하기 때문이죠.

즉, 창의적인 방법으로 List를 사용해서 앱의 성능을 크게 증가시킬 수 있습니다.

이번에는 공헌자 리스트를 가지고 있는 Repository 객체가 있고, 공헌자 중 일부가 승인과 접근 권한을 받기 위해 기다리는 경우를 한 번 생각해 보겠습니다.

앱의 UI에서 대기 중인 공헌자와 승인된 공헌자, 두 개의 리스트를 보여줘야 합니다. 다른 데이터베이스에서 사용하던 방법대로라면 아래 코드처럼 작성해야겠죠.

// this is the slow old way
class User: Object {
  dynamic var name: String = ""
  dynamic var isPending: Bool = true
  dynamic var dateAdded: NSDate = NSDate()
}

class Repository: Object {
  dynamic var repository: String = ""
  let contributors = List<User>()
}

//display pending users
realm.objects(Repository.self).first!
  .contributors
  .filter("isPending = true")
  .sort(byProperty: "dateAdded", ascending: false)
  
//display approved users
realm.objects(Repository.self).first!
  .contributors
  .filter("isPending = false")
  .sort(byProperty: "dateAdded", ascending: false)

다시 말하지만, Realm 모바일 데이터베이스를 사용하면 이런 방식을 사용할 필요가 없습니다.

위 코드는 SQL 데이터베이스에 필요한 방식으로, 같은 테이블을 계속해서 필터링해야만 하므로 주로 사용하게 되는 모습입니다. 그러나 다시 한 번 생각해 보시죠. 화면에 보여줄 때마다 이 객체들을 계속 필터링하고 정렬해야만 할까요?

List를 사용하면 이럴 필요가 없습니다. 하나는 보류된 공헌자, 하나는 승인된 공헌자를 담는 두 목록을 독립적으로 유지하면 어떨까요?

사용자를 한 번 이상 필터링하거나 정렬할 필요가 없으며 쿼리를 느리게 만드는 두 가지 작업인 필터링과 정렬에서 해방됩니다.

리스트 내에서는 이미 필요한 순서대로 필터링된 객체를 순회하게 됩니다. 즉, 두 리스트에 들어갈 객체를 필요한 방식대로 미리 필터링하고 미리 정렬하는 작업을 단 한 번 수행합니다. 그 다음 데이터를 표시할 때 컬렉션을 순회하면 됩니다.

그럼 이런 효과적인 설정 방법을 살펴볼까요? dateAdded 속성이 정렬에만 사용된다면 안전하게 제거할 수 있습니다. isPending이 유지될 수는 있지만 필터링을 위해서 사용되지는 않으며, 필요한 시점에서 User 객체의 상태를 결정하기 위해서만 사용합니다.

// the easy Realm way
class User: Object {
  dynamic var name: String = ""
  dynamic var isPending: Bool = true
}

class Repository: Object {
  dynamic var repository: String = ""
  let approvedContributors = List<User>()
  let pendingContributors = List<User>()
}

물론 이후로는 공헌자를 다시 쿼리할 필요가 없고, 이미 만든 두 리스트만 순회하면 됩니다. 새로운 User 객체가 추가되면 pendingContributors에 넣고, 사용자가 승인되면 pendingContributors에서 삭제한 후 approvedContributors에 넣으면 됩니다.

// user requesting repo access
let user = realm.object(ofType: User.self, forPrimaryKey: id)
try! realm.write {
    repo.pendingContributors.append(user)
}

//later, when the user is approved
try! realm.write {
  user.isPending = false
  if let index = pendingContributors.index(of: user) {
    pendingContributors.remove(objectAtIndex: index)
  }
  approvedContributors.append(user)
}

생성과 유지에 큰 비용이 들지 않으므로 부담없이 동일한 Repository 객체에 추가적인 리스트를 만들 수도 있습니다.

마지막으로 리스트가 기존 객체에 대한 직접 링크만을 제공하므로 성능 저하 없이 여러 개의 리스트에 같은 객체를 넣을 수도 있습니다.

예를 들어 아래 그림처럼 같은 사용자 객체가 저장소 객체 내의 승인 사용자 리스트와 커밋 히스토리 리스트에 존재할 수 있으며, 이들 리스트가 앱의 각기 다른 화면에서 쓰일 수도 있습니다.

Lists powering view controllers

Realm의 리스트의 원리를 설명드렸으니 저장소의 커밋을 화면에 띄우는데 무엇이 필요한지 마지막으로 SQL 테이블과 비교해보겠습니다.

SQL을 사용하면 사용자가 커밋 리스트 화면을 열 때마다 모든 커밋 테이블을 검토하고, 모든 레코드를 필터링해서 현재 저장소를 찾고, 결과와 사용자 테이블을 다시 일치시켜야 하며, 마지막으로 이들 레코드를 다시 정렬해야 합니다.

List를 사용하면 Repository의 커밋 리스트에서 처음 몇 개의 객체를 읽어서 화면에 표시하면 됩니다. 간단하죠? 👌

List 클래스는 모바일 UI에서 자주 사용되는 아이템 목록을 강화하기 위해 설계됐습니다. 다른 데이터베이스나 저장 도구를 사용하던 방식에서 벗어나 객체 및 객체 그래프 측면에서 데이터를 생각한다면 이 클래스의 강력한 기능을 온전히 활용할 수 있습니다.

마지막으로, 이 글에서는 Swift Realm SDK를 사용했지만, 효율적인 객체 쿼리 및 객체 리스트는 Realm 모바일 데이터베이스 코어에서 구현되는 기능므로 다른 플랫폼과 프로그래밍 언어에서도 동일하게 작동합니다.

연재되는 다음 글에서는 Realm 모바일 데이터베이스 SDK에서 제공되는 다른 여러 클래스를 살펴보고 어떻게 빠르고 효율적인 모바일 개발을 할 수 있는지 돕는지 살펴 보겠습니다.

글이 마음에 드셨나요?

Realm 모바일 데이터베이스가 최첨단 앱을 구현하면서 마주치는 문제들을 해결하도록 돕는 여러 가지 방법 중 하나를 간략하게 알아 봤습니다.

더 많은 정보를 원한다면 Realm 모바일 데이터베이스 관련 내용을 살펴 보세요.

다음 편에서는 Realm 모바일 데이터베이스 API로 간결한 소프트웨어 구조 설계하기와 최신 베스트 프랙티스에 대해 다루겠습니다.

이 모듈 코드를 활용하고 한 프로젝트에서 tvOS, iOS, macOS, watchOS 간에 코드를 공유하는 멀티플랫폼 프로젝트를 볼 수 있습니다.

다음 편에서 만나요! 👋

다음: Realm의 차별화 요소들 #3: Realm은 Apple 시스템과 안드로이드를 넘나드는 멀티 플랫폼 데이터베이스입니다.

General link arrow white

컨텐츠에 대하여

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


Marin Todorov

Marin Todorov는 iOS 컨설턴트이자 퍼블리셔입니다. “iOS Animations by Tutorials”의 저자이며 iOS Animations by Emails” 뉴스레터를 운행하고 있습니다. 20년 넘게 Apple 관련 개발을 했으며 Monster Technologies와 Native Instruments 등의 회사에서 일하면서 4개 이상의 나라에서 거주했습니다. 또한 raywenderlich.com 튜토리얼 팀의 설립 멤버이기도 하죠. 코드 개발 이외에는 블로그 게재와 책 저술, 교육과 강연에 관심이 많으며, 코드를 오픈 소스화 하기도 합니다. Santiago 순례 경험도 있습니다.

4 design patterns for a RESTless mobile integration »

close