코어데이터(Core Data)와 데이터베이스의 차이

애플의 Core Data Programming Guide 문서에는 코어데이터(Core Data)가 데이터베이스가 아니라고 명시되어있다. 하지만 코어데이터와 데이터베이스 둘다 검색가능하고, 영속적인 저장소를 제공하는 방법이므로 구체적으로 무엇이 다른지는 명확하지 않다. 이 포스트에서는 코어데이터가 동작하는 방법을 살펴보면서 왜 코어데이터가 일반적인 SQL 데이터베이스와 다른지 비교해 볼 것이다(코어데이터를 사용하더라도 실제 뒷단의 저장소는 SQL 데이터베이스가 사용되는 경우도 있다).

소개

코어데이터와 SQL 데이터베이스 모두 구조화된 데이터를 검색가능한 저장소에 저장하는 수단을 제공해준다. 일반적으로 개발자들이 데이터베이스에 익숙하고, 코어데이터가 실제로 뒷단에서 SQLite 데이터 베이스를 사용하여 데이터를 저장하는 경우도 있기 때문에 코어데이터가 마치 SQLite의 wrapper인 것처럼 생각되기 쉽다.

하지만 코어데이터의 경우 사실상 SQLite와는 다른 도메인에서 동작하는 기술이기 때문에 SQLite에서 지원하는 기능들을 지원하는 경우도 있고, 반대로 SQLite에 없는 코어데이터 고유의 기능을 가지고 있기도 하다. 또한 코어데이터와 SQLite 둘다 동일하게 지원하는 기능들이라도 성능상의 차이가 있을 수 있다.

데이터베이스의 주된 기능

다소 좁은의미의 서술이긴 하지만 본 글에서 데이터베이스는 영속적이고 검색가능한 테이블구조의 행/열로 구성된 데이터 저장소라고 의미를 부여할 것이다. 이 저장소의 주된 목적은 디스크에 항상 최신의 데이터를 저장하는것이며, 데이터를 불러오고 업데이트 하는 것이 두번째로 중요한 목적이다.

이러한 주된 기능들을 훨씬 넘어서는 고급 데이터베이스 구현들이 많이 있지만, 대부분의 개발자들이 익숙해하는 SQLite 스타일 데이터베이스가 가지는 핵심 기능들에 초점을 맞추도록 하자.

비록 많은 데이터베이스들이 관계형이라고 불리지만, SQLite와 많은 다른 관계형 데이터베이스들은 실제로 객체들의 직접적 연결을 다루지는 않는다. 테이블간의 행/열에 관한 관계를 유지하는 것은 데이터베이스를 사용하는 사용자가 해야한다. 이러한 관점에서 데이터베이스는 “멍청한(dumb)” 저장소이다. 테이블의 열(rows)을 다룰 경우 읽기/쓰기 이상의 동작이 거의 전부이고 이를 확장하거나 커스터마이징하기 위해서는 결국 데이터베이스 시스템 자체를 확장하게 될수밖에 없다. 트리거(trigger)기능이 지원되더라도 프로그램 가능한 범위는 제한될 수 밖에 없다.

코어데이터의 주된 기능

코어데이터의 본질은 라이프사이클, 검색, 영속성 기능을 가진 객체 그래프 관리자(object graph manager)이다. 객체 그래프 관리는 다음을 포함한다:

  • 객체 A를 객체 B와 연결할 수 있으며, 해당 연결은 영속적으로 동기화 된다. A쪽에서 연결을 변경하면, B가 업데이트 되면서 그에따른 알림(notification)을 발생시킨다(이 알림에 대해 임의의 코드를 짜넣어서 실행하는 것이 가능).
  • 한쪽에서 객체를 삭제할 경우 연결을 타고 cascade 삭제가 일어나도록 할수도있고, nullify 시켜서 해당 객체만 삭제 할 수도 있다.
  • 연결된 객체가 로딩되는 순간에 바로 연결에 접근한 것이 아니라면, 연결의 반대편에 있는 객체가 out of memory (faulted) 상황에 빠져있을 수도 있다.

보통의 데이터베이스들과는 다르게 코어데이터는 완전한 인메모리(in-memory) 형태로 사용이 가능하다. 일반적으로 사용자들이 영속성 특성을 대부분 사용하기 때문에 코어데이터가 “객체 영속성(object persistence)” 프레임워크라고 불리지만, 실제로는 어떠한 형태의 영속성도 지니지 않은 in-memory형태로 사용하는 것이 가능한 것이다. (역자주: 코어데이터는 명시적으로 저장 명령을 내릴때까지 디스크에 저장하지 않는다.)
즉, 코어데이터를 사용할때 “영속성”이 의무적인 기능이 아니란 것을 알아두는 것이 중요하다.

또한 코어데이터를 어떠한 형태의 검색기능 없이도 사용가능하다. 일단 객체들이 할당되고 연결되었을 경우, 한 객체에만 접근이 가능하다면, 추가적인 불러오기(fetch) 없이 해당 객체로부터 나머지 연결된 객체들을 타고 넘어가면서 접근이 가능하다. 일단 데이터들이 메모리에 로딩되면, 연결고리를 따라 이동하는 것은 검색 없이도 가능하기 때문에 코어데이터가 이러한 비검색(seachless) 특성을 갖게된다.

모든 코어데이터 객체들은 완전히 인스턴스화된(fully instantiated) Objective-C 객체이며 속성값과, 관계, 라이프사이클을 관리하는 것이 가능하다. 이것은 또한 객체들의 속성들(property)과 동작들이 메서드(method)에 의해 구현될 수 있음을 의미하며, 이러한 메서드들은 서브클래싱을 통해 옵저브와 오버라이드가 가능하다(observable & overridable).

데이터베이스 vs. 객체 그래프 관리

데이터베이스와 코어데이터의 객체 그래프 관리자 기능은 완전히 배타적인 관계는 아니다. SQLite에서는 기본적으로 외래키(foreign key)가 지원되지 않지만, MySQL 등의 다른 데이터베이스 들은 여러 테이블들에 대해 identifier들을 관리하여 관계가 설정된 레코드들간의 싱크를 맞출 수 있으며 심지어 cascade 삭제 또한 가능하다. 즉 오버라이드 가능한 객체들을 이용하여 코드를 작성하는 커스터마이제이션은 불가능하지만 기본적인 수준의 관계 관리는 가능하다는 의미이다.

코어데이터의 모델과 비슷한 다른 객체 관계 프레임워크들이 존재하긴 하지만 대부분 원자성(atmomicity)을 보장하며, 트랜잭션(transaction)기반의 데이터베이스들이다. 이런 프레임워크들이 객체 그래프를 업데이트 하기 위해서는 다음과 같은 절차를 따른다.

  • 데이터베이스로부터 적절한 레코드 행(row)을 불러온다
  • 이러한 레코드들로부터 객체 인스턴스를 생성한다
  • 인스턴스화되어 메모리상에 존재하는 그래프 객체들을 수정한다
  • 수정된 내용들을 다시 데이터베이스에 반영(commit) 한다.

원자성을 보장하기 위해서는 위의 4가지 절차가 하나의 트랜잭션으로 수행되어야 한다. (즉, 트랜잭션이 진행중일때는 다른 읽기/쓰기 작업이 레코드에 영향을 주면 안된다.) 이러한 원자성 보장이 요구되는 시스템들도 있긴하지만, 이 방법은 일반적인 객체 그래프 시스템에 대해 적용할 경우 많이 느릴수밖에 없다.

따라서 더 일반적인 객체 관리 시스템을 위해 설계된 코어데이터는 더 나은 성능과 유연성을 위해 이 모델을 따르지 않는다.

인메모리(in-memory) DB vs. 디스크(on-disk) DB

실제 코어데이터의 소스코드에 접근할 권한이 없기때문에 완벽하게 내부적인 동작을 알수는 없지만, NSManagedObjectContext가 힙(heap)에 존재하는 객체 인스턴스들 혹은 나중에 다시 접근 가능한 형태의 구조화된 컨테이너(structured container)들을 트래킹(tracking)하고 있을거라 추측할 수 있다.

이러한 코어데이터의 트래킹 구조는 인메모리(in-memory) 데이터베이스와 비슷하게 동작하지만 인스턴스화 된 객체들을 트래킹하는 특징을 가진다. 또 한가지 주목할 점은 중앙집중적인 NSManagedObjectContextNSManagedObject인스턴스들을 다루기 위해서 NSManagedObject 포인터에게 메시지를 보내는 방식을 사용한다는 점이다.

코어데이터는 처리속도를 위해 이러한 인메모리 방식에 초점을 맞추고있다. 객체 그래프의 변화로인해 연결된 여러 객체들이 수정될 필요가 있을 때, 모든 객체들이 메모리에 이미 올라와있으면 훨씬 빠르게 작업이 가능하다 (데이터베이스에서 해당 내용을 검색해서 불러오는것은 느릴 수 밖에 없음).

디스크에 영속적으로 저장될 필요 없이 임시 내용을 담는 객체가 필요한 경우, 데이터베이스에 비해 코어데이터가 훨씬 빠른속도로 객체를 생성/수정/조작이 가능하다. SQlite의 경우 관련 인덱스와 B-tree의 노드를 업데이트해야 하기때문에 속도가 더 느리다. 코어데이터가 수초동안 수백만개의 객체들을 생성 가능한데 반해, SQLite가 수백만개의 객체 생성을 하려면 분단위의 시간이 걸린다.

인메모리 방식의 코어데이터를 사용하더라도 실제로 데이터의 영속성을 위해 저장소(backing store)로 SQLite를 많이 사용한다. 이 경우 디스크에서 읽기/쓰기를 할때의 SQLite의 기본적인 오버헤드에 코어데이터와 SQLite 간의 컨버전을 위한 오버헤드가 추가되서 느려지는 단점이 존재한다.

데이터베이스에서는 할 수 있지만 코어데이터가 하지못하는 작업

코어데이터가 데이터베이스에 비해 좋은 점들도 있지만, 반대로 부족한 점들도 존재하기 때문에 이부분을 짚고 넘어가는 것이 중요하다.

코어데이터는 데이터들을 메모리에 로딩하는 과정 없이는 작업이 불가능하다.

SQL구분에서 테이블을 삭제하거나 레코드들을 업데이트하기 위해서 “DROP tableName” 이나 “UPDATE tableName SET key1 = value WHERE key2 = otherValue” 명령이 이용된다. 명령 처리를 위해서 각 레코드에 해당하는 작은 크기의 데이터만 메모리에 로드를 하면 되기때문에, 데이터의 양이 많더라도 효율적으로 업데이트가 가능하다.
하지만 코어데이터는 메모리상의 객체를 수정하는것만이 가능하기 때문에 이러한 온디스크(on-disk) 방식의 사용이 불가능하다. 심지어 코어데이터는 객체를 삭제 할 때도, 일단 인스턴스화 시켜서 메모리에 로드를 먼저 해야 삭제가 가능하다. 객체에 추가적으로 오버라이드된 동작들이 로드되고 실행되기 위해서, 그리고 다른 객체와의 연결정보를 최신으로 유지하기 위해서는 이러한 메모리 로드작업이 필수적일 수밖에 없다.

따라서 코어데이터에서 많은 수의 객체들(수만개 혹은 그 이상)을 수정할 경우 다음과 같은 방법들을 사용하여 메모리 사용량(memory footprint)을 최소한으로 유지할 필요가있다.

  • refreshObject:mergeChanges:메서드를 이용하여 주기적으로 변하지 않은 객체들을 해제
  • NSFetchRequest사용시 setIncludesPropertyValues:NO 설정을 이용하여 객체의 전체데이터를 불러오는 것을 피하기
  • 전체 컨텍스트를 저장한 후, 로드되어 있던 모든 객체들을 릴리즈

코어데이터는 데이터 로직을 다루지는 않는다.

SQL에 존재하는 저장되는 데이터를 제약할 수 있는 “unique” key 같은 기능이 코어데이터에는 포함되어있지 않다.

코어데이터로부터 생성된 모델클래스를 상속받아서 사용할경우, 코어데이터의 attribute에대해 getter/setter를 오버라이드 할 수 있다보니 코어데이터 입장에서는 이것이 unique key 인지 아닌지 알 수가 없다. 결국 이러한 제약조건들이 코어데이터에서 제어가능한 도메인 밖에 있다보니 모델에 적용하려면 직접 비지니스 로직상에서 따로 구현해야 한다.

멀티스레드(Multi-threaded), 멀티유저(multi-user) 시나리오

코어데이터는 멀티스레드가 지원되지 않는다. SQLite 또한 싱글 스레드만 지원하지만, 다른 많은 데이터베이스들은 멀티스레드/멀티유저를 지원한다.
일반적으로 스레딩을 위한 락(lock)을 사용하지 않을경우 프레임워크는 훨씬 빠르고 심플하게 구현될 수 있으며 싱글유저 환경(데스크탑 또는 아이폰)에서 발생할 수 있는 일반적인 시나리오들을 충분히 실현 가능하기때문에 코어데이터는 멀티스레드 기능이 빠져있다. 하지만 여전히 특수한 경우에는 멀티스레드를 이용하여 데이터를 읽어올 필요가 있다. NSManagedObject들과 NSManagedObjectContext의 경우는 싱글스레드에서만 접근되어야만 하므로, 만약 다른스레드가 같은 데이터에 대해 접근해야 한다면 기존 스레드에서 데이터를 저장 한 후, 다른 스레드에서 새로운 NSManagedObjectContext를 이용하여 데이터를 읽어들여야 한다.

요약

데이터베이스(Database)) 코어데이터(Core Data)
주된 기능은 데이터 저장/불러오기 주된 기능은 객체 그래프 관리(하지만 디스크로의 읽기/쓰기 또한 중요한 기능들 중 하나)
디스크에 저장된 데이터에 대해 작업 (필요에 따라 처리할 데이터만 최소한으로 메모리에 로드) 메모리상에 로드된 객체에 대해 작업 (디스크로부터 lazy loading이 가능하긴 함)
멍청한(dumb) 데이터를 저장 스스로 관리되는 완전한 객체를 다루며 서브클래싱을 통해 커스터마이즈된 동작 정의 가능
Transactional, Thread safe, multi-user Non-transactional, single threaded, single user
메모리에 로딩할 필요없이 테이블 삭제 및 편집 가능 무조건 메모리에 로드해야 작업 가능
디스크에 영속적으로 저장 (에러에 강인함) 별도의 저장 프로세스 필요
수백만 레코드를 생성하려면 오랜시간 소요 메모리상에 객체 생성은 매우 빠르게 가능(디스크에 저장 하는 작업은 동일하게 오래 걸림)
“unique” key 와 같은 데이터 제약기능 제공 데이터 제약기능은 프로그램 내부의 비지니스 로직에서 따로 구현해야 함

출처 및 저작권 관련 정보

이 글의 저작권은 CocoaWithLove.com을 운영중인 Matt Gallagher 에게 있고, 저자의 동의하에 한국어로 번역되었습니다. 원본 글은 다음 링크를 통해 접근 가능합니다.
Link to original article