2017년 1월 10일 화요일

UserDefaults (NSUserDefaults) 에 대한 소소한 이야기

UserDefaults (Objective-C 에서는 NSUserDefaults) 는 대체로 앱의 설정 값을 저장하고 나중에 읽기 위한 용도로 종종 사용된다. 별도의 파일이나 데이터베이스 엑세스 없이 쓸 수 있다. 굉장히 쉽고 다방면에 활용되어서 글로 정리하는게 피곤한(?) 일이 될 정도인데, 개인적으로 궁금해서 찾아본 몇 가지 정보를 더해서 글로 정리해 본다.

이하 NSUserDefaults 를 포함하여 몽땅 UserDefaults 로 표현하니 오해하지 말자.

간단한 정의

UserDefaults 는 Key - Value 스타일, 즉 키 값에 해당하는 값을 읽거나 쓸 수 있는 기능을 제공하는 사전형(Dictionary)과 비슷한 클래스인데, 여기다 파일에 기록하고 나중에 불러올 수 있는 기능까지 제공한다.

대체로 standard 라는 이름의 싱글톤을 이용해 데이터에 엑세스 하기 때문에 UserDefaults 자체를 인스턴스화 하거나 하는 일은 거의 없을 거라 생각된다.

기본 사용법

값을 저장 할 때는
UserDefaults.standard.set(value, forKey: "Some Key Name")
UserDefaults.synchronize()
이런 식으로 쓸 수 있다. 물론 키 값은 원하는 문자열을 아무렇게나 쓸 수 있다. value의 타입이 Swift 에서 제공되는 기본 타입이라면 왠만하면 별 다른 추가작업 없이 데이터가 저장된다.

여기서 두번째 줄의 synchronize() 메소드는 UserDefaults 에 기록된 값을 파일에 쓰는 역활을 한다. 따라서 여러 값을 변경할 경우 매번 synchronize() 를 호출할 필요 없이 다 쓰고 난 다음에 마지막에 딱 한번만 synchronize() 를 호출하면 된다.

만약 synchronize() 를 호출하지 않은채로 앱을 종료하더라도 크게 걱정할 필요는 없다. UserDefaults 자체는 특정 인터벌이나 클래스 deinit 시를 기준으로 synchronize() 를 알아서 호출하기 때문이다.

참고로 nil 값을 저장하면 해당 데이터를 지우는 것과 동일한 효과다. 필요없다면 nil 을 넣어버리자.

반대로 이제 저장한 값을 읽을 때는
let value = UserDefaults.standard.object(forKey: "Some Key Name") as? String
이런 식으로 object(forKey:) 메소드를 쓸 수 있다. 이 메소드는 원래 데이터의 타입과 관계없이 호출이 가능하다. 다만 타입을 확실하게 바꿔주어야 할 것이다.

아니면 아래와 같이 타입을 명확하게 해서 읽을 수도 있다.
let value = UserDefaults.standard.string(forKey: "Some Key Name")
이 메소드는 위의 코드와 사실 같은 역활을 하지만 좀 더 짧고 가독성이 높은 코드이다. string 이란 말 그대로 UserDefaults 에서 문자열 값을 읽을 때 사용된다.

물론 string 말고도 Swift 기본 타입에 해당하는 읽기 메소드가 구현되어 있으니 레퍼런스 매뉴얼을 잘 읽어보자.

읽기 시에는 저장된 값이 없을수도 있어서 읽기 메소드는 모두 옵셔널을 리턴한다는 것도 잊지 말자. 반대로 이를 이용해 기본값을 제공하는 방법도 생각해 볼 수 있다.
var userName: String {
  return UserDefaults.standard.string(forKey: "User Name") ?? "Noname"
}
사용자의 이름을 읽어오기 위한 프로퍼티 getter 를 이런 식으로 구현하는 것도 가능하다는 이야기다. 물론 어떻게 쓰는지야 개인의 마음대로이다. ;-)

복잡한 데이터의 경우

배열(Array) 이나 사전형(Dictionary) 같은 콜렉션 타입은 들어가게 되는 아이템의 타입에 종속적이지 않다보니 쉽게 기록하거나 읽을 수 없다. (물론 [String : String] 같은 단순한 타입이면 모르겠지만) ... 대신 NSKeyedArchiver 나 NSKeyedUnarchiver 를 이용해 Data(NSData) 형식으로 인코딩한 후 기록하고나 디코딩해서 읽을 수 있다.

아래 예제는 기본적인 사전형 데이터를 UserDefaults 에 쓰기 위한 예제이다.
let data = NSKeyedArchiver.archivedData(withRootObject: someDictionary)
UserDefaults.standard.set(data, forKey: "Some Dictionary Data")
반대로 읽을 때는 읽은 데이터에서 언아카이버로 디코딩을 해야 한다.
if let data = UserDefaults.standard.object(forKey: "Some Dictionary Data") as? Data {
  let someDictionary = NSKeyedUnarchiver.unarchiveObject(with: data) as? [AnyHashable : Any]
}
두 단계로 나눠서 읽거나 써야하는 만큼 귀찮아 지긴 하지만 콜렉션 타입의 경우 내부에 들어오는 타입을 명확히 알기가 힘든 경우가 있으므로 종종 이용해야 할 수 있다.

특히 콜렉션 타입을 섞는 경우, 예를 들어 [[AnyHashable : Any]] 와 같이 사전형 타입의 배열형이라면 그냥 저장하는건 불가능하고 위 처럼 인코드 및 디코드 과정을 거쳐야 한다.

이외에 직접 만든 클래스의 경우는 오브젝트 직렬화(Object Serialization)를 구현하면 바로 읽거나 쓸 수 있다.

다만 아직까지 struct 형식에 대해 NSCoding 을 적용 할 수가 없다. 직접 만든 struct 타입은 귀찮겠지만 위와 같은 사전형 형식 등으로 변환한 뒤 저장하고 읽는 방법을 이용해 둘러서 구현하는 방법이 현재로썬 가장 간단해 보인다.

UserDefaults 의 내용은 어디에 저장되나

UserDefaults 의 내용은 plist 형식의 파일로 저장되며 저장되는 위치는 OS에 따라 다르다.

macOS 의 경우 아래의 디렉토리를 살펴보자.
/Users/사용자이름/Library/Preferences/
여기에 앱의 번들아이디(Bundle Identifier) 디렉토리를 찾아서 들어가보면 plist 파일을 발견할 수 있는데 바로 이 파일이다. 내용을 보며 알겠지만, UserDefaults 를 통해 저장한 내용 뿐만 아니라 인터페이스 빌더 등에서 이용하는 AutoSave 항목들도 여기에 저장되는 등 여러가지 앱의 정보가 기록된다.

iOS 의 경우 ... 모르겠다. 난 순정만 사용하다 보니 앱의 샌드박스 영역을 탐색할 권한이 없다. ;-)

iOS Simulator 의 경우 우선 아래 디렉토리를 들어가보자.
/Users/사용자이름/Library/Application Support/iPhone Simulator/버전/Applications
이 디렉토리에는 앱의 GUID 에 해당하는 값을 이름으로 하는 디렉토리가 잔뜩 있는데 여기서 원하는 것을 찾아야 한다. 그리고 해당 디렉토리에 들어간 다음 아래 디렉토리를 찾아보자.
Library/Preferences/
이 디렉토리 아래에 들어가보면 역시나 plist 파일을 찾을 수 있다.

성능 이슈 (Performance Issues)

쓰기 방법에서 synchronize() 라는 메소드를 언급했는데 아마 좀 지식이 있다면 성능에 대해서는 걱정할 필요가 없다는 것을 알게 될 것이다.

UserDefaults 는 기본적으로 메모리 상에서 모든 데이터를 관리한다. 즉 메모리 캐싱을 한다.

읽을 때 plist 에서 내용을 읽은 다음 메모리 상에 저장하고 다음에 동일한 키의 데이터를 읽으려고 하면 파일이 아니라 메모리의 내용을 돌려준다. 따라서 막 읽는다고 해서 파일 엑세스를 통한 퍼포먼스 저하 문제는 딱히 걱정하지 않아도 된다는 의미이다.

쓰기의 경우에는 명확하다. synchronize() 메소드를 호출해야 메모리의 내용이 파일로 동기화되니 그 이전에는 역시 퍼포먼스 문제를 걱정할 필요는 없다. 대신 synchronize() 가 호출되지 못 하는 비정상적인 상황 때문에 데이터를 잃어버리는 경우를 더 걱정해야 할지도 모르겠다. :-)

[관련링크] UserDefaults API Reference
[관련글] Swift/Objective-C 오브젝트 직렬화(Serialization) (Swift 구버전)
[관련글] 스위프트(Swift) 가이드

댓글 없음 :