Swift에서 데이터를 저장하는 방식은 UserDefaults, CoreData, Realm 등이 있습니다.
우선, UserDefaults는 간단한 String, Int 등의 단일 데이터 저장에 적합하지만, 객체 형태로 여러 데이터를 저장하거나 관리하는 데는 한계가 있습니다. 반면, CoreData는 객체로 데이터를 저장할 수 있어 복잡한 데이터 관리가 가능하지만, Xcode를 통해 Entity를 생성하고 데이터를 읽거나 추가, 수정하는 과정이 다소 복잡하며 초기 설정이 번거롭습니다. 또한, 스레드 관리와 데이터 관계 설정이 필요해 개발 과정에서 추가적인 구현과 관리가 요구됩니다.
이러한 이유로 저는 보다 간단하면서도 효율적인 데이터 관리를 위해 Realm을 선택하였습니다. 이제, Realm의 특징과 장점에 대해 자세히 알아보면서 제가 램을 사용할 때 어떤 부분을 신경썼는지 알아보도록 하겠습니다~
Realm
모바일 및 IoT 애플리케이션을 위한 경량 데이터베이스로, 로컬 데이터 저장과 동기화를 간단하게 처리 가능
장점
- 빠르고, 쓰기 쉽고, 기능이 많고, 크로스 플랫폼 지원
이 장점에 대해서 하나씩 살펴보겠습니다.
1. 빠르다
위의 그래프만 봐도 속도 면에서 다른 DB들을 압도하는 모습을 볼 수 있습니다.
(사용할 때 직접적으로 느끼진 못했습니다. 아마 제가 사용한 데이터의 양이 많지 않아서 인 것 같아요)
2. 쓰기 쉽다
3. 결과 자동 업데이트
Realm과 관련된 내용을 찾아보다가 유튜브에 Realm에 대한 발표 영상을 보게 되었는데요, 그 영상에서 간단하면서도 좋은 예시인 것 같아 캡쳐해왔습니다.
위의 코드만으로 체이닝 쿼리를 사용하면서 간편하게 코드를 구현해 놓은 모습을 볼 수 있음과 동시에, 데이터에 변화가 생겼을 때 결과값이 자동적으로 바뀜을 알 수 있었습니다.
저도 사실 데이터에 수정이 일어나면 새로 값을 가져왔었는데, 이 영상을 보고 호다닥 코드를 수정했답니다 ㅎ.ㅎ
4. 기능이 많다
기능이 많다? 살짝 모호한 표현일 것 같긴합니다만..!
알게모르게 유용하게 쓰고 있는 부분들이 정말 많아서 몇 가지만 설명해보겠습니다!
Realm Studio
(⭐️실기기 빌드에선 불가합니다..!!!!!)
위의 기능을 통해 앱에서 실시간으로 진행되는 CRUD 작업을 눈으로 확인할 수 있습니다. 특히, Realm Studio 내에서는 데이터를 직접 추가하거나 삭제할 수 있어, 더미 데이터를 미리 생성해 놓고 테스트하거나 작업할 때 매우 편리합니다. 이를 통해 데이터의 흐름을 확인하고, 앱 개발 중 발생할 수 있는 데이터를 다루는 문제를 빠르게 해결할 수 있었습니다.
또한, 옆에 살포시 나와있는 Documents파일을 보실 수 있는데요! 해당 파일에서는 FileManager를 통해 이미지를 저장/삭제했을 때 이미지가 실시간으로 추가되고 삭제되는 것을 확일해 볼 수 있습니다. 요 부분은 RealmStudio 기능은 아닌데, Realm Studio까지 가는 길에 무조건 보이는 부분이기 때문에 설명해봤습니다.
관계형 데이터 처리 구조 - List, LinkingObjects
final class DateList: Object,ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var id: ObjectId
@Persisted var mission: List<MissionData>
@Persisted var today: Date
convenience init(mission: List<MissionData>, today: Date) {
self.init()
self.mission = mission
self.today = today
}
}
final class MissionData: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var id: ObjectId
@Persisted var todayDate: Date
@Persisted var wakeUpTime: Date?
@Persisted var startTime: Date
@Persisted var endTiem: Date
@Persisted var mission: String
@Persisted var missionComplete: Bool
@Persisted(originProperty: "mission") var main: LinkingObjects<DateList>
convenience init(todayDate: Date, wakeUpTime: Date, mission: String, missionComplete: Bool, startTime: Date, endTime: Date) {
self.init()
self.todayDate = todayDate
self.mission = mission
self.missionComplete = missionComplete
self.wakeUpTime = wakeUpTime
self.startTime = startTime
self.endTiem = endTime
}
}
위의 코드를 분석해보면 DataList를 통해 하루에 해당하는 데이터를 관리합니다. MissionData의 경우 개별 미션에 대한 정보인데요
List를 통해 1:N관계임을 설정해 하루에 여러개의 미션이 연결되는 것을 구현했습니다.
main 속성은 LinkingObjects<DateList>를 사용하여 역참조를 구현하여 각 미션이 어떤 날짜(DateList)와 연결되어 있는지 확인 가능하도록 구현했습니다.
이처럼 구현하게 되면 특정 날짜의 미션을 조회하기도 편하고, 특정 미션의 날짜를 쉽게 추적이 가능해 데이터를 직관적으로 관리하는데 도움이 됐습니다.
마이그레이션
데이터 모델을 변경하게 되었을 때, 데이터를 손실하지 않고 새로운 스키마로 변환하는 기능
func setupRealm() {
let config = Realm.Configuration(
schemaVersion: 2,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 2 {
newObject?["isMission1Complete"] = false
newObject?["isMission2Complete"] = false
newObject?["isMission3Complete"] = false
}
}
)
Realm.Configuration.defaultConfiguration = config
}
Realm 마이그레이션은 스키마가 변경되더라도 기존 데이터를 손실 없이 유지할 수 있도록 지원합니다.
단순한 설정으로 간단한 변경 사항은 자동 처리하고, 복잡한 변환 작업도 사용자 정의 로직을 통해 유연하게 대응할 수 있습니다.
5. 크로스 플랫폼 지원
이 부분은 사실 제가 경험할 방법이 없어서 pass~
사실 스키마 정도 공유하는 것 일텐데 흠.. 잘 모르겠슴다!! ㅎ.ㅎ
혹시라도 후에 사용하게 되면 블로그 끄적끄적 해보겠습니다!
How to use - 개인 출시앱 <모닝글로리>
그럼 이제 제가 위의 특성들을 바탕으로 어떻게 Realm을 사용했는지 설명해보겠습니다..!
- 싱글톤 패턴으로 사용하지 않는 이유?
Realm을 사용하다보면 한 번쯤은 생각해볼만한 내용인 것 같습니다.
let realm = try! Realm()
Realm을 사용하다 보면 위의 코드를 반복적으로 작성하게 되는 경우를 발견할 수 있는데요. 이로 인해 지속적으로 인스턴스를 생성하게 되면서 싱글턴 패턴에 대한 고민이 들기도 합니다. 하지만 Realm은 동일한 스레드에서 액세스된다는 점이 보장되며, Realm 객체는 생성된 스레드에서만 사용해야 합니다.
그렇지 않을 경우, 스레드 교차로 인해 크래시가 발생할 위험이 있습니다.
하지만 Realm의 가장 큰 장점 중 하나는, 각 메서드 내에서 Realm 객체를 새로 생성해도 문제가 없다는 점입니다. 이렇게 생성된 객체는 해당 시점의 스냅샷 데이터를 반환하므로, 다른 스레드에서 데이터가 변경되었더라도 항상 일관된 데이터를 볼 수 있습니다. 따라서 데이터의 변경 여부를 걱정하지 않아도 됩니다.
- Realm 코드의 정리와 구조화: Repository 패턴의 도입
위의 싱글톤에 대한 고민도 중복 되는 코드가 많아지다 보니 고민하게 되었는데요, 이 레포지토리 패턴을 사용하고자 하는 생각 역시 중복되는 코드에 대한 불편함에서 시작되었습니다.
또한 ViewModel에서 데이터베이스 로직을 직접 다루는 경우가 대다수였기 때문에
- ViewModel이 비대해짐
- Realm에 대한 의존도가 높아짐
- 중복 작업 반복
와 같은 불편함이 눈에 띄기 시작했습니다.
이와 같은 불편함을 해소하기 위해 Repository 패턴을 사용하여 데이터 소스와 비즈니스 로직을 분리했습니다.
데이터 접근 로직을 캡슐화 하기 위해 Repository 내부에 코드를 숨기고, ViewModel에서는 Repository를 통해 데이터를 처리하도록 구현했습니다.
구현 방법
1. 공통 인터페이스 정의
Realm 외 다른 데이터베이스로 교체하거나 테스트용 Mock Repository를 사용하기 위해, 공통 인터페이스인 DatabaseRepository를 설계
protocol DatabaseRepository {
func fetchData<T: Object>(of type: T.Type) -> [T]
func saveData<T: Object>(data: T)
func removeData<T: Object>(data: T)
}
2. RealmRepository 구현
Realm을 활용해 데이터를 저장, 조회, 삭제하는 RealmRepository를 구현
final class RealmRepository: DatabaseRepository {
private let realm: Realm
init(realm: Realm) {
self.realm = realm
}
func fetchData<T: Object>(of type: T.Type) -> [T] {
return Array(realm.objects(T.self))
}
func saveData<T: Object>(data: T) {
try? realm.write {
realm.add(data)
}
}
func removeData<T: Object>(data: T) {
guard let thawedData = data.thaw() else { return }
try? realm.write {
realm.delete(thawedData)
}
}
}
3. 의존성 주입(DI, Dependency Injection) 활용
ViewModel 에서 RealmRepository가 아닌 DatabaseRepository 프로토콜을 의존하도록 설계
final class ToDoVM {
private let repository: DatabaseRepository
init(repository: DatabaseRepository) {
self.repository = repository
}
func fetchItems<T: Object>(of type: T.Type) -> [T] {
return repository.fetchData(of: type)
}
}
위의 방식대로 기존의 여기저기 흩어져 있던 데이터 접근 로직을 리팩토링한 결과, 중복 코드가 눈에 띄게 줄어들었습니다. 또한 데이터베이스와 관련된 모든 로직이 Repository 내부에 모아지다 보니, 데이터베이스 변경 사항이 필요할 때 Repository 내부만 수정하면 되는 구조로 개선되었습니다.
이전에는 같은 코드를 변경하기 위해 여러 페이지를 돌아다녀야 했던 번거로움이 있었지만, 이제는 유지보수의 효율성이 크게 향상된 것을 직접 체감할 수 있었습니다.
뿐만 아니라, 새로운 기능이 필요하거나 기존 로직에 추가적인 코드가 요구될 경우에도 Repository 내부에서 처리할 수 있어, 확장성과 재사용성이 높은 구조로 변화된 점 역시 큰 장점으로 느껴졌습니다.
마무리
Realm의 간단한 설정과 다양한 기능 덕분에 개발 과정에서 데이터 관리를 매우 효율적으로 할 수 있었습니다.
특히, 스레드 안전성 강제, 자동 동기화, 실시간 데이터 업데이트와 같은 특징은 데이터를 다루는 부담을 크게 줄여주었습니다.
또한, Repository 패턴을 도입해 코드의 일관성과 재사용성을 높였고, 데이터베이스 변경에 유연하게 대응할 수 있는 구조를 만들어 유지보수성과 확장성 측면에서도 큰 개선을 경험할 수 있었습니다.
.
.
.
.
'Swift' 카테고리의 다른 글
[iOS] About Copy-On-Write(COW) (0) | 2024.09.28 |
---|---|
[iOS] About DI, DIP (2) | 2024.09.26 |
[iOS] About 메모리 구조 (1) | 2024.09.21 |
[iOS] About StateObject, ObservedObject (0) | 2024.09.18 |
[iOS] About ObservedResults, ObservedRealmObject (1) | 2024.09.16 |