Struct와 Codable 프로토콜을 통해 주로 Server Response Data를 받습니다.
하지만, API 수정 및 신규 배포 등 의 문제로 인해 예상치 못한 String Key 값이 오거나,
자료형이 다른 Response Data가 올 수 있습니다.
보통, 이런 Decoding Error가 발생하게 되면 해당 데이터 전체를 쓸수가 없게됩니다.
즉 10개의 프로퍼티가 있는 Model 에서 유저가 몰라도 되는 프로퍼티의 자료형이 다르게 온다면,
해당 데이터 전체에 대한 Decoding Error가 발생하게 됩니다.
그렇다면, 좀 더 유연해질 순 없을까요?
물론, 쉽고 편하게 가능합니다 !
나아가, String Key 값을 보통 enum으로 처리하게 되는데요.
(String으로 값을 받은 후, enum으로 관리하는 방법 포함)
원하지 않는 String Key 값이 온경우에도 enum으로 유연하게 대응하는 방법도
알아보겠습니다 ~
1. 자료형에 관하여 유연해지기
struct MusicModel: Codable {
let title: String?
let artistName: String?
let isHifi: Bool?
let musicGenreName: String?
}
위와 같은 Modeling의 문제점이 뭐라고 생각하시나요?
자칫 보면, 별 문제 없는 것 아닌가? 라는 생각이 들기도 합니다.
해당 MusicModel Data가 배열로 100개의 데이터가 들어온다고 가정하고, isHifi Data가
Bool 형이 아닌 String 값으로 내려져 온다면 어떤 결과가 생길까요?
catch log: typeMismatch(Swift.Bool, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 1", intValue: 1), CodingKeys(stringValue: "isHifi", intValue: nil)], debugDescription: "Expected to decode Bool but found a string/data instead.", underlyingError: nil))
전체 100개의 데이터를 1개도 사용하지 못하고, Decoding Error(typeMismatch)가 발생하게 됩니다.
결국 위와 같은 Modeling은 Json Data가 null로 내려오게 되는
부분에 대해서만 Cover 가능합니다.
struct MusicModel: Codable {
let title: String?
let artistName: String?
let isHifi: Bool?
let musicGenreName: String?
init(from decoder: Decoder) throws {
/// decodeIfPresent 사용해도 됩니다.
let values = try decoder.container(keyedBy: CodingKeys.self)
self.title = try? values.decode(String.self, forKey: .title)
self.artistName = try? values.decode(String.self, forKey: .artistName)
self.isHifi = try? values.decode(Bool.self, forKey: .isHifi)
self.musicGenreName = try? values.decode(String.self, forKey: .musicGenreName)
}
}
위와 같이 Struct의 init(from decoder:) 생성자를 이용해
디코딩 과정 중에서 Decoding Error를 throws 하지 않고, nil로 처리 가능합니다.
[{
"artistName" : "BTS",
"isHifi" : true,
"title" : "피땀눈물",
"musicGenreName" : "KPop"
},
{
"artistName" : null,
"isHifi" : "true",
"title" : "asdf",
"musicGenreName" : "Pop"
}]
위와 같은 데이터를 보면, isHifi 값 하나가 String 으로 내려져 옵니다.
[__lldb_expr_135.MusicModel(title: Optional("피땀눈물"), artistName: Optional("BTS"), isHifi: Optional(true), musicGenreName: Optional("KPop")), __lldb_expr_135.MusicModel(title: Optional("asdf"), artistName: nil, isHifi: nil, musicGenreName: Optional("Pop"))]
올바르게온 BTS 데이터는 그대로 오고, artisName이 없는 isHifi는 nil처리가 되었습니다.
즉, 올바르게 온 Bts MusicModel Data는 사용가능하죠.
홈 화면에서 100개의 뮤직 데이터를 보여준다고 가정했을 때,
Hifi 지원 여부를 보여주지 못한다고 해도 100개의 음악 데이터 전체를 보여주는 것이 바람직 하죠?
물론, 각 서비스 정책에 따라 1개의 Data의 자료도 틀리다면 유저에게 보여주지 말아야 할
상황도 존재한다고 생각됩니다.
다만, iOS 앱 클라이언트 서비스는 이미 배포된 코드에 대해 실시간 수정이 불가능합니다.
따라서 이러한 '안전장치'가 존재한다면 예상치 못한 상황을 Cover 할 수 있다고 생각합니다.
서비스 장애는 항상 예상치 못한 곳에서 발생하기 때문이죠.
회사의 핵심 비지니스와 관련된 View에 API 수정으로 인한 Data 오류가 발생했을 때
특정 데이터(고음질 지원 여부와 같은)가 보여지지 않더라도, 유저가 서비스의 핵심 로직을
사용할 수 있어야 한다고 생각합니다.
자, 다시 위에 Modeling 코드를 보면 또 부족한 점이 보이실 겁니다.
에러에 대한 유연한 처리 다 좋아 ~~ 🙂
근데 실제 Product에서 정말로 Data가 잘못 내려져 온다면, 빨리 파악해서 수정이 되어야 하는데
또 개발자 들은 해당 부분을 바로 파악하고 대응해야 하는데
위의 코드론 대응하지 못할 것 같은데? 🤔
맞습니다 !
더 완벽하게 대응해볼까요?
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.title = try? values.decode(String.self, forKey: .title)
self.artistName = try? values.decode(String.self, forKey: .artistName)
self.isHifi = try? values.decode(Bool.self, forKey: .isHifi)
self.musicGenreName = try? values.decode(String.self, forKey: .musicGenreName)
if self.title == nil || self.artisName == nil ... {
/// 실시간 Decoding Error 로깅 추가
/// 개발 환경에선 바로 Decoding Error를 파악할 수 있도록
#if DEBUG
throw DecodingError
#endif
}
}
위와 같이 Decoding Error가 발생해서 nil이 된 프로퍼티를
확인 해서 로깅 처리를 추가해줄 수 있습니다.
또한, 전처리문을 통해 개발 환경에선 해당 Model의 Error를
바로 파악할 수 있게 합니다 !!
그럼 nullable한 Data는 어떻게 유연하게 처리하면서 로깅하죠?
다 가능합니다 😺
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.title = try? values.decode(String.self, forKey: .title)
do {
self.artistName = try values.decode(String.self, forKey: .artistName)
} catch DecodingError.typeMismatch(let type, let context) {
/// MusicModel 디코딩 에러 실시간 로깅
self.artistName = nil
} catch {
self.artistName = nil
}
self.isHifi = try? values.decode(Bool.self, forKey: .isHifi)
self.musicGenreName = try? values.decode(String.self, forKey: .musicGenreName)
if self.title == nil || self.isHifi == nil || self.musicGenreName == nil {
/// 실시간 Decoding Error 로깅 추가
}
}
먼저 artistName이 nullable한 Data라고 생각할게요.
그렇다면 artistName 이 nil인 경우에는 실시간 로깅을 하지 않습니다.
nil이 오는 것이 정상적인 CASE 니까요.
하지만, artistName의 데이터가 String으로 Server에서 내려져 와야하는데
Double 이나 Int가 내려져 왔다면,
do catch 구문을 통해 해당 Decoding Error를 로깅하고, artistName에 nil이 들어가게 합니다.
typeMismatch여도 나머지 올바르게 온 데이터를 모두 사용해야 하니까요 ~
catch let error as DecodingError {
/// 각각의 DecodingError에 대해 로깅 및 별도의 로직 처리도 가능합니다.
switch error {
case DecodingError.typeMismatch(let type, let context):
case DecodingError.dataCorrupted(let context):
case .valueNotFound(_, _):
/// nullable한 데이터는 해당 케이스에 대해서 로깅하면 안되겠죠 ?
case .keyNotFound(_, _):
default:
} catch {
self.artistName = nil
}
error를 DecodingError로 캐스팅해서 각각의 DecodingError Case 마다
별도의 로깅 및 로직처리도 가능합니다.
당연히, 코드는 길어지지만 별도의 Server Response Data를 검증하는 과정의 레이어를
당장 추가하기 어렵다면, 위와 같은 코드들로 더욱 유연한 데이터 처리가 가능하다고 생각됩니다.
또한, 위의 실시간 로깅이 잘 이루어져야 하고 해당 로깅을
실시간으로 후킹해서 짧은 시간에 지속적으로 발생하는 로깅을 즉각적으로
대응할 수 있어야 합니다.
다음 시간엔 유연한 enum 처리에 대해 말해 보죵 ~
'Swift information' 카테고리의 다른 글
클로저 - Capturing Values와 ARC (0) | 2021.12.03 |
---|---|
ARC 시리즈 2 - SideTable (0) | 2021.11.04 |
ARC 시리즈 1 - Retatin Cycle 과 Reference Count (0) | 2021.11.02 |
Struct와 Class에서의 let 과 var의 차이에 대하여 (0) | 2021.11.02 |