iOS에서 데이터를 영구적으로 저장하는 방법은 여러 가지가 있습니다.
그중 대표적인 방법이 UserDefaults와 CoreData인데요.
오늘은 UserDefaults에 대해 알아보도록 하겠습니다.
UserDefaults
UserDefaults는 가장 기본적인 데이터베이스로,
복잡하고 큰 용량의 데이터보다는
스위치의 On/Off 같은 사용자 기본 설정처럼 간단한 데이터 저장에 적합합니다.
앱이 삭제되면 UserDefaults 데이터도 함께 삭제되므로
데이터가 영구히 유지되어야 한다면 UserDefaults는 부적합할 수 있습니다.
또한, 데이터가 암호화되지 않고 그대로 저장이 되기 때문에
보안과 관련된 정보는 저장하지 않는 것이 좋습니다!
(민감한 데이터는 키체인을 이용해야 합니다.)
UserDefaults는 싱글톤 객체입니다.
앱이 시작할 때 인스턴스가 생성되고 이후에는 이 인스턴스를 공유합니다.
thread safety(스레드 안정성)도 보장하기 때문에
데이터 동기화 문제를 고려하지 않아도 됩니다.
데이터 저장
UserDefaults는 key-value 쌍으로 데이터가 저장됩니다.
key는 String이고 value는 모든 객체를 담을 수 있는데요.
Int, Float, Double, Bool, URL?같은 공통 유형에 접근할 수 있는 메서드를 제공합니다.
또한, NSData, NSString, NSNumber, NSDate, NSArray, NSDictionary 유형의 객체를 저장할 수도 있습니다.
위의 타입이 아닌 사용자 정의 객체는 NSKeyedArchiver, Codable을 이용해 Data 형태로 아카이빙해서 저장해야 합니다.
만약 사용자 정의 객체를 아카이빙 하지 않고 저장하게 되면
non-property list object를 저장하려고 한다는 런타임 에러가 발생합니다.
여기서 property list object는 위에서 언급한 NSData 등입니다.
사용자 정의 객체를 저장하는 방법은 아래에서 더 자세히 다루겠습니다.
synchronize
리눅스를 할 때는 파일 변경 등을 완전하게 저장하기 위해 sync 명령어를 사용한 적이 있었습니다.
UserDefaults에서도 동일한 메서드가 보여 사용해야되나? 보았는데요.
데이터 안전 저장을 위해 필요한 메서드지만 자동으로 호출되고 있었습니다.
결정적으로
공식문서에서 이 메서드는 불필요하며 사용하지 않기를 권장하고 있네요 ㅎㅎ;
조금 더 찾아보니
UserDefaults 자체에서 클래스 deinit 시 자동으로 synchronize()를 호출하기 때문에
개발자가 직접 넣지 않아도 된다고 합니다.
대신, 배터리를 강제로 뽑거나... 폰이 부서지거나... 해서 synchronize()가 호출되지 않으면
데이터가 증발하니 주의해야겠....나?요? ㅎㅎ;;
폰이 부서지면 데이터가 문제가 아니겠지만 아무튼 대비하면 좋겠죠? ㅎ
UserDefaults의 데이터는 불변하다
UserDefaults에 저장된 값은 값을 덮어쓰지 않는 이상 변하지 않습니다.
var x = 10
UserDefaults.standard.set(x, forKey: "value")
x = 5
변수 x를 UserDefaults에 저장한 뒤 x의 값을 5로 바꾼다면
let userDefaultsValue = UserDefaults.standard.integer(forKey: "value")
print("\(userDefaultsValue)") //10
UserDefaults에 저장한 x 값은 5가 아니라 그대로 10입니다.
데이터 변경을 원한다면 set 메서드를 통해 다시 데이터 저장을 해줘야 합니다.
데이터 읽어오기
UserDefaults의 데이터를 읽는 메서드는 크게 기본 타입과 Any 타입으로 나뉩니다.
기본 타입들은 메서드 이름이 타입명인데 Any 타입은 object인 것을 볼 수 있습니다.
기본 타입을 읽을 때는 값이 해당 타입으로 반환되기 때문에 타입 캐스팅을 안 해줘도 되지만
이외의 타입은 Any? 타입으로 값이 반환되기 때문에 타입 캐스팅을 해줘야 합니다.
물론 기본 타입도 object(forKey:)를 이용해 읽을 수 있지만
if let x = UserDefaults.standard.object(forKey: "value") as? Int {
print(x)
}
타입 캐스팅으로 인한 옵셔널 바인딩 처리로 코드가 길어질 수 있으니
타입 전용 메서드를 사용하는 것을 추천합니다.
데이터 삭제
UserDefaults의 데이터는 removeObject(forKey:)를 이용하면 간단하게 삭제할 수 있습니다.
(혹은 nil을 설정해도 됩니다.)
UserDefaults.standard.set(10, forKey: "value")
let value = UserDefaults.standard.integer(forKey: "value")
print(value) //10
UserDefaults.standard.removeObject(forKey: "value")
let value2 = UserDefaults.standard.integer(forKey: "value")
print(value2) //0
10을 세팅한 후 값을 출력하면 저장한 10이 출력됩니다.
removeObject(forKey:)를 이용해 value 키의 데이터를 삭제하고
값을 출력하면 기본값인 0이 출력됩니다.
저장하지 않은 키를 삭제해도 에러가 난다던가 하는 일은 없으니
마음 편히 사용해도 됩니다!
기본값 설정 register(defaults:)
데이터를 불러올 때 값이 없을 수 있습니다.
위 메서드 리스트에서 return 타입이 옵셔널인 것과 옵셔널이 아닌 것이 있는데요.
return 타입이 옵셔널이 아닌 메서드는 기본적으로 기본값이 설정되어 있습니다.
예를 들어,
let data = UserDefaults.standard.integer(forKey: "none")
print(data) //0
none 키로 값을 저장하지 않고 값을 가져오면 기본값인 0으로 읽힙니다.
기본값은 Int, Float, Double은 0, Bool은 false입니다. (공식 문서에서 확인 가능)
return 타입이 옵셔널일 때는 기본값이 없는 것으로
저장하지 않고 값을 읽으면 nil로 읽힙니다.
let data = UserDefaults.standard.string(forKey: "none")
print(data) //nil
그래서 매번 옵셔널 바인딩을 해줘야 한다는 단점이 있는데요.
이때, register(defaults:)을 이용해 각 키의 기본값을 설정할 수 있습니다.
사용 방법은 인자로 [Key: Value] 쌍을 전달하면 됩니다.
UserDefaults.standard.register(defaults: [
"SomeKey" : "Some Message"
])
let data = UserDefaults.standard.string(forKey: "SomeKey")
print(data) //Optional("Some Message")
register를 이용해 SomeKey 키의 기본값으로 Some Message를 설정해주었습니다.
값을 가져와 출력하면 nil이 아니라 Some Message가 불립니다.
사용자 정의 타입 저장하고 가져오기
데이터 저장하기 문단에서 본 것처럼
구조체같은 사용자 정의 타입은 UserDefaults에 그냥 저장할 수 없습니다.
struct Human {
let name: String
}
let human: Human = Human(name: "애플")
UserDefaults.standard.set(Human, forKey: "Human")
struct를 Data형으로 변경하여 UserDefaults에 저장해야 하는데요.
객체를 Data형과 같이 바이트 형태로 변경하는 작업을 아카이빙이라고 합니다.
반대로 메모리, 디스크에 저장된 Data 형태의 바이트를 struct같은 객체로 바꾸는 것을 언아카이빙이라고 합니다.
struct를 UserDefaults에서 읽기 위해서는 언아카이빙 과정이 필요해요.
이번 포스팅에서는 Codable을 이용한 아카이빙, 언아카이빙 작업을 해보도록 하겠습니다.
아카이빙, 언아카이빙을 위해 Codable 프로토콜을 채택합니다.
struct Human: Codable {
let name: String
}
UserDefaults에 struct 저장
JSONEncoder를 이용해 객체를 아카이빙 합니다.
let encoder: JSONEncoder = JSONEncoder()
이렇게 생성한 JSONEncoder를 이용해
if let encoded = try? encoder.encode(human) {
print("type: \(type(of: encoded))") //Data
UserDefaults.standard.set(encoded, forKey: "Human")
}
human을 Data 타입으로 변경하고 이를 UserDefaults에 저장하면 됩니다.
UserDefaults에서 struct 가져오기
저장을 했으니 가져오기도 해야겠죠?
저장한 Data 타입을 Human 타입으로 언아카이빙 해야 합니다.
이번에는 JsonDecoder를 사용할거에요.
let decoder: JSONDecoder = JSONDecoder()
if let data = UserDefaults.standard.object(forKey: "Human") as? Data,
let human = try? decoder.decode(Human.self, from: data) {
print("type: \(type(of: human))") //type: Human
print(human) //Human(name: "애플")
}
JSONDecoder를 생성해서 UserDefaults에 저장한 Data를 디코딩 해줍니다.
그리고 출력을 해보면 저장한 Human 객체가 잘 가져와지는 것을 볼 수 있습니다.
NSKeyedArchiver와 NSKeyedUnarchiver 방법
Codable이 아닌 NSKeyedArchiver와 NSKeyedUnarchiver을 이용한 방법도 있습니다.
간단히 코드 소개만 하겠습니다.
아카이빙은 archiveData 메서드를 이용합니다.
let data = NSKeyedArchiver.archivedData(withRootObject: someDictionary)
UserDefaults.standard.set(data, forKey: "Some Dictionary Data")
언아카이빙은 unarchiveObject 메서드를 이용합니다.
if let data = UserDefaults.standard.object(forKey: "Some Dictionary Data") as? Data {
let someDictionary = NSKeyedUnarchiver.unarchiveObject(with: data) as? [AnyHashable : Any]
}
UserDefaults가 저장되는 공간
UserDefaults의 내용은 plist 형식의 파일로 저장되며 OS에 따라 저장되는 위치가 달라집니다.
iOS의 경우 탐색이 불가능하므로 정확히 어떤 경로에 저장되는지는 모릅니다.
시뮬레이터의 경우 /Library/Developer/CoreSimulator 에서 시뮬레이터 plist를 확인하면 됩니다.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last! as String)
return true
}
위 코드를 이용해 정확한 경로도 찾을 수 있다고 하네요.
UserDefaults 성능
UserDefaults가 plist에 값을 저장한다면
자주 읽고 쓰면 파일 IO가 자주 발생해서 성능이 떨어지는거 아닌가? 하는 걱정을 할 수도 있을텐데요.
UserDefaults는 기본적으로 메모리 상에서 모든 데이터를 관리합니다.
plist에서 값을 읽은 뒤에 메모리에 저장해두고,
동일한 키의 데이터를 읽으려고 하면 메모리에 있는 값을 전달합니다. (메모리 캐싱)
따라서 값을 많이 읽는다고 성능이 저하되거나 그런 일은 없습니다.
쓰기의 경우에도 synchronize()가 호출된 뒤 파일로 동기화가 되고
그 전까지는 메모리 상에 존재하기 때문에
값을 쓰는 것도 성능 저하를 일으키지 않습니다.
단!
UserDefaults에 대규모 데이터를 저장한다면
앱을 실행할 때마다 대규모 데이터를 메모리에 올리기 때문에
앱 성능이 나빠질 수 있습니다.
그래서 대규모 데이터는 UserDefaults보다 Core Data를 사용하는 것이 더 적절합니다.
Core Data에 대해서는 다음에 다뤄보겠습니다.
참고
https://developer.apple.com/documentation/foundation/userdefaults
https://zeddios.tistory.com/107
https://velog.io/@nnnyeong/iOS-UserDefaults-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
https://developer.apple.com/documentation/foundation/nskeyedarchiver
https://developer.apple.com/documentation/foundation/userdefaults/1417065-register
https://ios-development.tistory.com/702
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.