결론: 결국 결론은 정반합! 1 안도 2 안도 아닌 제 3안의 탄생! 제네릭 메서드로 구현하였습니다..!
프로젝트를 수행하면서 JSON
데이터의 디코딩 작업이 아래와 같이 두 단계로 이루어져 있어서 간소화하고 싶었어요.
NSDataAsset
타입의 인스턴스를 생성하여 JSON
파일의 데이터를 불러온다.JSONDecoder
타입의 decode(_:from:)
메서드를 NSDataAsset
인스턴스의 데이터를 디코딩한다.저는 1 번 작업을 생략(은닉화)해서 JSON
데이터를 디코딩하고 싶어하는 사용자들이 NSDataAsset
이라는 타입을 몰라도 메서드 이용이 가능하게끔 만들고 싶었어요. 그래서 2 가지 안을 준비해봤답니다.
associatedtype
을 활용한 초기 구현 프로토콜 제공typealias
만 지정해주면 인스턴스 생성 없이 바로 메서드 활용이 가능하다.typealias
를 한 번만 선언할 수 있기 때문에 채택한 타입 안에서 한 타입으로의 디코딩만 지원할 수 있다.1 안과 2 안을 코드를 통해 살펴보실까요?
import UIKit
struct CustomJSONDecoder<T> where T: Decodable {
private var decodingResult: T?
public mutating func decode(jsonFileName: String) -> T? {
let jsonDecoder = JSONDecoder()
guard let jsonData: NSDataAsset = NSDataAsset(name: jsonFileName) else { return nil }
do {
self.decodingResult = try jsonDecoder.decode(T.self, from: jsonData.data)
} catch {
print(error.localizedDescription)
}
return self.decodingResult
}
}
위 CustomJSONDecoder
타입은 아래와 같이 사용해요.
func test_artworks_jsonDecoding() {
let artworksJSONDecoder = CustomJSONDecoder<[Artwork]>()
sutArtworks = artworksJSONDecoder.decode(jsonFileName: "items")
for index in 0...(sutArtworks.count - 1) {
XCTAssertNotEqual(sutArtworks[index].name, nil)
XCTAssertNotEqual(sutArtworks[index].imageName, nil)
XCTAssertNotEqual(sutArtworks[index].shortDescription, nil)
XCTAssertNotEqual(sutArtworks[index].description, nil)
}
}
미리 언급했던 장단점이 느껴지시나요? 계속해서 2 안을 보시겠습니다.
associatedtype
을 활용한 초기 구현 프로토콜 제공import UIKit
protocol JSONDecodable {
associatedtype T: Decodable
func decode(jsonFileName: String) -> T?
}
extension JSONDecodable {
func decode(jsonFileName: String) ->T? {
var decodedResult: T?
let jsonDecoder = JSONDecoder()
guard let jsonData: NSDataAsset = NSDataAsset(name: jsonFileName) else { return nil }
do {
decodedResult = try jsonDecoder.decode(T.self, from: jsonData.data)
} catch {
print(error.localizedDescription)
}
return decodedResult
}
}
프로토콜이니 이 메서드를 사용하기를 원하는 타입에서 채택하면 바로 초기 구현된 decode(jsonFileName:)
를 사용할 수 있을거에요. 사용하는 모습을 볼까요?
class Expo1900Tests: XCTestCase, JSONDecodable { // 프로토콜을 채택!
typealias T = [Artwork]
// .. 중략 ..
func test_jsonDecodable_decode() {
sutArtworks = decode(jsonFileName: "items")
for index in 0...(sutArtworks.count - 1) {
XCTAssertNotEqual(sutArtworks[index].name, nil)
XCTAssertNotEqual(sutArtworks[index].imageName, nil)
XCTAssertNotEqual(sutArtworks[index].shortDescription, nil)
XCTAssertNotEqual(sutArtworks[index].description, nil)
}
}
}
그럼 저의 결론은요..!
매 번 인스턴스를 생성할 때마다 타입을 정해주는 방식보다 메서드를 사용할 때 디코딩할 타입을 지정하는 것이 편의성 측면에서 더 낫다고 생각하여 아래 코드와 같이 제네릭 타입을 제네릭 메서드로 변경했습니다. 아울러 기존 JSONDecoder
타입의 decode(_:from:)
메서드의 에러 처리를 호출하는 쪽에서 함께 담당하는 형식으로 변경하여 테스트를 수행할 때 새로 작성한 메서드에서 에러를 검출할 수 있게끔 만들었어요.
/// Decodable 프로토콜을 준수하는 타입에 대해 타입 이름과 파일 이름 입력만으로 JSON 디코딩을 도와주는 메서드를 제공하는 타입.
class CustomJSONDecoder {
/// 변환할 타입과 JSON 파일 이름을 전달인자로 받아 지정된 타입으로 디코딩 결과를 반환한다.
/// - 전달인자에 유효하지 않은 JSON 파일을 입력할 경우 `ExpoAppError.invalidJSONFile` 에러를 던진다.
/// - Parameter type: 변환할 타입. 모델 타입의 인스턴스를 원하면, `모델타입.self`로 작성한다.
/// - Parameter jsonFileName: JSON 파일 이름을 `String` 타입으로 작성한다.
public func decode<Decoded>(to type: Decoded.Type,
from jsonFileName: String) throws -> Decoded? where Decoded: Decodable {
var decodedResult: Decoded?
let jsonDecoder = JSONDecoder()
guard let jsonData: NSDataAsset = NSDataAsset(name: jsonFileName) else {
throw ExpoAppError.invalidJSONFile
}
decodedResult = try jsonDecoder.decode(Decoded.self, from: jsonData.data)
return decodedResult
}
}
사용하는 모습도 보실까요?
func test_customJSONDecoder_decode() {
let jsonDecoder = CustomJSONDecoder() // 이제 인스턴스는 한 번만 생성!
// 한 번의 인스턴스 생성으로 여러 가지 타입 디코딩이 가능!
XCTAssertNoThrow(try jsonDecoder.decode(to: ExpoIntroduction.self, from: "exposition_universelle_1900"))
XCTAssertNoThrow(try jsonDecoder.decode(to: [Artwork].self, from: "items"))
}
func test_customJSONDecoder_decode_withInvalidJSONFile() {
let jsonDecoder = CustomJSONDecoder()
XCTAssertThrowsError(try jsonDecoder.decode(to: ExpoIntroduction.self, from: "invalidJSONFile")) { (error) in
XCTAssertEqual(error as? ExpoAppError, ExpoAppError.invalidJSONFile)
} // 유효하지 않은 파일 이름을 입력하면 에러를 던질텐데, 던지는 에러가 `ExpoAppError.invalidJSONFile`가 맞는지 검증!
}
훨씬 낫네요..! 에러 처리 방식만 다듬어주면 더 좋아질 것 같습니다!