CoreData

코어 데이터(CoreData)

코어 데이터는 애플이 코코아 개발 환경을 통해 제공하는 인메모리(In-Memory) 방식의 데이터 관리 프레임워크이다. 코어데이터를 사용하여 예의 데이터베이스 개발 환경과 유사하게 데이터를 읽고 쓰며 수정하고 삭제할 수 있다.

인메모리 방식인 만큼, 코어 데이터에서 데이터를 다루는 모든 작업은 메모리를 기반으로 동작한다. 즉 코어 데이터를 통해 읽고 쓰는 모든 데이터는 원칙적으로 메모리에 로드된 다음에 처리된다. 메모리에 로드되기 때문에 대량의 읽기와 쓰기 작업에 의한 성능저하에 크게 영향을 끼치지 않는다. 대부분의 작업이 영구 저장소(Persistence Storage)에서 직접 처리되고, 효율성을 위해 읽기 목적의 데이터 일부만 메모리에 올려 놓고 사용하는 데이터베이스와는 구분되는 효율적 특성이다. 인메모리방식임에 불구하고 앱이 종료가 되어도 코어데이터 내부적으로는 파일이나 SQLite 같은 영구 저장소에 보조적으로 데이터를 저장할 수 있기 때문에 데이터가 삭제가 되지않는다.

코어 데이터 vs SQLite

SQLite<—–>Core Data
데이터베이스 파일<—–>데이터 모델 파일
테이블<—–>엔터티
컬럼<—–>어트리뷰트
왜래키+조인<—–>릴레이션

코어데이터가 사용하는 데이터 저장구조는 SQLite 와 상당히 유사하다. 코어데이터는 데이터베이스 처럼 구조화된 데이터를 영구 저장소에 저장하고, 이를 검색하거나 정렬할 수 있는 수단을 제공한다. 게대가 데이터를 정규화할 수 있으며, 빠른 검색을 위해 인덱스를 생성할 수도 있다.

코어데이터는 데이터베이스? SQLite의 래퍼클래스?

결론부터 말하면 코어데이터는 데이터베이스로 보면 안된다. 코어데이터는 영구 저장소로 SQLite 대신 바이너리 파일을 사용할 수 있을 뿐만 아니라 영구 저장소를 아예 사용하지 않고 순수하게 인메모리 방식으로 만 사용하는 것도 가능하기 때문에 데이터베이스의 한 종류 혹은 SQLite의 래퍼라고 말하는 것은 어렵다.

코어데이터를 다룰때 사용하는 코드 형식은 DAO 패턴 스타일과 매우 유사하다. SQL 문을 포함한 DB 처리 코드를 모두 DAO 클래스 내부에 숨기고 뷰 컨트롤러에서는 DAO 클래스의 메소드만 호출하는 방식으로 사용했던 것 처럼, 코어 데이터에서도 뷰컨트롤러는 단지 관리 객체 컨텍스트(Managed Object Context) 객체를 통해 필요한 메소드만 호출하면된다. 관리 객체 컨텍스트는 코어 데이터에서 만들어 제공해 주기 떄문에 직접 정의할 필요가 없는 것이 DAO 패턴과 차이점이다.

코어 데이터에서는 각각의 레코드를 관리 객체(Managed Object) 라고 하는데, 이 관리 객체는 VO패턴 유사한 형태로 정의된 클래스 인스턴스에 할당된 상태로 사용된다. 이를 MO 패턴이라고 한다. MO 클래스의 프로퍼티를 엔터티의 각 어트리뷰트와 직접 연결 시키는 방식을 사용하는데 이 방식을 ORM 매핑이라고 한다.

ORM 매핑 “Object - Relational Mapping” 데이터베이스와 객체 지향 프로그래밍 언어 사이의 비호환 데이터를 반환하는 프로그래밍 기법

코어 데이터에 대한 이해

객체 그래프 관리자(Object Graph Manager)

애플 공식문서에 따르면 코어 데이터는 “애플리케이션에서 모델(Model) 계층의 객체를 관리하는 데 사용하는 프레임워크이자, 라이프 사이클이나 영속성 관리를 위한 기능을 제공하는 객체 그래프 관리자(Object Graph Manager” 로 정의된다.

객체 그래프(Object Graph)란?

코어 데이터는 영구 저장소에 저장된 각각의 레코드를 읽어들인 다음, 독립적인 객체 형태로 만들어 낸다. 데이터를 다루는 행위는 코어 데이터에서 모두 객체 단위로 이루어진다. 이때 레코드의 데이터가 객체화된 것을 가리켜 ‘관리되는 객체(Managed Object)’, 또는 ‘관리객체’라고 부른다.

정규화된 데이터 객체는 다른 객체와 참조 관계 하에 있으며, 서로간의 관계를 통해 데이터의 완전성을 보장받을 수 있다. 이때 객체를 하나의 노드로 간주하고, 서로 간의 연관 관계를 릴크로 이어보면 다양하게 연결되는 복합적인 그래프 형태의 도형을 얻게 되는데, 이것이 바로 객체 그래프이다.

image

코어 데이터가 객체 그래프의 관리를 담당하는 것은, 객체 A를 객체 B와 연결할 수 있으며 이 연결을 통해 A와 B는 영속적으로 동기화 된다 는 것을 뜻한다. 객체 A 에서 업데이트가 발생하면 이에 연결된 객체 B에서도 연간된 데이터의 업데이트가 수행된다. 또한 한쪽에서 객체를 삭제하면 연결을 타고 연이어 객체 객체 B에서도 관련된 데이터의 삭제가 발생하도록 처리가 가능하다.

코어데이터는 전체적으로는 데이터베이스와 비슷한 기능을 제공하지만, 엄밀하게 말해 데이터 저에 관련된 기능을 제공하는 프레임워크라고 할 수 있다. 또한 데이터를 객체로 다루며, 정규화된 데이터 사이의 참조 관계를 바탕으로 한쪽 객체에서 발생한 변경 내용을 다른 쪽 객체에서도 전파하는 등 객체 간 관계의 일관성을 유지하는 객체 그래프 관리자로서의 특징을 가진다.

코어 데이터의 구조

코어 데이터는 다층 구조로 이루어진 프레임워크로, 각 층을 담당하는 핵심 객체들이 서로 밀접한 연관성을 가진 채 상호작용한다. 전체적으로 코어 데이터는 개발자와 영구 저장소 사이를 이어주는 프레임워크이기 때문에 FMDB 라이브러리와도 일맥 상통하는 브븐이 많다.

관리 객체(Managed Object)

관리 객체는 코어 데이터에서 데이터를 저장하기 위해 생성하는 인스턴스이다. 관계형 데이터베이스에서 테이블의 행이나 레코드와 유사하다 코어 데이터는 모든 레코드를 객체화하여 다루기 때문에, 독립된 객체로 동작한다. 이때, 레코드를 구성하는 각 칼럼들은 관리 객체의 속성이 된다.

레코드 두개를 읽기 위해서는 두개츼 관리 객체가 필요하고, 새로운 레코드를 추가할 때 역시 데이터를 담을 관리 객체가 생성되어야 한다. 코어 데이터에서 사용되는 관리객체는 모두 NSManagedObject 클래스나 또는 그 하위 클래스의 인스턴스 이며, 생성된 관리 객체들은 모두 관리 객체 컨텍스트에 담겨 관리된다.

관리 객체 컨텍스트(Managed Object Context)

관리 객체 컨텍스트는 코어 데이터에서 가장 핵심적인 객체로, 크게 두가지 역할을 담당한다

  1. 관리 객체를 담거나 생성, 삭제할 수 있다. 모든 관리 객체는 컨텍스트에 담겨 관리되는데 데이터를 읽거나 쓰고, 수정하는 작업은 모두 컨텍스트를 통해 처리된다. 코어 데이터가 다루는 모든 데이터는 메모리에 로드된 상태로 처리되는데 이떄의 메모리는 곧 컨텍스트를 의미한다.

  2. 영구 저장소 및 저장소 코디네이터에 대한 관리자이다. 컨텍스트는 영구 저장소 코디네이터와 매우 밀접하게 연결되어 있으며, 읽기 및 쓰기 요청을 처리한다. 하지만 이 작업은 모두 컨텍스트 내부에서 처리하기 때문에, 특정 메소드를 호출하는 것만으로 데이터를 읽거나 쓰는 데 필요한 모든 작업을 처리할 수 있다

영구 저장소 코디네이터(Persistent Store Coordinator)

영구 저장소 코디네이터는 컨텍스트와 직접 데이면서 다양한 영구 저장소들의 접근을 조정하고, 해당 저장소에 대한 실제 입출력을 담당한다.

코디네이터는 단일 컨텍스트와 연결되어 애플리케이션이 전달하는 각종 요청을 처리하는데, 예를들어 필요한 객체가 아직 컨텍스트에 로딩 되지 않았다면 코딩네이터는 캔텍스트로부터 요청을 받아 영구저장소에 데이터를 찾고, 이를 컨텍스트에 전달하여 메모리에 로드 하는 방식이다. 이 과정에서 코디네이터는 미리 정의된 관리 객체 모델을 사용하여 인스턴스를 생성하고 여기에 읽어온 데이터를 담아 전달하고, 이렇게 생성된 인스턴사 바로 관리 객체이다. 이 과정은 컨텍스트 객체와 영구 저장소 사이에서 자동으로 처리되기 때문에 개발자가 직접 코디네이터에 접속하여 뭔가를 처리해야하는 경우는 거의 없다.

관리 객체 모델(Managed Object Model)

코어 데이터에서 테이블에 대응되는 엔터티(Entitiy)의 구조를 정의하는 객체인 동시에 이 스키마를 바탕으로 정의된 MO 패턴의 모델 클래스를 가리킨다. 관리 객체에 저장된 데이터 구조에 대한 정보를 담고있다. 관리 객체 모델은 Xcode에서 설계한 엔터티로부터 생성된다.

관리 객체(Object)와 관리 객체모델(Object Model) 구분

  • 관리 객체 모델 클래스이자 형식이고 구조인 반면, 관리 객체는 이를 바탕으로 생성되는 인스턴스이다.

  • 관리 객체는 데이터 조작이나 저장 등에 관여하는 실질적인 객체이지만, 관리 객체 모델은 데이터를 조작하거나 저장하는 동작에는 영향을 미치지 않으며 대신 관리 객체의 각 요소를 제대로 담을 수 있도록 저장 데이터를 구조화 하는 데에만 사용된다.

영구 객체 저장소(Persistant Object Store)

코어 데이터를 사용할 때 데이터가 저장되는 저장소 환경을 의미한다. 내부적으로 코어 데이터는 데이터에 대한 변경 사항을 해석하여 영구 저장소에 저장한다.

  • 인메모리 저장소 타입(NSInmemoryStoreType)

  • 플랫 바이너리 저장소 타입(NSBinaryStoreType)

  • XML 저장소 타입(NSXMLStoreType)

  • SQLite 데이터베이스(NSSQLiteStoreType): 가장 많이 선택하는 영구 저장소 타입, 일부만 로딩하기 때문에 메모리에 객체 그래프가 완전히 로딩되어 있지 않을 수 있지만 일반적인 사용 범위에서는 크게 문제되지 않는 수준이다. iOS 프로젝트에서 코어 데이터가 기본으로 채택하는 방식이다.

영구 저장소 계층은 추상화되어 있기 때문에, 어떤 타입을 선택하더라도 애플리케이션에서 데이터를 처리하기 위해 사용하는 코어 데이터 API는 달라지지 않는다.

SQLite 저장소 타입은 iOS에서 기본 타입으로 사용되긴 하지만, 몇 가지 문제를 안고 있다. 이는 부분적인 업데이트를 수행하는 저장 방식 때문으로, 대표적인 문제가 처리 속도와 원자성이다. 전체 내용을 그대로 파일에 쭉 밀어 넣는 전체 업데이트와 달리 수정해야 하는 부분을 찾아서 해당 내용을 삭제하고, 여기에다 새로운 내용을 작성해 넣어야 하기 때문이다. 게다가 부분 업데이트로 내용을 변경하는 과정에서 오류가 발생하거나 제대로 진행되지 않을 경우 전체 파일의 손상이 생길 수도 있다.

인메모리(In-Momory) DB

코어데이터는 우선 메모리에 로딩이 되어야하는 인메모리 방식

코어 데이터는 인메모리 방식으로 동작하는 프레임워크이다. 사용하려는 모든 데이터는 우선 메모리에 로딩되는 과정을 거친 다음에야 비로소 사용할 수 있으며, 데이터를 읽거나 쓰고 수정하며 삭제하는 모든 작업은 메모리에 로딩된 데이터를 대상으로 이루어진다. 코어 데이터에서 파일이나 SQLite 등의 영구 저장소에 데이터를 저장하는 과정은 선택적이며, 사용자가 명시적으로 저장 메소드를 호출했을 때만 실행된다. 변경 내역을 영구 저장소에 반영하는 과정을 가리켜 커밋(commit) 또는 동기화(Synchronize)라고 부르는데, 커밋이 발생하기 전까지 변경된 데이터는 메모리에서만 존재한다.

이 방식의 장점으로는 빠른 처리 속도와 성능의 향상이다. 온디스크(On-Disk) 방식처럼 매번 디스크에 직접 작성하거나 읽어오지 않아도 되기 때문에 상대적으로 I/O가 적게 발생하며, 비즈니스 로직 수행 과정에서 발생하는 데이터의 변경 내역을 모두 메모리 수준에서 처리한 후 최종 결과만 영구 저장소에 반영하면 되기 때문에 여러 번 반복해서 읽거나 쓰더라도 성능에는 문제가 거의 생기지 않는다. 특히 디스크에 보존하지 않아도 되는 임시 데이터를 가공할 경우 코어 데이터는 데이터베이스에 비해 훨씬 빠른 속도로 데이터를 다룰 수 있다.

SQLite를 사용시

코드를 작성하는 방식에 따라 성능의 저하가 생길 수 있는데 영구 저장소로 SQLite를 사용하는 경우, 디스크에 읽기/쓰기를 하는 SQLite의 기본 오버헤드에다 코어 데이터와 SQLite 간의 데이터 컨버전을 위한 오버헤드가 추가되기 때문에 매 작업마다 커밋을 하게 되면 오히려 SQLite만 사용하는 것보다 느려질 수 있다.

데이터의 교환 및 저장 메커니즘

데이터 교환 방식은 영구 저장소에서 레코드를 로딩할 때에는 저장된 레코드를 로딩할 때에는 저장된 레코드를 그대로 읽어 관릴 객체로 만들어 내고, 반대로 관리 객체를 영구 저장소에 저장할 때에는 메모리상에서 수정된 객체가 그대로 영구 저장소에 반영되는 식이다. 따라서 데이터 항목 하나하나를 처리하기 위해 구문을 작성할 필요는 없으며, 프로퍼티 리스트를 저장할 때처럼 개별 데이터를 일일이 저장할 필요도 없다. 그냥 메모리상에서 컨텍스트에 로드된 관리 객체를 수정한 뒤 커밋하면 변경된 사항이 통째로 영구 저장소에 반영된다.

영구 저장소로 SQLite를 선택한 상태에서 컨텍스트의 관리 객체를 저장소에 커밋할 때에는 차등저장(Differencail Save) 메커니즘이 사용된다. 이 매커니즘은 매번 데이터 전체를 커밋하는 대신 마지막 저장 이후에 변경 부분만 커밋하는 방식으로, 빠르고 가볍게 처리할 수 있다.

코어 데이터를 사용하면서 생성,수정 또는 삭제된 데이터에 대해 최종적으로 save() 메소드를 호출하면 메모리상의 관리 객체 변경 내용이 그대로 영구 저장소에 커밋된다. 이를 커밋하지 않고 앱을 종료할 경우 데이터의 변경 내역은 반영되지 않은 채로 버려지기 때문에, 변경 사항이 발생했다면 적절한 시기에 반드시 save() 메소드를 호출하면 변경된 내역을 커밋해 주어야한다.

코어 데이터의 한계

데이터베이스의 기능 중에서 코어 데이터가 하지 못하는 기능의 한계까 존재한다.

  • 데이터를 메모리에 로딩하는 과정 없이는 작업이 불가능하다.

코어 데이터는 인메모리 방식을 기반으로 동작하는 프레임워크이다. 메모리에 로딩된 객체에 대해서만 수정이 가능하기 때문에, 먼저 객체를 메모리에 로딩해 두어야 한다. 데이터를 삭제하는 과정 역시 마찬가지이다. 이 같은 과정이 반복되면 메모리 사용량이 늘어나고 이는 결국 성능의 하락으로 이어질 수 있다.

많은 수의 객체들을 수정하거나 삭제해야 할 경우에는 NSFetchRequest 객체에 정의된 returnDistinctResults, propertiesToFetch 등의 속성을 적극 활용하여 객체의 전체 프로퍼티를 모두 불러오는 것을 지양하고, 주기적으로 NSManagedObjectContext#refresh(_:mergeChanges:) 메소드를 호출하여 변경 내역이 없는 객체들을 해제하며, 컨텍스트를 영구 저장소에 커밋한 후에는 로딩된 모든 객체를 메모리에서 해제해 주는 등의 방법을 사용하여 메모리를 효율적으로 관리해 주어야 한다.

  • 데이터 로직을 다루는 데에 한계가 있다.

관계형 데이터베이스에서 동일 테이블에 중복된 값의 입력을 방지하는 “Unique” 키와 같은 기능을 코어 데이터에서는 제공되지 않기 때문에 중복 값의 입력을 방지하려면 애플리케이션에서 비즈니스 로직을 통해 처리해 주어야 한다.

이는 구조적 특성 때문이다. 관리 객체 모델을 서브 클래싱하는 경우 원하는 대로 데이터 프로퍼티에 대한 오버라이드가 가능한데, 이를 통해 변경된 내용을 코어 데이터가 확인할 수 있는 방법은 없다. 즉, 상위 클래스에서 특정 값의 중복 입력을 방지했다하더라도 이를 서브 클래싱한 하위 클래스에서 중복 입력이 가능하도록 오버라이드해 버리면 이를 알 방법도, 제지할 수단도 없다는 것이다.

이처럼 코어 데이터는 자신의 영역을 벗어나는 데이터 로직에 대해서는 관여할 수 없기 때문에, 이를 비즈니스 로직으로 처리해야 한다.

  • 멀티 스레드, 멀티 유저를 지원하지 않는다.

코어 데이터는 원칙적으로 싱글 스레드만 지원한다. 한 번에 하나의 작업만 처리할 수 있다는 뜻이다.

단일 작업에서 처리 성능을 향상시키기 위함인데, 멀티 스레딩 방식으로 동작할 때에는 한쪽에서 작업하고 있는 동안 해당 영역을 다른 스레드가 침범하지 못하도록 락(Lock)을 걸어야 하는데, 이 락은 종종 데이터베이스 성능 저하의 원인이 된다. 락을 걸지 않음으로써 훨씬 빠르게 데이터를 처리할 수 있다.

코어 데이터 관리 객체 모델링

코어 데이터의 기능은 대부분 애플리케이션에서 사용할 엔터티, 속성 및 속성 간의 관계를 바탕으로 정의하는 스키마에 따라 달라진다. 스키마는 곧 관리 객체 모델을 의미하는데, NSMAnagedObjectModel 클래스의 인스턴스 이다. 관리 객체 모델을 사용하면 영구 저장소의 레코드를 애플리케이션에서 사용하는 관리 객체에 연결할 수 있다. 이는 곧, 레코드 각각을 관리 객체로 생성하여 다룰 수 있음을 의미한다.

엔터티(Entity)

엔터티(Entity)는 데이터가 저장될 구조 또는 형식(Database의 테이블)으로, 내부 구성은 어트리뷰트(Attribute),릴레이션(Relation), 페치속성(Fetched Properties)로 이루어져있다.

  • 어트리뷰터 : 하위 속성들을 정의하는 역할(열(Column))
  • 릴레이션 : 다른 엔터티와의 관계를 정의하는 역할
  • 페치속성: 데이터 검색 시 반복 사용되는 요청이나 값만 바꾸어 사용하는 비슷한 요청을 미리 템플릿 형태로 만들어 놓은 것

생성된 엔터티는 클래스 파일로 생성된 다음에 프로젝트로 반입되는데, 이것을 인스턴스로 찍어낸 것이 바로 관리 객체이다.

엔터티의 내용을 클래스 형태로 생성하면 NSManagedObject 혹은 그 서브 클래스가 되지만, 엔터티 자체는 NSEntityDescription 클래스로 표현된다. NSEntityDescription은 추상 클래스로서 엔터티의 구조를 설명한다는 의미를 가진다. 이 클래스는 새로운 엔터티의 인스턴스를 컨텍스트에 생성할 때 사용하게 된다. 새로운 데이터의 추가가 필요할때 NSEntityDescription을 이용하여 인스턴스를 생성하면 그 결과로 NSManagedObject 객체 혹은 그의 서브 클래스 객체가 만들어지며, 이를 컨테스트가 인식하여 관리하게 된다.

엔터티 정의

프로젝트를 생성할때 Use Core Data 항목을 체크하면 .xcdatamodeld 파일이 자동으로 생성되고 추후에 파일을 추가하고 싶으면 Data Model 타입의 파일을 추가해주면 된다.

image

엔터티를 생성하는 과정

  1. Add Entity 버튼을 클릭한다.
  2. 추가된 엔터티를 선택한다.
  3. 데이터 모델 속성 영역에서 원하는 엔터티명으로 수정한다.

어트리뷰트 및 릴레이션을 정의하는 과정

  1. 새 엔터티를 선택한 상태에서 Add Atribute를 클릭한다.
  2. 추가된 어트리뷰트 또는 릴레이션을 선택한다.
  3. 데이터 모델 속성 영역에서 원하는 이름으로 수정한다.

데이터는 애플리케이션에서 비즈니스 로직을 통해 생성된다. 이때 정의한 엔터티들은 레코드 단위의 데이터를 관리 객체(Managed Object)로 만들기 위한 기본 모델 역할을 담당한다.

엔터티 설정 추가

엔터티명과 클래스명

엔터티가 생성되면 그에 따른 데이터 모델 클래스를 정의하여 사용할 수 있다. 데이터 모델 클래스의 이름이 엔터티명과 완전히 동일할 필요는 없으나, 접미사 MO(Model Object)를 엔터티명 뒤에 붙여서 데이터 모델 클래스명을 정의해 주는 것이 관례이다.

엔터티의 상속과 추상 엔터티

엔터티는 상속 가능하다. 엔터티의 상속은 클래스에서와 비슷한 방식으로 동작한다. 유사한 엔터티들이 여러 개 있을 경우 엔터티마다 동일한 어트리뷰트를 정의하는 대신 공통 어트리뷰트를 뽑아 상위 엔터티를 정의하고, 나머지 엔터티들은 이를 상속받는 하위 엔터티로 정의하여 사용할 수 있다.

상속 관계를 설정할 때에는 인스팩터의 Parent Entity 항목을 이용한다. 상속받은 필요가 없는 엔터티라면 기본값인 No Parent Entity를 사용하고 상속받고자 한다면 원하는 엔터티를 선택해 주는 것으로 상속 관계가 성립한다.

상위 엔터티를 만들지 않고 단순히 상속 용도로만 사용하고 싶다면, 추상 엔터티로 지정해주면 된다. Abstract Entity 옵션을 통해 추상 속성이 설정된 엔터티는 관리 객체 인스턴스를 생성할 수 없으며, 이 엔터티를 상속받는 하위 엔터티에서만 인스턴스를 생성할 수 있다.

어트리뷰트 정의

어트리뷰트는 엔터티 내부를 구성하는 요소이며, 릴레이션은 다른 엔터티와의 관계를 정의하는 요소에 해당한다.

어트리뷰트의 이름은 몇 가지 규칙이 적용되는데, 대소문자를 구분하지만 대문자로 시작할 수는 없다. 카멜 표기법이나 언더바를 이용한 단어 연결 역시 가능하지만, 엔터티 구조를 관리 객체 모델 클래스로 변환한 결과를 생각해 본다면 가급적 언더바는 자제하는 것이 좋다. 어트리뷰트는 문자열, 날짜, 숫자 등의 값을 가질 수 있으며, 이들은 각각 NSString, NSDate, NSNumber 등 오브젝티브-C 스타일의 값으로 처리된다.

image

어트리뷰트에는 타입 설정에 따른 세부 도메인을 설정할 수 있다.

도메인(Domain): 특정 속성이 가질 수 있는 값의 범위를 나타내는 용어

정수 타입의 어트리뷰트에서는 최대값과 최소값, 기본값을, 날짜 타입의 어트리뷰트에서는 최소 날짜와 최대 날짜, 기본 날짜 등을 설정할 수 있다. 이들 값을 일일이 지정해 줄 필요는 없지만, 옵셔널 타입의 nil 값과 구분할 수 있도록 기본값 정도는 지정해 주는 것이 좋다.

릴레이션 정의

릴레이션은 정규화된 데이터 모델의 연결이나 참조를 위해 사용된다. 릴레이션을 통해 다른 엔터티와 연결해 놓으면 이 연결을 통해 다른 엔터티의 저장된 레코드까지 함께 참조할 수 있다. 데이터베이스에서의 조인과 거의 동일한 의미를 가진다. 또한 릴레이션은 어트리뷰트처럼 동작한다는 점에서 관계형 데이터베이스의 외래 키 칼럼과 같은 역할을 하기도 한다.

하지만 데이터베이스의 조인과는 분명한 차이가 있다. 데이터베이스가 정규화된 공통 칼럼을 이용하여 양쪽의 테이블을 연결하고 필요한 칼럼을 가져오는 방식이라면, 릴레이션은 다른 쪽 엔터티를 직접 참조함으로 해당 데이터를 몽땅 반입하는 방식이다.

릴레이션 관계 타입 3가지

  • 1 : 1 - 엔터티 A의 레코드 하나와 엔터티 B의 레코드 하나가 서로 참조

  • 1 : M - 엔터티 A의 레코드 하나와 엔터티 B의 레코드 여러 개가 서로 참조

  • M : N - 엔터티 A의 레코드 여러 개와 엔터티 B의 레코드 여러 개가 서로 참조

실제로 릴레이션을 설정할 때는 하나의 엔터티에서 다른 엔터티로의 단방향 릴레이션을 설정한다. 상호 참조하는 경우에는 단방향 릴레이션을 양쪽 엔터티에서 각각 설정하여 릴레이션이 서로 마주보도록 해 주어야 한다. 이때 단순히 상호 참조 형태로 릴레이션을 걸어 버릴 경우 순환 참조의 문제가 생길 수 있으므로, 이를 방지하기 위해 Inverse라는 항목의 설정이 필요하다.

단방향 릴레이션을 설정할 때에는 이것이 일대일 관계인지 일대다 관계인지에 대해 결정해 주어야 한다. 인스펙터 영역에서 Type 속성을 통해 일대일 관계일 때에는 “To One”, 일대다 관계일 때에는 “To Many”로 설정하면 된다.

코어 데이터의 릴레이션은 한쪽 엔터티가 다른 쪽 엔터티를 참조하는지 여부만 판단하기 때문에, 방향성을 가지게 된다. 엔터티 A가 B를 참조하는 것을 일반참조라고 하고, 반대로 엔터티 B가 A를 참조하는 것을 역참조라고 한다. 일반참조만 설정되어 있다면 엔터티 B는 A를 참조할 수 없기 때문에, B도 A를 참조하기 위해서는 반드시 반대 방향인 역참조가 설정되어야 한다. 일반참조와 역참조는 역할이 고정되어 있는 것이 아니라 관점에 따른 상대적인 것이다.

Name은 릴레이션의 이름을 지정하는 항목이며 첫 글자는 소문자, 이후로는 대소문자를 섞어 사용할 수 있다.

Optional은 참조 대상이 없는 경우를 허용하기 위한 옵션이다. 엔터티의 구조 자체는 다른 엔터리를 참조하도록 되어 있지만, 레코드에 따라서는 nil 참조가 발생하는 경우가 있을 수도 있다.

Inverse 항목은 두 개의 엔터티가 상호 참조하는 경우 순환 참조의 오류를 방지하기 위해 사용된다. 상대방 릴레이션과 서로 역참조 관계라는 것을 알려줌으로써 순환 참조를 차단한다. 이 옵션에서 선택할 수 있는 것은 대상 엔터티에 설정된 릴레이션 목록으로, 이들 중 대응 관계에 있는 릴레이션을 찾아 선택해 주면 된다.

Delete Rule은 참조 대상 엔터티의 레코드가 삭제되었을 경우 이를 참조하고 있는 엔터티의 레코드를 어떻게 할 것인지를 결정 한다. 이 옵션이 설정된 릴레이션은 대상 엔터티의 레코드가 삭제될 시 이를 참조하고 있는 레코드를 해당 옵션으로 설정된 내용에 따라 처리한다.

선택 가능한 옵션 항목

  • Nullify: 참조 대상이 삭제되면 nil 값으로 처리하도록 하는 옵션

  • Cascade: 참조 대상이 삭제되면 이를 참조하고 있던 모든 레코드를 함께 삭제하도록 하는 옵션

  • Deny: 참조하고 있는 레코드가 있을 경우 참조 대상을 삭제하지 못 하도록 막는 옵션

CoreData 구현

엔터티 설계하기

엔터티는 데이터베이스에서 테이블에 해당하는 개념이며, 엔터티 설계 또한 역시 테이블을 설계하는 과정과 매우 비슷하다. .xcdatamodeld 파일을 추가하여 entity, attribute, relationship을 설계할 수 있다.

image

image

fetch 기능 구현

코어 데이터에서 레코드를 읽어 오는 과정을 보통은 fetch(페치) 라고 표현한다. “데이터 가져오기” 정도의 의미로 해석하면된다.

fetch 의 4단계

  • 1단계 : AppDelegate 객체 참조

  • 2단계 : Context 참조

  • 3단계 : 요청 객체(NSFetchRequest) 생성

  • 4단계 : fetch(_:) 메서드 호출하여 레코드 가져오기

fetch 구현

image

위의 4단계 과정을 진행하고나면 NSManagedObeject 또는 그 하위 타입의 인스턴스로 이루어진 배열을 반환 할 수있다. (fetch() - > [NSManagedObject] 의 반환타입 이기 때문) 이때 배열을 이루는 각각의 인스터스가 바로 관리객체(Managed Object) 이다.

코어 데이터에 저장된 데이터를 가져올 때에는 요청 사항을 정의한 NSFetchRequest 객체가 사용된다. 이 객체는 다양한 요청들을 복합적으로 정의할 수 있다. 대표적으로 이들 3가지가 있다.

  1. 어디에서 데이터를 가져올 것인가?(엔터티 지정) = FROM

  2. 어떤 데이터를 가져올 것인가?(검색 조건 지정) = WHERE

  3. 어떻게 데이터를 가져올 것인가?(정렬 조건 지정) = ORDER BY

NSFetchRequest는 데이터베이스의 SELECT 쿼리문과 유사한 역할이다. 쿼리문과 마찬가지로 2, 3 의 조건들을 생략할 수 있다. 그러나 SELECT 쿼리문에서 FROM 이 필수 요소 인것처럼, NSFetchRequest에서도 엔터티 정보는 생략할 수 없다.

NSFetchRequest객체를 생성할 때 특정 엔터티의 이름을 인자값으로 넣어줌으로써 어느 엔터티에서 데이터를 가져올지 선택할 수 있다. 이렇게 생성된 NSFetchRequest객체는 해당 엔터티 구조로 저장된 모든 데이터를 읽어들이도록 요청을 전달하게 된다.

NSFetchRequest객체가 만들어지고 나면 이제 실제로 데이터를 가져올 차례이다. 코어데이터는 컨텍스트 객체를 통해 각각의 CRUD에 해당하는 메소드를 제공한다. 가령 원하는 레코드 여러개를 한꺼번에 가져올 때에넌 fetch(_:) 메서드를 사용하고, 레코드를 저장하거나 수정된 내용을 반영 할 때에는 (insert, update) save()메서드를 사용하는 식이다. 레코드의 객체 ID를 알고있다면 object(_:) 메서드를 이용하여 원하는 레코드만 읽어올 수도 있다.

image

지정한 관리객체(NSManagedObject) 배열 프로퍼티로부터 이 행(cell)에 해당하는 데이터를 읽어와 준비하는 과정과, 준비된 데이터를 사용하여 cell을 구성하고 반환하는 과정으로 이루어 져있다. list배열 내부의 타입은 NSManagedObject 이기 때문에 우리가 원하는 항목의 값을 읽어오기 위해서는 value(for:Key:)메서드를 사용해야한다. 이때 코어 데이터는 실제로 저장되는 값의 타입이 어떤 것인지 정확히 알지 못하므로 Any 타입의 값으로 반환한다. 그렇기에 적절한 타입으로 캐스팅을 해주어야한다. as? String으로 타입 캐스팅을 해주는 것이 그러한 이유이다.

데이터 등록 기능 구현

코어 데이터에 데이터를 등록하는 로직은 크게 세단계로 이루어진다.

  • 1단계 빈 관리 객체를 생성하고, 이를 context 객체에 등록한다.

  • 2단계 생성된 관리객체에 값을 채워넣는다.

  • 3단계 컨텍스트 객체의 변경 사항을 영구 저장소에 반영한다. 이를 커밋또는 동기화라고 부른다.

관리 객체는 생성과 동시에 컨텍스트 객체의 관리 하에 있어야 하기 때문에, 기본 초기화 메서드를 이용해서 객체만 생성할 경우 런타임 오류가 발생한다. 반드시 다음과 같은 방식으로 관리 객체를 생성하여 컨텍스트 객체가 관리할 수 있도록 해주어야한다.

let entity = NSEntityFescription.entity(forEntityName: "Company", in: context)
let object = NSManagedObject(entity: entity, insertInto: context)

위의 코드를 한줄로 대체

let object = NSEntityDescription.insertNewObject(forEntityName: "Company", into: context)

이렇게 생성된 관리 객체에 setValue(_:forKey:)메서드를 이용하여 값을 설정하고 나면 일단 관리 객체에 대한 작은 완료된다. 하지만 완전히 끝난것이 아니다. 현재까지는 메모리에만 반영되어있는 상태로, 영구저장소에 반영해주지 않으면 변경 사항이 어느순간 사라져 버릴수도있다. 따라서 데이터를 생성/수정/삭제 하는 과정에서는 항상 제일마지막에 save() 메서드 호출을 통해 컨텍스트의 변경사항을 영구 저장소에 반영 해주어야 한다.

이떄 코어 데이터는 컨텍스트의 내용을 전부 영구 저장소에 저장하는 것이 아니라 마지막 커밋 이후 변경된 사항만 선별해서 반영한다. 이것을 차등 저장(Differencial Save) 라고한다.

image

save()메서드를 통해 컨텍스트 변경 사항을 영구 저장소와 동기화 한다. 동기화가 끝나면 self.list 배열에도 추가하여 굳이 다시 데이터를 읽어오지 않아도 되도록 처리했다.

이때 동기화가 실패했을 경우에 대한 rollback() 메서드 호출이다. 메서드는 마지막 동기화 시점 이후의 모든 변경 내역을 원래대로 되돌리는 역할을 한다. 영구 저장소에 커밋이 실패했다 하더라도 현재의 컨텍스트에는 새로 생성된 객체가 남아있게 되므로 이를 그대로 두면 실제 저장소와 일시적으로 데이처가 일치 하지 않는 문제가 생길 수 있다. 이를 방지 하기 위해 try~ catch블록에서 컨텍스트를 롤백키셔 주는 처리를 한다.

image

데이터 삭제 기능 구현(Delete)

코어 데이터에서 삭제 과정은 두단계로 이루어진다.

첫번째 단계는 컨텍스트에 로딩된 틀정 데이터를 삭제하는 과정이다.
정확히는 관리 객체 인스턴스를 삭제하는 것이다. 영구 저장소에서 직접 데이터를 삭제하는 것이 아니라는게 유의할 점이다.
이때 만약 삭제하려는 값이 컨텍스트에 로딩되어 있지 않다면 영구 저장소에서 가져와 일단 컨텍스트에 로딩한 다음에 삭제해야한다.

두번째 단계는 컨텍스트의 변경사항을 영구 저장소에 동기화하는 과정이다.
이과정은 “삭제”가 아니라 변경사항을 “반영”하는 개념이기에 실제로는 데이터를 삭제하는 과정임에도 불구하고 정작 컨텍스트에서 호출하는 메서드는 save() 이다.

image

  • tableView 행을 지움과 동시에 코어 데이터에서도 해당 목록 삭제

image

image

수정 기능 구현하기(Update)

코어 데이터의 수정 로직은 등록 조직과 삭제 로직을 각각 반반 합쳐 놓은 것으로 이해하면된다.
setValue(_:forKey:)메서드를 사용하는 것이나 유저 인터페이스 구조 등은 등록 기능과 비슷하지, 새로운 관리 객체를 생성하는 것이 아니라 컨텍스트에 로딩된 기존 관리 객체를 이용하여 값을 변경하는 과정이 삭제 로직과 비슷하기 때문이다.

수정 기능 구현

image

수정 작업은 이미 컨텍스트에 로딩 되어있는 관리 객체에서 이루어 져야 한다. 이를 위해 edit(object:name:address)메서드에서는 첫번째 매개 변수로 수정할 관리 객체를 전달하고 있다.
항목별로 값을 수정하면 컨텍스에 저장된 내용도 그대로 변경되지만, 영구 저장소에는 반영되지 않기 때문에 수정 작업이 끝난 후 적절한 시점에서 save()메서드를 호출하여 동기화를 시켜주어야한다.

image

정렬 기능 구현하기(Sorting)

최신 데이터가 위쪽이나 아랫쪽에 가게 하게끔 데이터를 정렬해서 가져오는 처리가 필요하다.
코어데이터에서 원하는 순서대로 레코드를 가져오기 위해서는 정렬을 담당하는 NSSortDescriptor 객체를 알아야 할 필요가 있다.

let sort = NSSortDescriptor(key: <정렬기준칼럼>, ascendingL <오름차순여부>)
fetchRequest.sortDescriptors = [sort]

NSSortDescriptor객체 생성 과정에는 두 개의 매개 변수가 사용된다. 첫 번째는 정렬할 칼럼, 즉 어느 어트리뷰트를 기준으로 정렬할 것인가에 대한 값이고, 두번째는 어떤 순서로 정렬할 것인가에 대한 Bool타입의 값이다. 의미적으로는 오름차순 여부에 대한 값이므로, true를 입력하면 오름차순 정렬이 되고 false를 입력하면 내림차순 정렬이 된다.

생성된 NSSortDescriptor 객체는 fetchRequest.sortDescriptors 속성에 할당됨으로써 요청 객체의 일부로 동작하게 된다.
이 속성은 배열이므로 두개이상의 NSSortDescriptor 객체를 정의하여 대입할 수 있다.

정렬은 fetch 함수에 정렬 속성을 추가해주면 된다.

CoreData 에 값을 추가 할 때마다 NSManagedObject배열을 재정렬 한다음에 반영하는 방식으로 하면된다.

image

  • 주의
    NSManagedObject배열의 값을 변경 시키고 tableView 를 reload()하고 끝내면 안된다.
    직접 tableView의 Cell의 내용을 변경 시켜 주어야한다. 단순하게 테이블 뷰를 reload 해버리면 순서가 제대로 변경 되지 않는 문제가 있고, reload() 메서드와 moveRow(at:to:)메서드를 함께 사용할 경우에는 두 메서드의 실행 중 충돌로 인해 원하는 결과가 제대로 나오지 않기 때문에 셀의 내용을 수정 해준 다음 셀의 순서를 변경하도록 로직을 처리하는 것이 바람직 하다.

직접 Cell 의 내용을 수정 할 거면 list배열을 다시 읽어 올(fetch) 할 필요가 없지 않는가?

Cell 내용을 수정하고 순서를 변경하면서 list 배열의 값을 변경하지 않는다면 cell의 순서와 실제 list 배열에 저장된 데이터의 순서가 서로 맞지 않는 문제가 생긴다. 이때 indexPath.row를 이용하여 해당 행에 맞는 값을 읽어오는 코드가 모두 잘못될 수 있으므로, 직접 셀의 순서를 변경하더라도 list 배열의 내용도 갱신하여 현재의 셀 순서와 일치 시켜 주어야 한다.


Reference





© 2020. by Gaki

Powered by gaki