iOS 면접질문 중 앱 내부 저장소에 대해서 정리해보고자 직접 공부해봤습니다!
따라서 지금까지 사용해봤던 UserDefaults 와 FileManager, 그리고 CoreData 및 KeyChain에 대해 앱 내부 저장소 관점에서 정리해보고자 합니다.

SandBox

먼저 앱 내부에 데이터를 저장하기 전에 문득 이런생각이 들었어요

데이터들이 디바이스 내에서 어떻게 관리되는거지?
다른 앱과 공통적으로 사용되는 공간이 있나?
앱 별로 분리되어 저장되는 건가?

이러한 것을 먼저 이해해야 내가 저장하고자 하는 행위가 다른 앱과 연관이 있는지가 명확해질 것 같더라구요!
그래서 SandBox 구조를 한번 알아봤습니다.

https://developer.apple.com/documentation/security/app_sandbox#//apple_ref/doc/uid/TP40011183-CH1-SW1

정리해보면

  • 앱 손상으로 인해 system에 문제가 발생되지 않도록 하기 위하여 system 자원 접근과 사용자 데이터에 대한 접근을 제한
  • system 자원이 필요한 경우 권한을 부여받아 접근

즉, 앱이 SandBox 구조로 저장되어 다른 앱, 또는 system에 영향이 없도록 하기 위한 구조라고 볼 수 있습니다!

그리고 SandBox 정의가 바로

SandBox란 외부로부터 들어온 프로그램이 보호된 영역에서 동작해 시스템이 부정하게 조작되는것을 막는 보안 형태이다.

이렇게 SandBox 구조를 통해 보안적으로 안전한 구조가 되었다는 특징이 있습니다.
따라서 iOS 개발자들은 이러한 특징을 이해하고 개발해야겠죠?

SandBox 구조 특징으로 system 자원을 접근할때는 권한이 필요합니다. 앱 초기에 설치했을 때 대표적으로

"사용자의 위치에 접근하도록 허용하시겠습니까?"

이런 Alert 많이 봤죠?
이것이 바로 SandBox 구조로 인해 권한을 얻어야만 접근가능한 상황이라고 볼 수 있습니다.

대표적인 권한이 필요한것들도 정리해봤습니다!

  • 카메라
  • 마이크
  • USB 장치
  • 문서 인쇄
  • 블루투스
  • 주소록 read/write
  • 위치정보
  • 캘린더 read/write
  • 파일 액세스 (Finder 폴더별 권한)

SandBox 구조

그러면 SandBox의 존재이유는 알겠는데, 어떤 구조일까요?

SandBox 구조의 대표적인 이미지들을 가져와봤습니다.
이렇게 App 별로 SandBox 그룹으로 묶여있으며, SandBox 내 Data Container 영역에 바로 앱에서 데이터를 read/write 한다고 볼 수 있겠네요!

그래서 내부 저장소에 저장하는 각 방법들을 살펴보니

  • UserDefaults: SandBox 내 Library/Preferences 루트에 저장
  • FileManager: SandBox 내 Documents 루트에 저장
  • CoreData: SandBox 내 Library/Application Support 루트에 저장
  • KeyChain: SandBox 외부에 저장

참고: [iOS] 앱 샌드박스(App Sandbox)와 Container Directory

여기서 중요한점은 KeyChain의 경우는 SandBox 외부에 저장됩니다!
이점이 정말 중요한데요, token이나 password와 같은 정보들을 keyChain 내 암호화되어 저장하잖아요? 그러면 앱을 지워도 여전히 token 이나 password 값들이 살아있다는 겁니다!!

SandBox 내 내부저장소

정리하면 앱은 SandBox 형식으로 분리되어 있어 다른 앱과 분리되어 있는 구조입니다.
그리고 UserDefaults, FileManager, CoreData를 통해 저장된 데이터는 SandBox 내 저장됩니다.
따라서 앱 외부에 영향이 생기지 않으며, 앱을 지울 때 같이 지워진다는 점이 특징입니다!

그리고 KeyChain의 경우 시스템 전반적으로 접근가능한 공용저장소 영역이라 볼 수 있기에 더욱 주의해서 사용해야 한다는 점이 특징입니다!

그러면 이제 하나씩 알아보겠습니다!

UserDefaults

https://developer.apple.com/documentation/foundation/userdefaults

정리를 해보면

  • 사용자의 defaults database 에 대한 인터페이스
  • 사용자의 preference 를 저장하여 동작을 커스텀
  • key-value 형태로 저장
  • runtime에 UserDefaults 객체를 사용하여 read 하며 cache 됩니다.
  • 값을 설정하면 동작중인 프로세스 내에서는 synchronously하게 변경되고, 다른 프로세스 및 persistant storage는 asynchronously하게 변경됩니다.
  • thread-safe 합니다.

아마 대부분 아시겠지만 UserDefaults는 간단한 환경설정(preference)과 같은 값들을 저장하는 곳이라고 생각하시면 됩니다! 모든 정보를 UserDefaults에 저장하는건 바람직하지 않죠

여기서 몇가지 새로운 사실을 알 수 있었어요!
바로 cache가 된다는 점과 UserDefaults 객체를 부른 프로세스 내에서만 동기적으로 동작되었다는 사실!

아마도 이런 점 때문에 시뮬레이터로 앱을 개발할 때 가끔 UserDefaults에 저장이 안되는 것 같았네요

저장가능한 형태

그러면 UserDefaults를 통해 read/write 가능한 형태를 알아볼께요

  • commom types
    • Float
    • Double
    • Int
    • Bool
    • URL?
  • Any?: array(property list objects)
    • NSData
    • NSString
    • NSNumber
    • NSDate
    • NSArray
    • NSDictionary

이렇게 저장가능한 형태를 보면 기본적인 값들을 저장하는 형태라는 것도 알 수 있습니다.
만약 특정 struct를 저장하고 싶다면 NSData 형태로 변환하여 저장하는 방법이 가능하겠죠?

read/write

그러면 UserDefaults를 통해 read/write 방법을 알아볼께요

  • read
// 반환 type이 정해진 형태
let floatValue = UserDefaults.standard.float(forKey: "floatValue")
let doubleValue = UserDefaults.standard.double(forKey: "doubleValue")
let intValue = UserDefaults.standard.integer(forKey: "intValue")
let boolValue = UserDefaults.standard.bool(forKey: "boolValue")
let urlValue = UserDefaults.standard.url(forKey: "urlValue")
let stringValue = UserDefaults.standard.string(forKey: "stringValue")
// 반환 type을 type casting 하는 형태
let floatValue2 = UserDefaults.standard.object(forKey: "floatValue") as? Float ?? 0

이렇게 commom types 형태로 직접 반환되는 메소드와
Any?가 반환되어 type casting하여 사용하는 메소드를 통해 읽을 수 있습니다!

  • write
UserDefaults.standard.set(floatValue, forKey: "floatValue")
UserDefaults.standard.set(doubleValue, forKey: "doubleValue")
...

write의 경우는 메소드 형태만 보면 저장하고자 하는 타입에 상관없이 set 메소드를 통해 저장이 가능합니다!
사실 이는 내부적으로 overloading을 통해(같은 메소드명, 다른 파라미터 타입) common types에 따라 여러 메소드가 있지만 결론적으론 set 메소드를 사용해서 저장하는 형태로 구현되어 있답니다!

UserDefaults Manager

추가로 저는 UserDefaults를 사용할 때 여러 Key들을 enum으로 관리하고, 편하게 read/write 할 수 있도록 UserDefaultsManager라는 struct를 만들었습니다!

struct UserDefaultsManager {
    enum Keys: String {
        // app start
        case isFirst
        // notification
        case timer5minPushable = "timer5minPushable"
        case timerPushable = "timerPushable"
        case stopwatchPushable = "stopwatchPushable"
        // alert
        case updatePushable = "updatePushable"
        ...
    }
    
    static func set<T>(to: T, forKey: Self.Keys) {
        UserDefaults.standard.setValue(to, forKey: forKey.rawValue)
        print("UserDefaultsManager: save \(forKey) complete")
    }
    
    static func get(forKey: Self.Keys) -> Any? {
        return UserDefaults.standard.object(forKey: forKey.rawValue)
    }
}

이렇게 하여 좀 더 편리하게 UserDefaults를 사용하고 있습니다! :)

FileManager

https://developer.apple.com/documentation/foundation/filemanager

정리를 해보면

  • file system과 상호 작용 가능한 인터페이스
  • filedirectory를 locate, create, copy, move 할 수 있습니다.
  • file system 내 저장시 경로(path) 정보를 효율적으로 표현하는 NSURL을 사용하여 위치를 지정
  • iCloud 내 저장되어 있는 정보를 관리할 수 있는 메소드 존재
  • cloud storage 로 tag된 file 과 directory 들은 iCloud와 동기화 된다.
  • thread-safe 하지만 delegate를 통해 notification을 수신시 FileManager 개체의 고유한 인스턴스가 필요

FileManager는 UserDefaults와는 조금 다르게 file-system 기반으로 저장된다고 볼 수 있습니다!
즉, 저장하고자 하는 데이터가 file 형태로, 그리고 directory 별로 관리가 될 수 있다는 것입니다!

동일한 형태의 여러 데이터를 저장할때도, 데이터 형태별로 각기 저장할때도 file 형식으로 저장할 수 있겠죠?
간단하게는 .json 파일로 저장하여 JSONEncoder, JSONDecoder를 통해 read/write가 가능하답니다!

저장가능한 형태

file system 내 저장되기 때문에 Data 값을 토대로 저장하게 됩니다!
따라서 Codable을 채택한 Structencode 하여 Data를 저장하거나, 특정 Data를 직접 저장하면 됩니다!

예를 들어 Struct를 저장한다면

do {
	let data = try JSONEncoder().encode(myStruct)
} catch {
	print(error.localizedDescription)
}

이렇게 데이터를 추출하여 저장할 수 있고,
이미지를 저장한다면

let data = UIImage(named: "image")?.pngData()

식으로 데이터를 추출하여 저장하면 되겠죠?

read/write

그러면 FileManager를 통해 read/write 방법을 알아볼께요
먼저 file-system을 통해 read/write 하므로 저장되는 경로인 URL이 중요합니다!

// file-system 내 저장된 file은 URL을 통해 위치한다.
let documentURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let directoryURL = documentURL.appendingPathComponent("directoryName", isDirectory: true)

보통 directory를 생성하여 관리하는 경우 documentURL 내 directory를 생성합니다.
이를 토대로 read/write를 살펴보겠습니다!

  • read
// file의 URL
let fileURL = directoryURL.appendingPathComponent("filename.format")
// fileURL의 path를 통해 Data를 반환할 수 있다.
let fileData = FileManager.default.contents(atPath: fileURL.path)

file의 URL은 directoryURL 뒤에 filename.json 과 같은 파일명을 붙여 URL을 생성합니다.
이런 fileURL을 토대로 저장된 경우 해당 fileURL.path를 토대로 Data? 값을 얻을 수 있습니다!

  • write
// 저장하고자 하는 file의 URL
let fileURL = directoryURL.appendingPathComponent("filename.format")
        
do {
	// 저장하고자 하는 file의 Data
	let data = try encoder.encode(myStruct)
	FileManager.default.createFile(atPath: fileURL.path, contents: data, attributes: nil)
} catch {
	print(error.localizedDescription)
}

이렇게 file의 URL을 정의하여 Data를 URL 위치에 저장합니다!
이렇게 저장된 file들은 UIActivityViewController를 통해 공유 또한 가능하답니다! :)

CoreData

https://developer.apple.com/documentation/coredata/

정리를 해보면

  • 단일 기기, 또는 CloudKit을 사용한 다중장치간 데이터 동기화를 위한 프레임워크
  • 단일 기기 내 오프라인 사용을 위해 영구 데이터를 저장, 임시데이터 cache, 실행 취소 및 다시 실행 기능 제공
  • CloudKit을 사용하여 다중장치간의 데이터 동기화
  • CoreData의 Model editor를 통해 데이터 type과 relationship을 통해 class를 정의
  • CoreData가 runtime에 객체 인스턴스를 관리하여 사용
  • CoreData를 통해 데이터베이스를 직접 관리하지 않고도 Swift를 통해 객체를 저장소에 매핑
  • CoreData의 undo manager를 통해 변경사항을 추척하여 roll back 가능
  • Background에서 작업수행이 가능
  • table 및 collectionView에 대한 데이터소스를 제공하여 view와 데이터간의 공기화를 유지하는데 도움 제공
  • CoreData는 데이터 모델를 버전별 관리, 마이그레이션 기능 제공

예.. 정말 많은 기능을 제공하는 것 같죠?

일단, 다른 저장방법들과는 다른 점들이 여럿 있습니다. 바로 relationship, 데이터간의 관계를 맺을 수 있다는 것 입니다!
일종의 RDBMS와 같은 느낌을 제공하는 프레임워크라고 볼 수 있죠!
하지만 데이터베이스를 직접 다루지 않고 CoreData 프레임워크를 통해 Swift 언어로 추상적으로 데이터베이스를 접근하다는 것이 특징입니다!
그리고 undo나 redo와 같이 roll back이 가능하며 background 모드에서 다운로드도 됩니다!

느낌이 오시겠지만, 앱 내부에 많은 데이터를 관리하여 사용해야 하는 경우 CoreData 프레임워크를 사용해라! 라는 느낌으로 제공된 것 같아요
하지만, 많은 기능이 있고 장점이 있지만 개인적으로는 사용할 때 복잡했다.. 라는 느낌도 있었어요

저장가능한 형태

CoreData는 Core Data Model을 생성하여 데이터를 저장합니다.

Model Data

기존 프로젝트에서 new file을 통해 Core Data 섹션의 Data Model을 추가하면 됩니다!
그러면 .xcdatamodeld 확장자인 파일이 생성됩니다!

참고: coredata/creating_a_core_data_model

Entity

그리고 Data Model 내에서 name, attribute, relationthip을 포함한 Entity를 생성하여 실질적인 데이터 구조를 정의합니다.
Entity Name을 포함하여 상속관계인 Parent Entity, Versioning Hash Modifier를 통한 동일한 Entity를 버전별 관리 등을 설정할 수 있습니다.

참고: coredata/modeling_data/configuring_entities

Attribute

이렇게 Eitnty까지 생성된 이후에 비로소 저장하고자 하는 데이터들을 Attribute별로 설정할 수 있습니다!
Attribute 별로 Attribute name, 그리고 data type을 명시해야 하며 default value를 설정할 수 있습니다!

참고: coredata/modeling_data/configuring_attributes

그러면 CoreData를 통해 read/write 가능한 data type을 알아볼께요
CoreData는 NSAttributeType 값들을 저장할 수 있습니다!

  • Integer (NSNumber)
  • Double
  • Float
  • String
  • Bool
  • Date (NSDate)
  • UUID
  • URL
  • Binary data (Data)
  • Transformable (NSObject)

기본적인 common types은 저장 가능하며 추가적으로 Date, UUID가 type으로 지정이 가능하며 이 외에도 Data 형태로 저장이 가능합니다!

근데.. Transformable은 어떤 형태일까요?
바로 NSObject 타입으로 relationship을 통해 다른 Entity 타입들을 지닐수도 있답니다!

저같은 경우는 relationship을 통해 문제집 workbook 내 포함된 problem 들을 Transformable 타입으로 설정하여 [Problem_Core] 형태로 사용해봤습니다

relationship 내용에 관해서는 여기서는 중요한 부분이 아닌 것 같아 넘어가겠습니다!

그리고 추가로 중요한점은

바로 이미지와 같은 용량이 큰 데이터Data 형태로 CoreData 내 담지 말고 외부에 저장하는것을 권장한다는 점 입니다!
가능은 해요! (왜냐면 제가 그렇게 개발했어서...) 하지만 저같은 경우 메모리 차지용량도 커지고 메모리 누수도 발생하더라구요!ㅠㅠ

그래서 이전에 살펴본 FileManager를 통해 이미지와 같은 데이터들을 file로 저장하고, 이 file의 저장위치인 URL를 CoreData 내 저장하는 것이 가장 좋은 구조라고 생각합니다 :)

read/write

그러면 벌써부터 복잡해 보이는데, CoreData를 통해 데이터를 read/write 하는 방법을 알아볼께요

Code Generation Option

먼저 위에서 Entity까지 잘 설정하셨다면 runtime에 인스턴스로 생성되기 위한 class를 코드로 정의해야합니다.
그리고 이 class의 타입은 모든 CoreData model 객체가 상속받는 기본 클래스인 NSManagedObject 타입이여야 합니다.
또한 Attribute 들은 @NSManaged 프로퍼티여야 합니다.

이런 규칙에 따라서 일일이 만들기 너무 복잡하겠죠?
다행이도 이런 코드를 자동으로 생성해주는 기능이 있답니다!

그 전에, Entity 설정 내에서 class의 Codegen(Code Generation Option)을 알맞게 설정해줘야 합니다.
Manual/None 옵션을 설정하면 class 내 추가적인 메소드를 정의할 수 있답니다! 따라서 Manual/None 옵션을 설정합니다.

참고: coredata/modeling_data/generating_code

그런 다음 Xcode 상단의 Editor > Create NSManagedObject Subclass... 를 선택합니다.

그러면 class를 생성하고자 하는 Entity를 지니고 있는 Model을 택한 후 Entity를 택하면 코드가 생성됩니다!

  • Entity명+CoreDataClasses.swift
import Foundation
import CoreData

@objc(Todo)
public class Todo: NSManagedObject {

}
  • Entity명+CoreDataProperties.swift
import Foundation
import CoreData

extension Todo {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Todo> {
        return NSFetchRequest<Todo>(entityName: "Todo")
    }

    @NSManaged public var text: String?
    @NSManaged public var createdAt: Date?
    @NSManaged public var done: Bool
}

extension Todo : Identifiable {

}

이렇게 NSMAnagedObject를 채택하여 Attribute 들이 @NSManaged 프로퍼티로 생성되었습니다!

NSFetchRequest

그러면 CoreData로 저장된 값들을 class 인스턴스로 어떻게 읽을 수 있을까요?
CoreData는 데이터베이스에 read/write 하기 위하여 내부적으로 많은 구조를 거칩니다. 이를 프레임워크를 통해 추상적으로 사용하는 방식이죠
그리고 저희는 CoreData에 저장된 특정 Entity특정 조건들을 설정하여 fetch 하는 NSFetchRequest를 통해 읽을 수 있습니다!

https://developer.apple.com/documentation/coredata/nsfetchrequest

KeyChain

profile
 iOS Developer

0개의 댓글

Powered by GraphCDN, the GraphQL CDN