UIKit / CoreData

Minsang Kang·2022년 2월 17일
0

UIKit

목록 보기
2/2
post-thumbnail

"꼼꼼한 재은 씨의 Swift 실전편" 책의 요약 내용 입니다.

이론

7.1.4 코어 데이터의 한계

멀티 스레드, 멀티 유저를 지원하지 않는다.
단일 작업에서 처리성능을 향상시키기 위함.

멀티 스레드를 통해 코어 데이터를 사용해야 한다면 관리 객체와 컨텍스트의 사용에 주의해야 한다.
기존의 스레드가 데이터 저장을 끝내고난 후 다른 스레드가 새로운 컨텍스트 객체를 이용하는 식으로 접근해야 한다.

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

스키마 = 관리 객체 모델 = NSManagedObjectModel 클래스의 인스턴스
관리 객체 모델을 사용하여 영구 저장소의 레코드를 관리 객체에 연결할 수 있다.
즉, 레코드 각각을 관리 객체로 생성하여 다루는 구조.

7.2.1 엔터티

엔터티(Entity) = 데이터가 저장될 구조, NSEntityDescroption 클래스로 표현

  • 어트리뷰트(Attribute) : 속성들의 정의
  • 릴레이션(Relation) : 다른 엔터티와의 관계를 정의
  • 페치 속성(Fetched Properties)

새로운 데이터 추가시 엔터티(NSEntityDescription)를 이용하여 관리 객체 모델(NSManagedObject) 객체가 만들어지며, 컨텍스트가 인식하여 관리하게 된다.

7.2.2엔터티 정의하기

프로젝트가 만들어진 이후일 경우 프로젝트 탐색기에서 .xcdatamodeled 파일을 선택
코어 데이터 모델 편집창을 통해 Entity, Attribute, Relation 등을 정의

7.2.3 엔터티 설정 추가하기

클래스명(관리 객체 모델명)은 통상 Entity명+MO를 붙여 정의하는게 관례 (Model Object의 약자)
코어데이터의 Entity는 상속이 가능

  • 공통 Attribute를 뽑아 상위 Entity를 정의, 나머지 Entity는 상속받는 식으로 정의

상속 관계를 설정하려면 인스펙터의 Parent Entity 항목을 이용

  • 원하는 Entity를 Parant로 설정

예시: Person Entity를 상속받은 Employee Entity

Person
- bitthDate
- genter
- name
Employee
- empCd
- grade
- joinDate
- stateCd

7.2.4 어트리뷰트 정의 하기

Attribute: 데이터베이스에서의 칼럼, 또는 필드위 유사 개념

  • 이름: 대문자로 시작할 수 없으며, 언더바를 자제하는것이 좋다. (관리 객체 모델 클래스로 변환되기에)
  • 타입: objective-c 스타일의 값 (NSString, NSDate, NSNumber 등등)

도메인(Domain) 값을 설정할 수 있다. (값의 범위 설정)
기본값(default value) 값을 설정할 수 있다. -> nil 값과는 다르게 처리하기 위한 용도로 사용
optional 타입으로 설정할 수 있다.

7.2.5 릴레이션 정의하기

정규화된 데이터 모델의 연결이나 참조를 위해 사용된다.
릴레이션을 통해 다른 엔터티의 레코드를 함께 참조할 수 있다. (join과 비슷)
관계형 데이터베이스의 외래키 칼럼과 같은 역할

코어데이터의 릴레이션의 경우 데이터베이스와 다르게 레코드를 직접 참조하는 방식

릴레이션에서 Type 값을 Entity 입장에서 참조할 Entity로의 단방향 릴레이션을 설정

  • 상호 참조해야 하는 경우는 두 Entity 각각에서 단방향 일레이션을 설정하여 서로 참조하도록 설정해야 한다. (일반 참조, 역참조 관계)
  • 상호 참조 릴레이션의 경우 순환 참조의 문제가 생길 수 있으므로 Inverse 항목의 설정이 필요

Relationship

  • Name: Attribute 와 동일 규칙
  • optional: 참조 대상이 없는 경우를 허용하기 위한 옵션
  • Inverse: 순환 참조 오류를 방지하기 위함, 상호 참조되는 Entity 명시
  • Delete Rule: 참조 대상 Entity가 삭제되었을 경우 참조하고 있는 Entity의 레코드를 어떻게 할 것인지 설정.
    • No Action
    • Nullify: nil 값으로 처리 (대기)
    • Cascade: 모든 레코드를 함께 삭제 (퇴사)
    • Deny: 참조하고 있는 레코드가 있을 경우 참조 대상을 삭제 못하도록 막는 옵션

7.2.6 엔터티와 데이터 모델 클래스

@objc: objective-c 기반의 코드에서도 인식할 수 있음. (attribute 값들이 objective-c 기반)
@NSManaged: 해당 property가 동적으로 정의될 수 있음을 컴파일러에게 알리는 역할. (extension 내에서도 property 설정이 가능)
To One Relation

@objc(Problem_Core)
public class Problem_Core: NSManagedObject {
	@NSManaged public var pid: Int64
    @NSManaged public var pName: String?
    @NSManaged public var contentImage: Data?
    @NSManaged public var page: Page_Core? // 릴레이션에 의해 생성된 attribute
}

relation 된 entity 값은 problem.page.vid 식으로 접근할 수 있다.

To Many Relation

@objc(Page_Core)
public class Page_Core: NSManagedObject {
	@NSManaged public var vid: Int64
    @NSManaged public var materialImage: Data
    @NSManaged public var layoutType: String
    @NSManaged public var problems: NSSet? // 릴레이션에 의해 생성된 attribute
}

Array 형식이 아닌 NSSet 타입으로 정의 (동일 레코드의 중복 참조를 방지하기 위함)
순서를 보장하기 위해서는 Relationship 속성에서 Arrangement 값의 Ordered 값이 활성되어야 한다. (NSOrderedSet 타입이 된다.)

커스텀 모델 클래스

코어 데이터에서 사용되는 모든 모델 클래스는 NSManagedObject의 하위 클래스.
커스텀 모델 클래스를 정의하기 위해서도 또한 NSManagedObject 의 하위 클래스여야 한다.
Xcode: Editor -> CreateNSManagedObjectSubclass... 를 통해 생성 가능.

Class 인스펙터

  • Name: 커스텀 모델 클래스명 (Entity명+MO)
    • 적히지 않을 경우 관리 객체를 생성하는 과정에서 런타임 오류가 발생.
  • Codegen: Manual/None 값으로 설정
    • 아닐 경우에 같은 이름으로 모델 클래스를 자동 생성하기에 클래스간 충돌로 인해 컴파일 오류가 발생.
    • 코어 데이터가 자동으로 생성한 모델 클래스와 서로 다른 별개의 객체임이 매우 중요.

실습

AppDelegate

let container = NSPersistentContainer(name: "semomun")

프로젝트에 추가된 .xcdatamodeled 파일을 코어 데이터 시스템에 등록,
NSPresitentContainer 객체를 생성.
만약 .xcdatamodeled 파일명이 수정된다면 name 값 또한 수정되어야 한다.

let context = persistentContainer.viewContext // -> NSManagedObjectContext

코어 데이터에서 데이터를 읽고 쓰기 위한 컨텍스트 객체

  • AppDelegate -> container -> viewContext 순서로 참조
func applicationWillTerminate(_ application: UIApplication) {
    self.saveContext()
}

위 코드를 통해 앱이 종료될 때 안전하게 한번 더 저장할 수 있다.

fetch

  • AppDelegate 참조
  • Context 참조
  • NSFetchRequest 생성
  • fetch()

ViewController 상에서 context에 접근하는 예시 코드

func fetch() -> [NSManagedObject] {
	let appDelegate = UIApplication.shared.delegate as! AppDelegate
    let context = appDelegate.persistentContainer.viewContext
    
    let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Board")
    let result = try! context.fetch(fetchRequest)
    
    return result
}

위의 예시 코드처럼 NSManagedObject 타입으로 받은 경우 attribute 값을 접근하려면 .value(forKey: ) //Any 타입 식으로 접근해야 한다.
Any 타입으로 반환되기에 타입캐스팅을 통해 사용해야 한다.

NSFetchRequest

코어 데이터에 저장된 데이터를 fetch하기 위한 요청 사항을 정의하기 위한 객체. (데이터베이스 상의 SELECT 와 유사 역할)

  • Entity 지정 (FROM): 필수 - NSFetchRequest
  • 검색 조건 지정 (WHERE): 생략 가능 - NSPredicate
  • 정렬 조건 지정 (ORDER BY): 생략 가능 - NSSortDescriptor

NSPredicate

objective-c 표준 표현식을 따른다.
아래 세가지 값을 토대로 만들어진다.

  • 검색 대상 Attribute 값 (format)
  • 비교연산자
  • 비교할 값 (바인딩 변수)
let fetchRequest: NSFetchRequest<Problem_Core> = ProblemCore.fetchRequest()
let predicate = NSPredicate(format: "pid == %@, "\(pid)") // <검색조건 표현식>, <바인딩 변수>
fetchRequest.predicate = predicate

바인딩 변수

%@, %K, %d 등의 표현식 사용 가능.

비교연산자

Predicate Format String Syntax 에서 많은 정보를 알 수 있다.

  • %d일 경우 <, >, <=, >= 연산이 가능하다.
  • %@ 형식에 CVarArg 값일 경우 <, >, <=, >= 연산이 가능하다.
  • && 를 통해 두가지 이상의 비교연산자를 사용할 수 있다.
NSPredicate(format: "joindate >= %@ && joindate <= %@", startDate, endDate) // start ~ end 이내
  • CONTAINS 연산자를 통해 특정 String 값이 포함된 문자열을 검색할 수 있다.
NSPredicate(format: "name CONTAINS %@", "길동") // "길동"이 포함된 name 모두 해당
  • CONTAINS[c] 를 통해 대소문자 구분 없이 포함된 문자열을 검색할 수 있다.

NSSortDescriptor

NSSortDescriptor 객체를 생성하여 fetchRequest 객체의 정렬 속성에 대입.

let sort = NSSortDescropter(key: "date", ascendiing: false) // date 값을 내림차순으로 정렬 (최신순)
fetchRequest.sortDescropters = [sort] // 정렬 속성에 대입

속성이 2가지 이상일 경우 순서대로 앞 정렬값에 따라 정렬이 된 값을 토대로 다음 정렬이 이루어진다.

만약 새로 만들어진 object를 최신순으로 표시하고자 할때는 list.insert(object, at: 0) 식으로 넣는다.
또는 object를 context 상에 반영 후 save 한 다음 sort된 list를 받아오는 것도 방법이다.

Create & Save

  • 객체 생성, Context 등록
  • 객체 값 설정
  • context.save(): Context 변경 사항을 영구저장소에 반영 (커밋, 동기화)
  • 저장에 실패할 경우 context.rollBack(): 모든 변경 내역을 원래대로 되돌리는 역할 (메모리상 변경 내역을 저장소와 동일시(제거))
let object = NSEntityDescroption.insertNewObject(forEntityName: "Board", into: context) // 생성 후 context에 등록
object.setValue("title", forKey: "title") // 값 설정, 메모리에만 반영된 상태
context.save() // context 변경사항을 영구 저장소에 반영

Delete

제거가 되기 위해서는 context(메모리) 상에 인스턴스가 존재해야만 한다.

  • context 에 삭제될 관리 객체 인스턴스를 반영
  • context.delete(object): context 내에서 제거
  • context.save(): 영구저장소에 반영
    context 가 정상적으로 반영된 이후 CollectionView 또는 TableView 등에서 제거 후 UI를 반영하면 된다.

Edit

  • .setValue: 관리 객체 인스턴스의 attribute 값 수정
  • context.save(): 영구 저장소에 반영

실습: 릴레이션 관계 사용

Relationship

Relationship 생성

  • Entity 편집기: Entity: Relationship 상에 참고할 entity명으로 relationship을 생성한다.
  • Destination 값을 참조할 Entity 로 설정한다.
  • Type 속성을 알맞게 설정한다. (To One / To Many)
  • 참조하고 있는 Entity가 제거될시인 Delete Rule 속성을 설정한다.
  • 상호참조가 필요할시에 동일하게 작업한다. (Relationship: Inverse 값을 설정하면 이전 Relationship 의 Inverse 도 자동으로 반영된다.)

커스텀 모델 클래스를 relation 내에 저장

  • 커스텀 모델 클래스를 생성 (context 상에 반영)
  • 커스텀 모델 클래스 값 설정 (setValue)
  • 위 커스텀 모델 클래스를 relation을 지닌 커스텀 모델에 반영
@objc(Page_Core)
public class Page_Core: NSManagedObject {
	@NSManaged public var vid: Int64
    @NSManaged public var materialImage: Data
    @NSManaged public var layoutType: String
    @NSManaged public var problems: NSSet? // 릴레이션에 의해 생성된 attribute
    
    @objc(addProblem:)
    @NSManaged public func addToProblems(_ value: Problem_Core)
}
let problem = Problem_Core(context: context) // 커스텀 모델 생성
problem.setValues(problem: ProblemDTO) // 커스텀 모델 값 설정
section.addToProblems(problem) // 커스텀 모델 relation 내에 반영

1개 입장에서 pageCore.addToProblems(problem) 를 통해 등록해도 되고,
n개 입장에서 problem.page = pageCore 식으로 등록해도 같은 결과를 가져옵니다.
이는 서로 역참조 관계이기 때문.

let problems = pageCore.problems?.array as! [Problem_Core]

식의 코드를 통해 relation에 연결되어 있는 커스텀 모델 클래스들을 배열로 접근할 수 있습니다.
.array 속성을 통해 NSSet or NSOrderedSet -> Array 로 반환합니다.
중요한 점은 problems 는 "참조" 정보들(메모리 주소들)이며, 실제 데이터를 담고 있지는 않다.
따라서 크기가 그만큼 커지지는 않는다.

let fetchRequest: NSFetchRequest<Problem_Core> = ProblemCore.fetchRequest()
let predict = NSPredicate(format: "page == %@", pageCore)
fetchRequest.predicate = predict
let sort = NSSortDescripter(key: "pid", ascending: true) // 만약 pid 값으로 오름차순이 필요하다면
fetchRequest.sortDescriptors = [sort]

context.fetch(fetchRequest) // fetch 실행

위 코드를 사용해도 동일하게 불러올 수 있습니다.

object

objectID: NSManagedObjectID
object.objectID 값을 통해 특정 커스텀 모델의 identifier 값을 알 수 있습니다.
context.object(with: objectID) 메소드를 통해 특정 커스텀 모델을 접근할 수 있습니다.
단, 데이터베이스 상의 PK 값과는 다르게 context 값이 수정될 때 마다 변경될 수 있으므로 그때그때 context 에서 참조해서 사용하는 것이 안전합니다.

profile
 iOS Developer

0개의 댓글