iOS 면접질문 중 앱 내부 저장소에 대해서 정리해보고자 직접 공부해봤습니다!
따라서 지금까지 사용해봤던 UserDefaults 와 FileManager, 그리고 CoreData 및 KeyChain에 대해 앱 내부 저장소 관점에서 정리해보고자 합니다.
먼저 앱 내부에 데이터를 저장하기 전에 문득 이런생각이 들었어요
데이터들이 디바이스 내에서 어떻게 관리되는거지?
다른 앱과 공통적으로 사용되는 공간이 있나?
앱 별로 분리되어 저장되는 건가?
이러한 것을 먼저 이해해야 내가 저장하고자 하는 행위가 다른 앱과 연관이 있는지가 명확해질 것 같더라구요!
그래서 SandBox 구조를 한번 알아봤습니다.
정리해보면
즉, 앱이 SandBox 구조로 저장되어 다른 앱, 또는 system에 영향이 없도록 하기 위한 구조라고 볼 수 있습니다!
그리고 SandBox 정의가 바로
SandBox란 외부로부터 들어온 프로그램이 보호된 영역에서 동작해 시스템이 부정하게 조작되는것을 막는 보안 형태이다.
이렇게 SandBox 구조를 통해 보안적으로 안전한 구조가 되었다는 특징이 있습니다.
따라서 iOS 개발자들은 이러한 특징을 이해하고 개발해야겠죠?
SandBox 구조 특징으로 system 자원을 접근할때는 권한이 필요합니다. 앱 초기에 설치했을 때 대표적으로
"사용자의 위치에 접근하도록 허용하시겠습니까?"
이런 Alert 많이 봤죠?
이것이 바로 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 형식으로 분리되어 있어 다른 앱과 분리되어 있는 구조입니다.
그리고 UserDefaults
, FileManager
, CoreData
를 통해 저장된 데이터는 SandBox 내 저장
됩니다.
따라서 앱 외부에 영향이 생기지 않으며, 앱을 지울 때 같이 지워진다는 점이 특징입니다!
그리고 KeyChain
의 경우 시스템 전반적으로 접근가능한 공용저장소 영역
이라 볼 수 있기에 더욱 주의해서 사용해야 한다는 점이 특징입니다!
그러면 이제 하나씩 알아보겠습니다!
https://developer.apple.com/documentation/foundation/userdefaults
정리를 해보면
defaults database
에 대한 인터페이스preference
를 저장하여 동작을 커스텀key-value
형태로 저장UserDefaults
객체를 사용하여 read 하며 cache
됩니다.동작중인 프로세스 내에서는 synchronously
하게 변경되고, 다른 프로세스 및 persistant storage는 asynchronously
하게 변경됩니다.아마 대부분 아시겠지만 UserDefaults는 간단한 환경설정(preference)
과 같은 값들을 저장하는 곳이라고 생각하시면 됩니다! 모든 정보를 UserDefaults에 저장하는건 바람직하지 않죠
여기서 몇가지 새로운 사실을 알 수 있었어요!
바로 cache
가 된다는 점과 UserDefaults 객체를 부른 프로세스 내에서만 동기적
으로 동작되었다는 사실!
아마도 이런 점 때문에 시뮬레이터로 앱을 개발할 때 가끔 UserDefaults에 저장이 안되는 것 같았네요
그러면 UserDefaults를 통해 read/write 가능한 형태를 알아볼께요
이렇게 저장가능한 형태를 보면 기본적인 값들을 저장하는 형태라는 것도 알 수 있습니다.
만약 특정 struct를 저장하고 싶다면 NSData 형태로 변환하여 저장하는 방법이 가능하겠죠?
그러면 UserDefaults를 통해 read/write 방법을 알아볼께요
// 반환 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하여 사용하는 메소드를 통해 읽을 수 있습니다!
UserDefaults.standard.set(floatValue, forKey: "floatValue")
UserDefaults.standard.set(doubleValue, forKey: "doubleValue")
...
write의 경우는 메소드 형태만 보면 저장하고자 하는 타입에 상관없이 set 메소드를 통해 저장이 가능합니다!
사실 이는 내부적으로 overloading을 통해(같은 메소드명, 다른 파라미터 타입) common types에 따라 여러 메소드가 있지만 결론적으론 set 메소드를 사용해서 저장하는 형태로 구현되어 있답니다!
추가로 저는 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를 사용하고 있습니다! :)
https://developer.apple.com/documentation/foundation/filemanager
정리를 해보면
file system
과 상호 작용 가능한 인터페이스file
과 directory
를 locate, create, copy, move 할 수 있습니다.URL
을 사용하여 위치를 지정iCloud
내 저장되어 있는 정보를 관리할 수 있는 메소드 존재iCloud와 동기화
된다.thread-safe
하지만 delegate를 통해 notification을 수신시 FileManager 개체의 고유한 인스턴스가 필요FileManager는 UserDefaults와는 조금 다르게 file-system 기반으로 저장된다고 볼 수 있습니다!
즉, 저장하고자 하는 데이터가 file 형태
로, 그리고 directory 별로 관리
가 될 수 있다는 것입니다!
동일한 형태의 여러 데이터를 저장할때도, 데이터 형태별로 각기 저장할때도 file 형식으로 저장할 수 있겠죠?
간단하게는 .json
파일로 저장하여 JSONEncoder
, JSONDecoder
를 통해 read/write가 가능하답니다!
file system 내 저장되기 때문에 Data
값을 토대로 저장하게 됩니다!
따라서 Codable을 채택한 Struct
를 encode
하여 Data를 저장하거나, 특정 Data를 직접 저장하면 됩니다!
예를 들어 Struct를 저장한다면
do {
let data = try JSONEncoder().encode(myStruct)
} catch {
print(error.localizedDescription)
}
이렇게 데이터를 추출하여 저장할 수 있고,
이미지를 저장한다면
let data = UIImage(named: "image")?.pngData()
식으로 데이터를 추출하여 저장하면 되겠죠?
그러면 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를 살펴보겠습니다!
// 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? 값을 얻을 수 있습니다!
// 저장하고자 하는 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를 통해 공유 또한 가능하답니다! :)
https://developer.apple.com/documentation/coredata/
정리를 해보면
예.. 정말 많은 기능을 제공하는 것 같죠?
일단, 다른 저장방법들과는 다른 점들이 여럿 있습니다. 바로 relationship, 데이터간의 관계
를 맺을 수 있다는 것 입니다!
일종의 RDBMS와 같은 느낌을 제공하는 프레임워크라고 볼 수 있죠!
하지만 데이터베이스를 직접 다루지 않고 CoreData 프레임워크를 통해 Swift 언어로 추상적으로 데이터베이스를 접근
하다는 것이 특징입니다!
그리고 undo나 redo와 같이 roll back이 가능하며 background 모드에서 다운로드도 됩니다!
느낌이 오시겠지만, 앱 내부에 많은 데이터를 관리하여 사용
해야 하는 경우 CoreData 프레임워크를 사용해라!
라는 느낌으로 제공된 것 같아요
하지만, 많은 기능이 있고 장점이 있지만 개인적으로는 사용할 때 복잡했다.. 라는 느낌도 있었어요
CoreData는 Core Data Model
을 생성하여 데이터를 저장합니다.
기존 프로젝트에서 new file
을 통해 Core Data 섹션의 Data Model
을 추가하면 됩니다!
그러면 .xcdatamodeld
확장자인 파일이 생성됩니다!
참고: coredata/creating_a_core_data_model
그리고 Data Model 내에서 name
, attribute
, relationthip
을 포함한 Entity
를 생성하여 실질적인 데이터 구조를 정의합니다.
Entity Name
을 포함하여 상속관계인 Parent Entity
, Versioning Hash Modifier
를 통한 동일한 Entity를 버전별 관리 등을 설정할 수 있습니다.
참고: coredata/modeling_data/configuring_entities
이렇게 Eitnty
까지 생성된 이후에 비로소 저장하고자 하는 데이터들을 Attribute
별로 설정할 수 있습니다!
Attribute 별로 Attribute name
, 그리고 data type
을 명시해야 하며 default value
를 설정할 수 있습니다!
참고: coredata/modeling_data/configuring_attributes
그러면 CoreData를 통해 read/write 가능한 data type을 알아볼께요
CoreData는 NSAttributeType 값들을 저장할 수 있습니다!
기본적인 common types
은 저장 가능하며 추가적으로 Date
, UUID
가 type으로 지정이 가능하며 이 외에도 Data
형태로 저장이 가능합니다!
근데.. Transformable
은 어떤 형태일까요?
바로 NSObject
타입으로 relationship
을 통해 다른 Entity 타입
들을 지닐수도 있답니다!
저같은 경우는 relationship을 통해 문제집 workbook 내 포함된 problem 들을 Transformable 타입으로 설정하여 [Problem_Core] 형태로 사용해봤습니다
relationship 내용에 관해서는 여기서는 중요한 부분이 아닌 것 같아 넘어가겠습니다!
그리고 추가로 중요한점은
바로 이미지
와 같은 용량이 큰 데이터
를 Data 형태로 CoreData 내 담지 말고 외부에 저장하는것을 권장
한다는 점 입니다!
가능은 해요! (왜냐면 제가 그렇게 개발했어서...) 하지만 저같은 경우 메모리 차지용량도 커지고 메모리 누수도 발생하더라구요!ㅠㅠ
그래서 이전에 살펴본 FileManager
를 통해 이미지와 같은 데이터들을 file로 저장
하고, 이 file의 저장위치인 URL를 CoreData 내 저장
하는 것이 가장 좋은 구조라고 생각합니다 :)
그러면 벌써부터 복잡해 보이는데, CoreData를 통해 데이터를 read/write 하는 방법을 알아볼께요
먼저 위에서 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
.swiftimport Foundation
import CoreData
@objc(Todo)
public class Todo: NSManagedObject {
}
Entity명+CoreDataProperties
.swiftimport 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 프로퍼티로 생성되었습니다!
그러면 CoreData로 저장된 값들을 class 인스턴스로 어떻게 읽을 수 있을까요?
CoreData는 데이터베이스에 read/write 하기 위하여 내부적으로 많은 구조를 거칩니다. 이를 프레임워크를 통해 추상적으로 사용하는 방식이죠
그리고 저희는 CoreData에 저장된 특정 Entity
와 특정 조건들을 설정
하여 fetch
하는 NSFetchRequest
를 통해 읽을 수 있습니다!
https://developer.apple.com/documentation/coredata/nsfetchrequest