Sandbox & UserDefaults

Choong Won, Seo·2024년 5월 31일

TIL

목록 보기
4/7
post-thumbnail

UserDefaults를 처음 사용했을 때는 ‘서버가 있는데 왜 UserDefaults를 사용하지..?’ 하고 생각해서 활용을 제대로 못했던 것 같다.

아마 그 때는 UserDefaults가 서버처럼 송신속도가 느리다고 내멋대로 생각해서 이럴바엔 서버를 사용하지 하고 생각했던 것 같은데, 지금 다시 배우고 생각해보니 UserDefaults가 어디에 저장되는지도 자세히 몰랐고, 좀 더 편하게 사용할 수 있는 방법같은 것들도 모르고 사용했던 것 같다. 이번 기회에 조금 정리해보고자 한다.

UserDefaults

UserDefaults - Runtime동안에 Key-Value형태로 사용자의 기본 데이터베이스에 데이터를 저장하는 인터페이스로, plist파일로 존재.

plist?? 우리가 자주 접근하는 info.plist, 그 plist(Property List)파일이 맞다. plist는 macOS와 iOS에서 자주 사용되는 구조화된 데이터 저장 방식으로, XML형식이라서 사람이 읽고 편집할 수 있고, 동시에 바이너리 형식이라서 읽고 쓰는 작업에 있어서 효율성이 있다.

조금의 단점이라면 Key-Value형식이기 때문에 사용자 정의 테이터 타입은 넣기가 어렵고, Array, String, Bool 등 간단한 데이터 타입을 다루는데에 용이하다.

→ 그래서 이러한 특성들 때문에 빠르게 간단한 데이터를 저장할 수 있는 UserDefaults가 plist에 저장된다.

In Memory Caching

userDefaults에 많은 양의 데이터를 저장하면 안되는 이유는 또 있다. UserDefaults는 내부적으로 앱이 실행될 때 plist파일이 메모리에 한 번에 load되게 하는 방식인 인메모리 캐싱(In Memory Caching)방식을 사용한다. 파일에 접근할 때마다 파일 I/O 작업을 줄여 성능을 개선하기 위해서인데(이로 인해 연산 속도가 엄청나게 빠르다.), 이로 인해 userDefaults에 너무 많은 양의 데이터를 저장하게 되면 앱의 실행속도가 현저히 떨어지는 문제를 야기할 수 있다.

Blocking Algorithm

예전부터 UserDefaults에 잘 손이 안갔던 이유는 서버 CRUD과 마찬가지로 UserDefaults도 엄청나게 많은 호출이 일어나면 여러 비동기적, 쓰레드적 문제가 발생할 것 같아서 번거롭다고 느꼈기 때문이다.

하지만 이번에 알아보면서 이게 전혀 틀린 접근이었다는 것을 깨닫았다. 블로킹 알고리즘(Blocking Algorithm)때문인데, 이는 호출한 저장defaults.set()이나 읽기defaults.string() 작업이 완료될 때까지 Thread가 대기상태가 된다는 것을 의미한다.

UserDefaults의 동기적 작동과 이러한 Blocking Algorithm으로 인해서 UserDefaults는 예전에 내가 우려했던 Data Race를 막고(Thread Safe), 데이터 무결성과 일관성을 보장한다. (물론 너~무 많은 CRUD작업이 들어왔을 경우에는 성능에 영향이 있긴 하다.)c

또 그럴 때에는 GCD를 이용한 비동기 처리도 불가능한 것은 아니라고 한다.

// 비동기 저장
DispatchQueue.global(qos: .background).async {
    defaults.set("John Doe", forKey: "username")
    DispatchQueue.main.async {
        print("Data saved")
    }
}

// 비동기 읽기
DispatchQueue.global(qos: .background).async {
    let username = defaults.string(forKey: "username")
    DispatchQueue.main.async {
        if let username = username {
            print("Username: \(username)")
        }
    }
}

Singleton Pattern

또한 여기에 더 추가적으로 UserDefaults는 싱글톤 패턴(Singleton Pattern)으로 데이터 일관성 유지에 더욱 힘을 실어준다.

‘standard’ property는 클래스 레벨에서 접근할 수 있는 단일 UserDefaults 인스턴스를 제공한다.

class var는 처음 봤는데 static var에 비해 override가 가능한 computed property라는 특성이 있다.

이로 인해 전역 접근으로 인한 편리성, 지연 초기화로 인한 리소스 절약, 데이터 일관성 등 Singleton Pattern이 가질 수 있는 여러 장점을 가져 간편하다.

(사실 UserDefaults는 싱글톤 패턴이다? 정확한 표현은 아니다. 왜냐하면 싱글톤 패턴은 private init으로 자체의 instance가 생성되면 안되는데, UserDefaults는 가능하기 때문이다. 그럼에도 불구하고, 그렇게 instance를 생성해서 접근을 해도 결국 동작 자체는 싱글톤처럼 동작하기 때문에 조금 애매한 부분이 있는 것 같다.)

SandBox

잘 알겠어! 그래서 UserDefaults가 어디에 저장되는데??? 어디에 저장되길래 앱이 사라지면 같이 사라지는 특성을 갖는거지?

SandBox - 커널 수준에서 시행되는 macOS의 접근 제어 기술

커널? - 메모리에 상주하는 운영체제의 부분

소프트웨어가 컴퓨터에서 실행되기 위해서는, 메모리에 그 프로그램이 올라가 있어야 한다. 운영체제(OS)또한 마찬가지이다. 하지만, 운영체제는 규모가 너무 커서 그때그때 필요한 부분을 올리게 되는데, 이 올라가는 영역을 커널이라고 한다.

안드로이드나 다른 운영체제에서 모든 앱이 하나의 Document폴더를 공유하고 데이터를 저장하는 방식과 달리, iOS에서는 하나의 앱당 하나의 SandBox라는 공간을 갖는다.

이로써 얻을 수 있는 이점은 그토록 강조하고 차별화를 가지고 있는 보안성이다. 하나의 앱에서 해킹, 유출 등 데이터 취약상황이 발생했을 때, 각각의 앱의 SandBox들은 서로 공유되지 않는 고유의 영역을 가지므로 피해규모를 최소화할 수 있다.

하지만 이렇게 고유의 영역을 가지고 있어도 하나의 앱이 가지고 있는 권한들을 사용해서 더 많은 부분에 접근을 하여 추가적인 피해를 입힐 수도 있다. 이를 방지하기 위해 SandBox는 또한 각각의 앱이 접근할 수 있는 리소소를 명시하여 피해에 대한 마지막 방어선을 구축한다.

→ 이 말이 무엇일까? 바로 우리가 항상 Camera, Library, Location, Notification등에 접근할 때 plist에서 명시해주었던 접근권한에 대한 부분이다.

(예를 들어 설명하자면, 만일 사진을 찍고 저장을 하는 App이라면, 명시되어있던 카메라와 라이브러리에 대한 권한은 허용하더라도 App이 마이크, 달력 등의 접근권한을 요청하면 시스템 자체에서 자체적으로 거부를 하는 식이다.)

그렇다면 SandBox의 구성은 어떻게 될까?

FileManager로 .documentDirectory를 잡아서 직접 살펴볼 수도 있다.

if let sandboxPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
    print("Documents Directory: \(sandboxPath.path)")
}

App설치 시점에 생성되며, 3개의 하위 Container를 가진다.

Bundle Container - App의 Bundle Identifier, info.plist등과 같은 App을 구성하는 정보들을 담고있다. .swift파일이 바이너리 형태의 실행 파일로 변환되고, 스토리보드, Xib등도 함께 변환된다.

Data Container - Documents, Library, SystemData, tmp로 구성되어 있다.

조금씩 간단히만 알아보자면, Documents에서는 사용자가 앱을 통해 생성한 문서, 파일 등의 컨텐츠가 저장된다. Library에서는 Documents에서 취급되는 사용자 데이터를 제외한 모든 파일을 관리한다. 앱의 스냅샷 등 앱의 기능이나 관리에 필요한 파일을 저장한다.

→ 쭉 알아보고 있는 UserDefaults가 바로 여기 Library에 저장된다!!

Library - Preferences - plist파일을 열면 실제로 plist파일에 UserDefaults가 저장되어있는 모습을 시각적으로 관찰해볼 수 있다.

(조금 더 시각적으로 보고싶다면, RocketSim 시뮬레이터 플러그인을 이용해볼 수도 있다.)

RocketSim - Enhancing the iOS Simulator

이렇게 실제로 보니까 이해가 빠를텐데, UserDefaults는 비휘발성 정보라서 메모리(RAM)이 아닌 Disk에 저장된다.

Runtime에 사용될때는 앞서 알아본 것처럼 인메모리 캐싱 방식이기 때문에 빠른 읽기/쓰기를 위해 Heap메모리에 Cache값을 동적으로 할당해서 사용한다.

UserDefaults의 활용 방법

보통 UserDefaults를 사용할 때 이렇게 명시적으로 불러와서 사용하거나,

UserDefaults.standard.string(forKey: "title")
UserDefaults.standard.set("New Title", forKey: "title")

UserDefaults.standard.bool(forKey: "isFinished")
UserDefaults.standard.set(false, forKey: "isFinished")

Manager Model를 만들어서 computed property로 관리해주기도 한다.

class UserDefaultsManager {
    static var title: String? {
        get { return UserDefaults.standard.string(forKey: "title") }
        set { UserDefaults.standard.set(newValue, forKey: "title") }
    }
    
    static var isFinished: Bool {
        get { return UserDefaults.standard.bool(forKey: "isFinished") }
        set { UserDefaults.standard.set(newValue, forKey: "isFinished") }
    }
}

하지만 이렇게 Model을 만들어도 계속 반복되는 부분(Boiler Plate Code)이 있는데, 이는 Swift 5.1에서 추가된 @Property Wrapper를 통해서 해결해줄 수 있다.

@Property Wrapper - 반복되는 로직들을 프로퍼티 자체에 연결 해 줄 수 있는 기능

@propertyWrapper
struct UserDefault<T> {
    
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: self.key) as? T ?? self.defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: self.key) }
    }
}

class UserDefaultsManager {
    @UserDefault(key: "title", defaultValue: nil)
    static var title: String?
    
    @UserDefault(key: "isFinished", defaultValue: false)
    static var isFinished: Bool
}

UserDefaults.standard 또한 하나의 인스턴스로 만들고 싶다면 아래처럼 작성하면 된다.

@propertyWrapper
struct UserDefault<T> {
    
    let key: String
    let defaultValue: T
    let storage: UserDefaults
    
    var wrappedValue: T {
        get { self.storage.object(forKey: self.key) as? T ?? self.defaultValue }
        set { self.storage.set(newValue, forKey: self.key) }
    }
    
    init(key: String, defaultValue: T, storage: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = defaultValue
        self.storage = storage
    }
}

추가적으로, 원래 Model 만들 때 Enum Key를 사용하던 것 또한 추가해줄 수 있다.

enum Key: String {
    case title = "title"
    case isFinished = "isFinished"
}

@propertyWrapper
struct UserDefault<T> {
    
    let key: Key
    let defaultValue: T
    
    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: self.key.rawValue) as? T ?? self.defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: self.key.rawValue) }
    }
}

사용자 정의 타입

UserDefaults에 Default Type이 아닌 내가 정의한 타입을 넣고 싶을 때는 어떻게 해야 할까?

struct Work: Codable {
    var title: String
    var isFinished: Bool
}

class UserDefaultsManager {
    static func set<T: Codable>(_ value: T, forKey key: String) {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(value) {
            UserDefaults.standard.set(encoded, forKey: key)
        }
    }
    
    static func get<T: Codable>(forKey key: String, as type: T.Type) -> T? {
        if let data = UserDefaults.standard.data(forKey: key) {
            let decoder = JSONDecoder()
            if let decoded = try? decoder.decode(type, from: data) {
                return decoded
            }
        }
        return nil
    }
}

UserDefaults에 Data type으로 저장을 하고, 읽을 때와 쓸 때 인코딩과 디코딩이 필요하다.

Property Wrapper부분이 조금 어려웠다. 너무 새로운 개념이라서 일부러 여러번 사용하고 적용할 수 있는 부분들을 찾아봐야겠다.

참조

[iOS] iOS SandBox 란?

[iOS] App SandBox란?

Property Wrapper

profile
UXUI Design Based IOS Developer

0개의 댓글