활동 요약

  • UIKitNSDataAssetFoundationJSONDecoder를 이용한 JSON 디코딩 방법 학습
  • 작성한 모델 타입 검증을 위한 테스트 메서드 작성 및 테스트
  • CustomJSONDecoder 타입, JSONDecodable 프로토콜 작성
  • 만국박람회 프로젝트 Step 1 PR
  • [야곰의 TechCast] 개발자의 면접 노하우 세미나 참석

활동 상세

UIKitNSDataAssetFoundationJSONDecoder를 이용한 JSON 디코딩 방법 학습

JSON 데이터를 최종적으로 디코딩하기 위해 Foundation 모듈에 내장된 JsonDecoder 타입의 인스턴스를 활용하는 경우가 많습니다.

JSONDecoder 타입에는 제네릭이 활용된 decode(_:from:)이라는 멋진 인스턴스 메서드가 작성되어 있기 때문이에요. 이 인스턴스 메서드는 디코드할 데이터를 필요로 하는데 이 데이터를 NSDataAsset 타입을 통해 만들 수 있습니다!

데이터를 만들면 미리 언급한 decode(_:from:) 인스턴스 메서드를 통해 바로 디코딩 결과물을 반환 받을 수 있으니 함께 이용해보고 결과물도 print 해보겠습니다. 먼저 AssetsJSON 파일을 import 해주시는 것 잊지마세요!

저는 ViewController에서 해볼게요~!

import UIKit // UIKit 모듈에는 Foundation 모듈이 내장되어 있으니 따로 import 해주시지 않으셔도 됩니다.

class ViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    
    var artworks: [Artworks]?
    let jsonDecoder = JSONDecoder()
    guard let jsonData: NSDataAsset = NSDataAsset(name: "korean-artworks") else { return }
    
    do {
      artworks = try jsonDecoder.decode([Artwork].self, from: jsonData.data)
    } catch {
      print(error.localizedDescription)
    }
    
    print(artworks)
  }
}

결과물을 보시면 성공적으로 디코딩된 모습을 확인하실 수 있습니다!


작성한 모델 타입 검증을 위한 테스트 메서드 작성 및 테스트

작성을 했으면 검증을 하는 것이 도리! 바로 테스트해봅시다. 이니셜라이징이 제대로 되는지 먼저 확인해볼까요?

Note

잘 모를 때 작성해서 아래와 같은 이니셜라이징 성패 여부 테스트를 했는데, Swift의 이니셜라이저는 init?()과 같은 실패 가능한 이니셜라이저가 아니라면 사용되기 전에 이니셜라이징의 성공을 보장하고 있습니다.
참고 링크: The Swift Programming Language - Initialization

func test_artwork_Initializing() {
    sutArtwork = Artwork(name: "Name",
                         imageName: "Image name",
                         shortDescription: "Short description",
                         description: "Description")

    XCTAssertEqual(sutArtwork.name, "Name")
    XCTAssertEqual(sutArtwork.imageName, "Image name")
    XCTAssertEqual(sutArtwork.shortDescription, "Short description")
    XCTAssertEqual(sutArtwork.description, "Description")
}

func test_expoIntroduction_Initializing() {
    sutExpoIntroduction = ExpoIntroduction(title: "Title",
                                           visitors: 1234567890,
                                           location: "Location",
                                           duration: "1900. 04. 14 - 1900. 11. 12",
                                           description: "Description",
                                           artworks: [])

    XCTAssertEqual(sutExpoIntroduction.title, "Title")
    XCTAssertEqual(sutExpoIntroduction.visitors, 1234567890)
    XCTAssertEqual(sutExpoIntroduction.location, "Location")
    XCTAssertEqual(sutExpoIntroduction.duration, "1900. 04. 14 - 1900. 11. 12")
    XCTAssertEqual(sutExpoIntroduction.description, "Description")
    XCTAssertEqual(sutExpoIntroduction.artworks, [])
}

Unit Testing Bundle에서 위 테스트 메서드들의 테스트 성공을 확인했어요. 이니셜라이징 성공을 확인했으니 디코딩을 통해 정상적으로 데이터를 받아올 수 있는지 확인해볼까요?

func test_artworks_jsonDecoding() {
  let decoder = JSONDecoder()
  
  guard let artworksData: NSDataAsset = NSDataAsset(name: "items") else { return }
  
  do  {
    self.sutArtworks = try decoder.decode([Artwork].self, from: artworksData.data)
  } catch {
    print(error.localizedDescription)
  }
  
  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)
  }
}

멋지네요! 위 예시에서는 XCTAssertNotEqual(_:_:) 메서드로 검증했지만 디코딩 작업을 새로운 메서드로 래핑해서 XCTAssertNoThrow(_:)로 검증하는 방법도 있겠죠? 이 검증 메서드는 메서드의 인스턴스의 프로퍼티들이 nil이 아니면 내용에 관계 없이 통과시킨다는 단점이 있으니까요..!


CustomJSONDecoder 타입, JSONDecodable 프로토콜 작성

디코딩 작업을 할 때마다 매 번 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를 한 번만 선언할 수 있기 때문에 채택한 타입 안에서 한 타입으로의 디코딩만 지원할 수 있다.

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

만국박람회 프로젝트 Step 1 PR

  • 이번 프로젝트 리뷰어는 무려 야곰!!!
  • PR에 현재 고민 중인 부분을 모두 포함해서 적다보니 굉장히 길어졌는데, 한 편으로는 고민을 많이해봤다고 생각해서 흐뭇한 느낌도 있다..!

[야곰의 TechCast] 개발자의 면접 노하우 세미나 참석

기억에 남은 내용은..

  • 모르는 기술은 쓰지도 마라!
  • 신입은 지원 분야보다 CS가 중요할 수 있다.
  • 본인만의 매력 포인트 (필살기)를 만들 것!
profile
합리적인 해법 찾기를 좋아합니다.

0개의 댓글