
글 작성 개요 : 상속을 받거나, 상위타입으로 묶인 데이터를 한번에 encode해야하는 상황이 발생 했을 때 어떤 방식으로 하면 좋을까?
ex) var stores: [Store] = [appleStore, blueBerryStore, candyStore]을 한번에 encode하고 싶은데 어떻게 할까?
일단 상위 타입에 대해서 고민해 봤습니다. 상위 타입은 struct일수도 있고 class일수도 있다. struct는 상위 타입이란 말이 어색할 수 있습니다. 하지만 이를 protocol을 통해서 구현 할 수 있습니다. protocol을 통해서 구현하게 된다면 객체를 상속 받은 것 처럼 타입을 통해 묶을 수 있습니다.
이를 통해서 작성해 보겠습니다. 일단 Protocol을 통해서 묶는다면 상위에 존재하고, 그리고 하위에 꼭 필요한 것들을 묶어서 만듭니다.
하위 객체 만들기
다음은 상위에 존재하는 것 처럼 만드는 protocol입니다.
protocol Store: Codable {
var sellCount: Int {get}
var price: Int {get}
var storeType: StoreType {get}
}
enum StoreType: String, Codable {
case apple
case chicken
}
그리고 하위타입을 만들어준다. Protocol Store을 만족해야 합니다. 그리고 이 객체들은 Codable이라는 프로토콜을 만족시켜야 한다. (차후 encode decode둘다 이루어 지기 때문입니다.)
struct ChickenStore: Store, Codable {
let price: Int
let chickenTypeCount: Int
let sellCount: Int
let storeType: StoreType
}
struct AppleStore: Store, Codable {
let price: Int
let iphonType: Int
let sellCount: Int
let storeType: StoreType
}
그리고Json데이터로 decode할 수 있게 새로운 init함수를 만들어 줍니다. decoder.container를 활용하기 위해 codingKeys를 만들어 줍니다.
struct ChickenStore: Store, Codable {
let price: Int
let chickenTypeCount: Int
let sellCount: Int
let storeType: StoreType
enum CodingKeys: String, CodingKey {
case price
case chickenTypeCount
case sellCount
case storeType
}
init(price:Int, chickenTypeCount: Int, sellCount: Int) {
self.price = price
self.chickenTypeCount = chickenTypeCount
self.sellCount = sellCount
self.storeType = .chicken
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.price = try container.decode(Int.self, forKey: .price)
self.chickenTypeCount = try container.decode(Int.self, forKey: .chickenTypeCount)
self.sellCount = try container.decode(Int.self, forKey: .sellCount)
self.storeType = try container.decode(StoreType.self, forKey: .storeType)
}
}
struct AppleStore: Store, Codable {
let price: Int
let iphonType: Int
let sellCount: Int
let storeType: StoreType
init(price: Int, iphonType:Int , sellCount:Int) {
self.price = price
self.iphonType = iphonType
self.sellCount = sellCount
self.storeType = .apple
}
enum CodingKeys: String, CodingKey {
case price
case iphonType
case sellCount
case storeType
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.price = try container.decode(Int.self, forKey: .price)
self.iphonType = try container.decode(Int.self, forKey: .iphonType)
self.sellCount = try container.decode(Int.self, forKey: .sellCount)
self.storeType = try container.decode(StoreType.self, forKey: .storeType)
}
}
묶음 객체 만들기
묶음객체를 단순히 array형태로 선언해도 좋지만, 어디선가 묶어서 filter나 다양한 함수들을 활용할 것으로 일반화 하였습니다. 추가로 Store protocol을 만족하는 array형태가 묶여 있다면 다음과 같이 json으로 인코드할 수 있습니다.
let tempStores: [Store] = [
AppleStore(price: 10, iphonType: 5, sellCount: 3),
ChickenStore(price: 20, chickenTypeCount: 20, sellCount: 20),
AppleStore(price: 10, iphonType: 10, sellCount: 10),
ChickenStore(price: 20, chickenTypeCount: 20, sellCount: 20),
]
struct SomeStore {
var stores: [Store] = [] // Store 프로토콜을 준수하는 타입으로 배열 선언
func encode() -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
var dataArray: [Data] = []
stores.forEach { store in
if let json = try? encoder.encode(store) {
dataArray.append(json)
}
}
// 배열의 데이터를 결합하여 전체 데이터 생성
let combinedData = dataArray.reduce(Data()) { (result, data) -> Data in
var mutableResult = result
if !mutableResult.isEmpty {
mutableResult.append(",".data(using: .utf8)!)
}
mutableResult.append(data)
return mutableResult
}
// 양 끝에 대괄호 [] 추가
var finalData = Data()
finalData.append("[".data(using: .utf8)!)
finalData.append(combinedData)
finalData.append("]".data(using: .utf8)!)
// 합쳐진 데이터를 문자열로 출력
if let jsonString = String(data: finalData, encoding: .utf8) {
print(jsonString)
}
return finalData
}
init() {}
}
묶음 객체는 다음과 같은 값을 가집니다. json으로 포멧팅 했습니다.
[{
"price" : 10,
"sellCount" : 3,
"iphonType" : 5,
"storeType" : "apple"
},{
"price" : 20,
"sellCount" : 20,
"storeType" : "chicken",
"chickenTypeCount" : 20
},{
"price" : 10,
"sellCount" : 10,
"iphonType" : 10,
"storeType" : "apple"
},{
"price" : 20,
"sellCount" : 20,
"storeType" : "chicken",
"chickenTypeCount" : 20
}]
그리고 DecodeFactory를 만듭니다. type에 따라서, chicken인지 apple인지 나누고 이에 따라 내부변수 store에 저장합니다. (사실 init말고 func에 내부 매서드를 통해서 JSONSerialization 해결하면 되었지만 귀찮고 복잡해져서 다음과 같이 진행하였습니다.)
key를 통해서, chicken인지 apple인지 판별하고 각자 decode를 하게 해주었습니다. 그리고 생성된 값을 tempStore이라는 내부 변수에 저장해주었습니다.
struct DecodeFactory: Decodable {
enum Key: String, CodingKey {
case type = "storeType"
}
var tempStore: Store? = nil
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
let typeString = try container.decode(String.self, forKey: .type)
let type = StoreType(rawValue: typeString)
switch type {
case .chicken:
let store = try ChickenStore(from: decoder)
tempStore = store
case .apple:
let store = try AppleStore(from: decoder)
tempStore = store
case nil:
break
}
}
}
그리고 다음과 같이사용했습니다.
let temp = try! JSONDecoder().decode([DecodeFactory].self, from: targetJson)
let stores: [Store] = temp.compactMap{$0.tempStore}
이렇게 된다면 정상적으로 decode된 것을 확인할 수 있었습니다.
print("지금부터 디코딩된 값을 print합니다.")
stores.forEach{print($0)}

한계점
1. Solid원칙에 의해서 Store가 생길 때 마다 enum StoreType에 추가해야한다는 단점이 존재한다.
2. 내부에 optional값을 두어서 compactmap을 활용하는데, 이것이 사실 불필요할수도 있음.
참고자료
https://learn-hyeoni.tistory.com/45
https://developer.apple.com/documentation/swift/decoder/container(keyedby:)
https://developer.apple.com/documentation/swift/keyeddecodingcontainer
https://developer.apple.com/documentation/swift/decodable
https://developer.apple.com/documentation/swift/codable
https://nilcoalescing.com/blog/BringingPolymorphismToCodable/
https://david.y4ng.fr/codable-with-mixed-types-of-data/
https://medium.com/@indefini/decoding-json-in-swift-what-if-a-field-has-multiple-types-251838da42d0
'Swift' 카테고리의 다른 글
[iOS] 모듈화 하기 vs 그냥 살기. (주관적으로 느낀 모듈화 장점 3가지) (0) | 2024.11.22 |
---|---|
[iOS] SwiftData를 활용한 간단한 Todo 어플리케이션 만들어 보기 (0) | 2024.06.02 |
[iOS] Weak Dictionary 다이브 (1) | 2024.02.16 |
[swift] 비동기적으로 메시지를 전달하는 방법 part 1 (delegate) (0) | 2023.10.15 |
Swift 타입 지정하는 법 (0) | 2023.06.30 |