나만 이거 지금까지 리얼엠으로 읽은 것임? 어이가 없어. 나는. 내 세계가 부정당했어.
Realm을 공부하기 전에 .. 먼저 .. Database를 살짝 공부해보자
Database
데이터베이스란?
- 데이터를 저장한 파일들의 집합체
- Raw Data(수집된 데이터 그 자체)가 방대한 양으로 이루어져 있고, 파일로 저장되어 있던 출력물로 있던 효율적으로 저장된 집합체
- 도서관 시스템, 사원 관리, 고객 관리 등
DBMS란?
DB를 쉽게 만들고 관리하는 여러 프로그램들이 모여 하나의 시스템으로 갖춰진 프로그램
(= 데이터 베이스를 관리하기 위한 소프트웨어를 지칭)
대부분들의 DB들이 DBMS를 통해 만들어지고 운영되기 때문에 의미를 혼용해서 사용하기도 한다.
계층형, 관계형, 객체 관계형, NoSQL 등 여러 종류가 있고, 여러 종류의 DBMS 중 가장 많이 사용되는 것이 관계형 데이터베이스(=RDB)이다.
RDBMS
관계형 데이터베이스를 생성, 수정, 관리할 수 있는 소프트웨어
- 명확한 데이터 구조
- 테이블 - 컬럼 형태로 데이터를 저장
- 테이블과 테이블 간의 연관 관계를 통해서 필요한 정보를 구현
- 중복 데이터 저장 방지를 위해 정규화 (-> 데이터의 독립성이 높아짐)
- 정규화 : 데이터의 정합성을 확보하기 위해 테이블을 분할해 생성하는 것
- 비용과 시스템 복잡성
- Oracle 등을 사용할 경우 비용적 부담이 발생
- 시스템이 복잡해질수록 Query문이 복잡해지고 성능 저하가 이렁날 수 있다.
- 수직적 확장 가능
- 수평적 확장이 어려워, 수직적 확장을 하지만 한계에 직면할 수 있다.
RDBMS의 구조
- Scheme, Table, Column, Record, Row
<Filmography>
Name | Title | Date |
김수현 | 별에서 온 그대 | 2013 |
김수현 | 해를 품은 달 | 2012 |
현빈 | 사랑의 불시착 | 2019 |
위와 같은 테이블 구조가 있다고 할 때,
- Filmography : 하나의 테이블을
- Name, Title, Date : 하나의 컬럼을
- 현빈/사랑의 불시착/2019 : 하나의 레코드를 의미한다.
- Schema
- DB의 구조와 데이터들이 갖고 있는 제약 조건에 대한 전반적 명세를 기술한 것
- Table
- Row 와 Column 으로 이루어진 데이터들의 집합
- 일관적 특징을 가진, 중복되지 않은 데이터를 담기 위한 하나의 데이터 집합
- Column (= Attribute)
- 테이블을 구성하는 정보 중 세로로 묶은 데이터
- 데이터 타입을 지정할 수 있다.
- 테이블에서 특정한 레코드의 내용을 찾기 위해서는 Column의 내용을 기준으로 찾는다.
- Record (= Row)
- 테이블을 구성하는 정보 중 가로로 묶은 데이터
- 일반적으로 한 객체에 대한 정보를 갖고 있다.
- 기본키(PK)로 구별할 수 있다.
테이블의 제약 조건
Primary Key, Unique Key, Foreign Key
<Filmography>
ID | actorID | Title | Date |
1 | 186 | 별에서 온 그대 | 2013 |
2 | 186 | 해를 품은 달 | 2012 |
3 | 35 | 사랑의 불시착 | 2019 |
<Actor>
ID | Name | Birth |
186 | 김수현 | 19880216.. |
35 | 현빈 | 19780101.. |
250 | 김수현 | 19890303.. |
위와 같이 테이블 구조가 두개가 존재할 때,
각 테이블의 ID는 Primary Key가 되고 Filmograhpy 테이블에서 actorID는 Foreign Key를, Actor 테이블에서 Birth는 Unique Key가 된다.
- 기본키 : Primary Key (= PK)
- 테이블에 대한 고유 식별값 : 테이블에 오직 한 개만 생성 가능
- 레코드 검색의 기준이기 때문에 빠른 검색을 위해 내부적으로 인덱싱을 한다.
- 중복될 수 없고 비어있을 수 없다. (Not nil)
- ex) 주민등록번호, 학번, 사원 번호
- 외래키 : Foreign Key (= FK)
- 어떤 테이블의 기본키가 다른 테이블의 컬럼에 들어 있는 경우를 지칭, 다른 테이블의 PK
- 외부 식별자 키로 테이블 간의 관계를 의미한다.
- 두 테이블 간 종속이 필요한 관계라면 접점의 컬럼을 FK로 지정하여 서로 참조할 수 있도록 관계를 명시해야 한다.
- Unique Key
- PK는 아니지만 값의 중복을 허용하지 않기 위해 컬럼에 unique 제약을 설정
- 비어있을 수 있다. (nil 가능)
🤔 Indexing?
- RDBMS에서 검색 속도를 높이기 위해 사용하는 것
- 데이터의 위치를 식별하는데 사용하는 기능
- 별도의 Key-Value 생성되는 형태
- 테이블의 PK는 자동으로 인덱스 지정
- 효과적으로 사용하려면 정규화가 잘 되어 있어야 한다.
- 정규화가 되어 있지 않으면 조합할 수 있는 인덱스가 많아지고
- 이는 성능과 속도 저하로 이어진다.
🤔 Migration
- 하드웨어, 소프트웨어, 네트워크 등 넓은 범위에서 사용되고 있는 개념으로 현재 운영 환경으로부터 다른 운영 환경으로 옮기는 작업을 말한다.
- 데이터베이스에서는 스키마 버전을 관리하기 위해 수행한다.
Realm
렘을 사용하기 위해서 가장 먼저 할 것 역시 테이블을 설계하는 것이다.
Realm Table 설계하기
Scheme 설정
- 테이블과 컬럼 구조 설계
- 저장이 될 데이터의 타입과 옵셔널 여부에 대한 특성을 각 컬럼에 부여
- 컬럼 중 하나를 Primary Key로 설정 : 보통 ID에 대한 컬럼을 별도로 지정
🤔 어떤 타입의 모델로 테이블을 구성해야 할까?
Realm MongoDB Documentaion을 통해서 확인할 수 있다.
렘에서 Swift 언어로 지원가능한 타입을 살펴보고 그에 맞게 데이터 모델을 구성하면 된다.
여기서 Required와 Optional로 나뉘어져 있는 것은, 데이터 중 필수적으로 그 값이 필요한 정보와 그렇지 않은 정보로 나눈 것이다. (예를 들어서 학사정보 시스템을 만든다고 할 때 학번은 필수지만 부전공은 반드시 필요한 데이터가 아니다.)
Realm을 활용한 쇼핑리스트 앱 만들기
여기서는 UI 코드를 따로 작성하지는 않겠음 .. 굉장히 쉬운 UI이므로 ..
1. Scheme에 맞게 Realm Model 설계
쇼핑리스트에서 필요한 데이터는 아래와 같다.
- 이름 (필수)
- 체크 유무 (필수)
- 날짜 (선택)
import Foundation
import RealmSwift
class Product: Object {
@Persisted var name: String
@Persisted var check: Bool
@Persisted var date: Date?
// PK(필수): Int, UUID, ObjectID
@Persisted(primaryKey: true) var objectId: ObjectId
convenience init(name: String, check: Bool = false, date: Date) {
self.init()
self.name = name
self.check = check
self.date = date
}
}
위와 같이 데이터 모델을 만들 수 있다.
이 때, Product이라는 이름의 테이블을 하나 생성한 것이므로 이 테이블의 Primary Key를 설정해야 한다.
별도의 Stirng, Int로 설정할 수도 있고 UUID / ObjectID를 사용할 수 있다.
공식문서의 Usage Example - Define a Realm Object Scheme 을 확인하면 된다.
쇼핑리스트에서는 ObjectId를 통해 PK를 설정했다.
// PK(필수): Int, UUID, ObjectID
@Persisted(primaryKey: true) var objectId: ObjectId
Realm CRUD
Create
가장 먼저 할 일은 모듈을 설치하는 것이다.
SPM으로 설치 후 렘 코드를 작성할 곳으로 가서 import를 하면 된다. (만약 여기서 No Such Module 에러가 발생한다면 새로 빌드하면 해결된다.)
import RealmSwift
Realm 파일에 접근하는 상수를 선언한다.
private let localRealm = try! Realm()
렘 테이블에 내용을 추가, 수정, 삭제 등을 할 때 렘 테이블 경로에 접근하는 부분으으로,
iOS Document Folder 안의 default.realm의 위치를 찾는 코드이다. (이 파일 안에 앱에서 사용하는 realm에 대한 정보가 들어 있다.)
Realm에 작성한 데이터를 저장한다.
@objc func touchUpDoneButton() {
if let text = textField.text {
let task = Product(name: text, check: false, date: Date())
try! localRealm.write {
localRealm.add(task)
}
dismiss(animated: true)
}
}
Read
위의 과정까지 realm에 데이터를 저장했다면, 저장된 데이터를 불러올 수 있다.
// MARK: - Property
private let localRealm = try! Realm()
private var tasks: Results<Product>! {
didSet {
listTableView.reloadData()
}
}
데이터가 저장 되어 있는 realm에 접근하기 위한 상수와 database에 저장되어 있는 데이터를 앱에서 사용하기 위한 변수를 선언한다.
- 렘 파일에 접근하는 상수 선언
- 렘테이블에 내용을 추가, 수정, 삭제 등을 할 때 렘 테이블에 접근하는 부분
- 즉 도큐먼트 폴더 내의 default.realm 위치를 찾는 코드
- 렘에서 읽어온 데이터를 담을 배열 선언
- 렘의 데이터 중 원하는 내용을 필터, 정렬해서 갖고 올 데이터를 보관할 공간
- 렘의 테이블을 직접 수정하지 않고 데이터만 갖고 와서 화면에 사용하는 구조
// MARK: - Custom Method
private func getRealmData() {
tasks = localRealm.objects(Product.self).sorted(byKeyPath: "date", ascending: false)
}
그리고 realm에 접근해서 데이터를 가져올 수 있다.
tasks = localRealm.objects(Product.self)
이렇게 작성하면 realm에 저장되어 있는 순서대로 (= 저장한 순서로) 데이터를 갖고 오지만
.sorted(byKeyPath: "date", ascending: false)
뒤로 .sorted를 추가해서 원하는 방식으로 정렬해서 데이터를 가지고 올 수 있다.
렘에서 데이터를 가지고 오면 데이터의 순서가 보장이 되지 않는다.
일관적인 데이터 정렬 유지를 위해 데이터를 갖고 올 때 특정 컬럼을 기준으로 정렬을 하여 가져오는 것이 사용자의 UX에 좋다.
Update
이미 작성 되어 있는 레코드에 대해서 접근하여 원하는 컬럼에 대한 값을 수정할 수 있다.
쇼핑 리스트 앱에서는 구매한 항목에 대해서 체크 표시를 눌러 Bool값을 수정하는 기능을 구현해보았다.
Cell에 checkButton을 만들어두고 Protocol 방식을 통해서 기능을 구현했다.
// MARK: - Custom Delegate
extension ListViewController: ListTableViewCellDelegate {
func touchUpCheckButton(index: Int) {
try! localRealm.write {
tasks[index].check.toggle()
}
}
}
해당 셀을 눌렀을 때 그 셀의 check의 값을 변경하는 방식으로 Update를 구현할 수 있다.
위의 경우는 하나의 레코드에서 특정 컬럼 하나만 변경한 것이지만
원하는 플로우에 따라서 하나의 테이블에 있는 특정 컬럼 전체를 변경할 수도 있고 (Product 모델에서 구매유무에 대한 키 값을 "check"로 설정했으므로)
tasks.setValue(true, forKey: "check")
하나의 레코드에서 여러 컬럼을 변경할 수도 있다.
localRealm.create(UserDiary.self, value: ["objectId" : self.tasks[index].objectId, "date" : Date()], update: .modified)
🔴 주의할 점은 데이터를 바꿔주고 나서, UI 업데이트를 적절한 타이밍에 해주어야 한다.
데이터의 변경 + 데이터를 새로 읽어오기 + UI reload
Delete
데이터를 삭제할 수도 있다.
쇼핑 리스트 앱에서는 테이블 뷰를 통해서 UI를 구성했고, swipe 방식으로 테이블의 레코드를 삭제하는 기능을 구현했다.
// MARK: - UITableView Protocol
extension ListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
do {
try localRealm.write {
removeImageFromDocument(fileName: "\(tasks[indexPath.row].objectId).jpg")
localRealm.delete(tasks[indexPath.row])
}
} catch let error {
print(error)
}
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
}
이외에도 Realm Query를 통해서 원하는 데이터를 sort / filter 할 수 있다.
공식문서를 통해 보다 많은 filter 방식을 찾아볼 수 있다.
쇼핑 리스트 앱에서는 구매 완료한 목록만 모아볼 수 있는 필터 버튼을 만들었다.
private lazy var menuItems: [UIAction] = {
return [
UIAction(title: "제목순", image: UIImage(systemName: "arrow.down.circle"), handler: { _ in
self.tasks = self.localRealm.objects(Product.self).sorted(byKeyPath: "name", ascending: true)
}),
UIAction(title: "날짜순", image: UIImage(systemName: "square.and.arrow.up"), handler: { _ in
self.tasks = self.localRealm.objects(Product.self).sorted(byKeyPath: "date", ascending: false)
}),
UIAction(title: "구매완료", image: UIImage(systemName: "square.and.arrow.down"), handler: { _ in
self.tasks = self.localRealm.objects(Product.self).filter("check == true")
})
]
}()
private lazy var menu: UIMenu = {
return UIMenu(title: "", options: [], children: menuItems)
}()
UIMenu를 통해서 구현했고, 구매완료 버튼을 누르면 filter를 통해 테이블의 컬럼 중 check에 대해서 true인 것들만 보여주도록 기능을 구현했다.
'iOS' 카테고리의 다른 글
셀의 재사용 메커니즘 (0) | 2022.08.28 |
---|---|
Realm에 Repository Pattern을 곁들인 (0) | 2022.08.28 |
App Sandbox 그리고 Files (2) | 2022.08.27 |
백업/복구 (+ 백업 리스트 UI 및 Custom Progress View) (3) | 2022.08.26 |
Access Control - Basic to Advanced (0) | 2022.08.16 |