본문 바로가기

MOBILE/ios

[iOS] UserDefaults와 커스텀 프로퍼티 리스트를 이용하여 데이터를 영구 저장하기

앱의 데이터는 실행 중에 메모리에 저장되지만 이는 휘발성이므로 앱을 종료하면 사라진다.

하지만 사라지지 않아야 하는 데이터는 어떻게 보관할 수 있을까?

예를 들면 메모장의 데이터나 일정과 같은 데이터들을 사라지지 않게 보관하는 방법에 대해 알아보자

 

코코아 터치 프레임워크는 다음과 같이 다양한 수준의 데이터 저장 방식을 제공한다.

 

UserDefaults/ 커스텀 프로퍼티 리스트

: key-value 형태로 저장되는 간단한 데이터를 저장하는 방식

 

코어 데이터/ SQLite

: 지속적으로 추가되거나 구조적으로 관리가 필요한 데이터를 저장하는 방식

 

아카이빙 (Archive)

: 일반 데이터 타입으로 표현 불가능한 데이터를 저장하는 방식

즉 특정 순간 인스턴스 객체의 데이터 자체를 캡쳐하여 그대로 저장하는 방식이므로 복잡한 데이터의 저장을 위한 방법이다.

 

이 중에서 가장 간단한 데이터들을 앱 내부 저장소에 저장하는 방식인 프로퍼티 리스트에 대해 알아보자


프로퍼티 리스트

: 객체의 내용을 바이트 단위로 변환하여 파일에 기록하거나 전달하는 객체 직렬화를 위한 XML 형식의 파일

 

xcode를 통해 프로젝트를 생성하면 기본으로 생성하는 파일 목록 중 Info.plist 라는 파일이 있다. 이 파일은 앱의 빌드와 실행에 필요한 환경 값을 저장하는 파일이다.

이렇게 plist 확장자를 가진 파일이 바로 프로퍼티 리스트이다.

 

간단한 데이터를 저장하는 것에 사용되기 때문에 대부분 앱의 공통 데이터, 설정 정보 등을 저장하는 데에 사용된다.

 

property list

프로퍼티 리스트는 단순한 데이터를 XML 포맷에 맞추어서 key-value 형태로 저장한다. 즉 딕셔너리 형태로 데이터를 저장한다.

이때 저장되는 데이터는 가장 상위레벨로 추상화된 형태이다.

 

예를 들어 swift와 Foundation 프레임워크, Core Foundation 프레임 워크에서 제공하는 NSString, String, CFstring 객체들은 이대로 저장되는 것이 아니라 "문자열" 이라는 공통적인 상위 레벨의 타입으로 추상화되어 저장된다.

따라서 데이터를 추출할 때에도 필요에 따라 캐스팅하여 사용할 수 있다.

이렇게 추상화한 형태로 숫자, 배열, 딕셔너리 등등 모든 스위프트 아키텍쳐 데이터 타입들은 추상화하여 저장된다.

이때 주의할 점은 데이터 자체가 추상화되는 것이 아니라 타입이 추상화 된다는 것이다.

 

프로퍼티 리스트를 [Open As] 를 통해 소스코드 형태로 볼 수 있다.

이 경우 다음과 같은 xml 형식의 파일이 보이게 된다.

 

파일 가장 위에는 프로퍼티 리스트에 대한 메타 정보가 있다.

1.0 version의 xml을 사용하고, 인코딩은 UTF-8로 되어 있다는 것을 알려준다.

그 다음 엘리먼트의 형식은 다음과 같은 문서를 따른다는 것을 알려준다.

 

property list source code version

위에서 보이는 항목들은 노드라고 부른다. 즉 트리구조를 재귀적으로 따르고 있다.

예를 들면, 위의 경우 3개의 노드를 가지는 것이다.

프로퍼티는 딕셔너리로 저장되기 때문에 최상위 노드는 반드시 딕셔너리여야 한다.

<key> : 항목명

<???> : 항목의 값에 해당하는 자료형 (예를 들어, <integer>는 정수)

 

이렇게 정의된 프로퍼티 리스트는 가장 먼저 ipa 파일에 반입된다. 이 ipa 파일은 앱스토어에 등록시 사용되는 배포 파일이다.

프로퍼티 리스트는 앱 번들(App Bundle) 영역에 설치된다.

앱 번들: 앱의 각종 소스코드나 이미지 등 리소스 파일이 저장되는 곳

Bundle.main.path 로 경로를 설정하여 접근 가능하다

이 앱 번들 영역은 앱 업데이트 시 번들 영역에 저장된 데이터를 모두 덮어쓰기 하기 때문에 데이터가 유실될 수 있다.

따라서 영구 보존이 필요한 데이터는 .documentDirectory 등의 내부 저장소를 이용해야 한다.

 


UserDefaults

: 기본 저장소를 다루기 쉽도록 ios에서 제공하는 객체

 

앱은 데이터를 저장하는 영역인 기본 저장소를 가지고 있다. 이는 프로퍼티 리스트를 기반으로 하며 당연히 xml 형식으로 데이터를 저장한다. ios는 간단한 데이터를 저장할 때 기본 저장소를 이용하는 것을 권장한다. 이러한 기본 저장소를 쉽게 다룰 수 있도록 userdefaults 객체를 제공한다.

 

UserDefaults

런타임 환경에서 동작하는 객체이다. 즉 앱이 실행되는 동안에만 기본 저장소에 접근하여 데이터의 read, write가 가능하다는 것이다.

이때 읽을 데이터가 없는 경우 기본적으로 nil을 반환한다.

 

또한 userDefaults는 싱글톤 패턴이다. 즉 전체에서 딱 하나의 인스턴스만 가지도록 시스템이 보장해준다.

따라서 인스턴스 생성이 불가하고 이미 생성된 하나의 인스턴스를 참조해서 사용해야한다.

인스턴스 생성이 불가하고 참조만 가능해야 하기 때문에 static 키워드로 클래스 레벨로 정의되어 있다. (클래스 메서드, 클래스 프로퍼티)

 

여기서 중요한 점은 userDefaults는 데이터를 다룬다는 것이다. 즉 하나의 기본 저장소에 데이터를 읽고 쓰게 된다.

이는 race condition의 발생 가능성이 있다. 즉 하나의 shared data에 여러 객체가 동시에 접근해서 사용할 수 있다.

이러한 문제는 데이터의 무결성을 보장해주지 않는다. 데이터가 오류로 인해 유실되거나 잘못 변경될 수 있다는 뜻이다.

다행히 UserDefaults는 Thread-safe 설계이다. blocking 알고리즘을 사용하여 따라서 한 스레드가 접근하고 있을 경우 다른 스레드의 접근을 막아준다.

내부적으로는 semaphore 또는 lock을 써서 제어하는 것 같음... 정확한 블로킹 알고리즘은 찾아봐야겠다.

 

 

UserDefaults API

userDefaults가 제공해주는 메소드를 알아보자

 

1. data write method

set(_:forKey:): userdefaults 전용 메소드이다. key에 해당하는 값을 value에 넣어주면 저장 가능하다.

 

 

또한 userdefaults는 딕셔너리 형태로 데이터를 저장하기 때문에 당연하게도 setValue 메소드도 사용 가능하다.

 

 

2. data read method

저장된 value의 타입에 맞는 method를 다양하게 쓸 수 있다.

string(forkey:), integer(forkey:), float, double, data, array, dictionary, object 등등 각자에 맞게 읽어온다.

이때 data type은 Base64 로 인코딩된 데이터를 읽는 용도이다.

즉 바이너리 데이터를 아스키 코드를 사용하여 문자열로 바뀐 데이터를 읽을 때 사용한다.

object는 범용 타입이다. 즉 return이 Any 타입이기 때문에 반드시 캐스팅을 해야한다.

 

예를 들면 다음과 같다.

UserDefaults 객체는 다음과 같이 UserDefaults.standard 를 통해 참조 가능하다.

 

let userDef = UserDefaults.standard

let result1 = userDef.string(forkey: "name")
let result2 = userDef.integer(forkey: "age")
let result3 = userDef.object(forkey: "gender") as? NSString

 

이때 userDefaults 로 읽은 데이터는 다음과 같은 이유로 옵셔널 타입이다.

1. key에 해당하는 파라미터가 잘못된 경우
2. key는 올바르지만 value가 없는 경우
3. 값을 읽어왔지만 캐스팅이 불가한 경우

결국 nil의 가능성을 배제할 수 없기 때문에 optional type이 된다.

 

userDefaults는 In-memory Caching 을 사용한다.

in-memory caching은 읽은 데이터를 메모리에 저장해두고 다음번 호출 시 이 메모리에서 읽는 캐시를 의미한다.

하지만 이러한 방식은 메모리와 저장소간의 데이터 불일치를 만들어낼 수 있다.

 

예를 들어 데이터를 읽어서 메모리에 저장해왔지만, 바로 set을 통해 갱신했다고 하면

메모리는 예전 데이터, 기본 저장소는 새로운 데이터가 있어 서로 일치하지 않게 된다.

따라서 데이터를 쓰고 나면 반드시 싱크를 맞추어서 메모리의 데이터도 갱신해야 한다.

이 과정은 복잡하지만 다행히 userDefaults의 API level에서 메소드로 제공한다.

바로 synchronize() 메소드이다.

 

다음과 같이 사용한다.

let userDef = UserDefaults.standard

userDef.set(23, forKey: "age")
userDef.set("josushell", forKey: "name")

userDef.synchronize()

 

데이터를 쓰고 나면 항상 이 synchronize() 메소드를 호출하여 싱크를 맞추는 작업이 필요하다.