[Swift] JSON 데이터 디코딩 지원 타입 만들기

Ryan (Geonhee) Son·2021년 4월 9일
0

오늘의 고민

목록 보기
2/10

디코딩 작업을 할 때마다 매 번 JSONDecoderNSAssetData의 인스턴스를 생성해서 이용해야 하니 번거롭다고 느꼈습니다. 그래서 이 작업을 줄이기 위해 두 가지 방법을 고안해봤어요.
1. 제네릭 타입이 적용된 타입을 만들어 디코딩할 타입에 맞는 인스턴스를 생성해서 JSON 파일 이름만으로 디코딩하는 방법
2. 디코딩 메서드를 지원하는 프로토콜을 만들고 초기 구현하여 프로토콜을 채택하는 타입에서 디코딩할 타입을 정할 수 있도록 만드는 방법

코드로 보시는 것이 더 편하실 것 같네요..!


1 안. 제네릭 타입이 적용된 타입을 만들어 디코딩할 타입에 맞는 인스턴스를 생성해서 JSON 파일 이름만으로 디코딩하는 방법

아래 CustomJSONDecoder 타입은 제네릭 타입을 적용해서 인스턴스를 생성하는 시점에 디코딩할 타입을 설정할 수 있어요. NSDataAsset 타입 사용을 은닉하고 디코딩할 JSON 파일 이름만을 전달인자로 요구하니 사용하는데 많은 것을 알아볼 필요도 없어보여요. 코드를 보시면 바로 이해하실 수 있으실겁니다!

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)
  }
}

역할과 책임이 훨씬 분할되었죠? NSDataAsset 타입 사용을 은닉하니 사용자 입장에서도 디코딩이라는 목적에 더 집중할 수 있겠어요.

2 안. 디코딩 메서드를 지원하는 프로토콜을 만들고 초기 구현하여 프로토콜을 채택하는 타입에서 디코딩할 타입을 정할 수 있도록 만드는 방법

이번에는 비슷하지만 초기 구현된 프로토콜로 구성해봤어요. 코드를 보실까요?

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)
    }
  }
}

인스턴스를 만들 필요도 없이 decode(jsonFileName:) 메서드를 활용할 수 있게 되었네요! 디코딩할 타입은 어떻게 정해주고 있나요? 프로토콜에서 associatedtype으로 정해준 타입 이름을 채택한 타입에서 typealias T = [Artwork] 문구로 지정할 수 있어요.

지금까지 보니 기존의 방식보다 한 단계 수월해진건 맞아보이는데, 두 방식에 어떤 장단점이 있을까요?

각 방식의 장단점

  • 1 안. 제네릭 타입을 지원하는 타입
    • 장점: 한 타입 안에서 여러 타입으로의 디코딩 작업이 가능하다. (이해가 안된다면 프로토콜의 단점 참고)
    • 단점: 디코딩할 타입이 여러 개인 경우 여러 개의 인스턴스를 생성하여 사용하여야 하므로 이 작업이 번거로울 수 있다.
  • 2 안. associatedtype을 활용한 초기 구현 프로토콜
    • 장점: 사용할 타입에서 채택하고 typealias만 지정해주면 인스턴스 생성 없이 바로 메서드 활용이 가능하다.
    • 단점: 채택한 타입 내부에서 typealias를 한 번만 선언할 수 있기 때문에 채택한 타입 안에서 한 타입으로의 디코딩만 지원할 수 있다.

저는 현재 진행하는 프로젝트에서 프로토콜로 해당 메서드를 제공하는 것이 더 나은 방법이라 판단하여 프로토콜로 적용하였는데, 이후 피드백을 받아보고 향후 진행 방향을 최종 결정해보겠습니다!

profile
합리적인 해법 찾기를 좋아합니다.

0개의 댓글