본문 바로가기

iOS

Database인데 Realm을 곁들인

728x90

나만 이거 지금까지 리얼엠으로 읽은 것임? 어이가 없어. 나는. 내 세계가 부정당했어.

 

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인 것들만 보여주도록 기능을 구현했다.