Swift 4 - Codable / JSONDecoder / JSONEncoder

개인적으로 꼽는 Swift 4의 가장 유용한 업데이트 중 하나로 Codable 프로토콜 및 JSONDecoder / JSONEncoder 를 꼽고 싶다. JSON 이라는 이름이 가지는 의미야 유명하니 무슨 말인지는 다들 알 것인데, Codable 이라는 프로토콜은 타입과 JSON을 언어 차원에서 묶어주는 방법을 제공해 준다는 점에서 파격적(?)이다.

이 글은 이 새로운 기능에 대해 겉(?)만 핥으려는 내용이다.
주의: 이 글은 Xcode 9 Beta 버전 기준으로 작성되었다. 추후 정식 릴리즈 때 변경될 가능성이 있다.

Codable

사실 Codable 은 Encodable 과 Decodable 을 합쳐놓은 프로토콜이다. 하지만 이 둘을 별개로 알지 않아도 간단하게 쓰는데는 무리가 없다.
우선 예제부터 보자. 아래는 Codable 프로토콜을 구현하는 MyType 이라는 구조체를 정의하는 코드이다.
struct MyType: Codable {
  let name: String
  let value: Int
  let hasJob: Bool
  let secondName: String?
}
여러가지 확인해 보고 싶어서 다양한 타입과 옵셔널도 집어 넣었다.

이제 이 타입에 맞는 JSON 데이터를 가지고 놀아볼 차례다.

JSONDecoder

Swift 3.x 이하에서는 JSON 데이터를 풀어헤치기(?) 위해서 JSONSerialization (Objective-C 라면 NSJSONSerialization) 이라는 클래스를 이용해 사전형 등의 표준 타입(?)으로 변경한 후 이를 다시 원하는 클래스나 구조체에 수동(?)으로 넣어주는 귀찮은 짓을 해야 했다.

하지만 Swift 4 부터는 Codable 을 따르는 구조체는 JSONDecoder 를 이용해 바로 분해해서 정리하는 것이 가능하다. Codable은 일종의 JSON 설계도 프로토콜인 셈이다.
let inputData1 = """
{ "name": "Tomato",
  "value": 100,
  "hasJob": false,
  "secondName": "is not fruit" }
""".data(using: .utf8)

let decoded1 = try JSONDecoder().decode(MyType.self, from: inputData1!)

decoded1.name       // "Tomato"
decoded1.value      // 100
decoded1.hasJob     // false
decoded1.secondName // "is not fruit"
decoded1 은 MyType 형식의 값(Value)으로 생성된다. 대충 봐도 '뭐야 이거 물건이잖아!' 라는 소리가 나온다.
참고: Swift 4 부터 새로운 문자열 표현 방법으로 """...""" 문법이 추가되었다. 멀티 라인 문자열을 정의할 수 있는 문법으로 시작 """ 의 다음 줄에서 문자열이 시작되고, 끝 """ 의 윗 줄에서 문자열이 끝난다. 참고로 Python 에도 이와 비슷한 문법이 있다.
옵셔널 필드인 secondName 을 정확히 테스트 하지 못 했기 때문에 한가지 더 시험해 보자.
let inputData2 = """
{ "name": "Onion",
  "value": 200,
  "hasJob": false }
""".data(using: .utf8)

let decoded2 = try JSONDecoder().decode(MyType.self, from: inputData2!)

decoded2.secondName // nil
일부러 입력 데이터인 inputData2 문자열에서 secondName 필드를 빼버렸다. 이렇게 하면 디코드 시 알아서 해당 부분을 옵셔널 nil 로 치환해 주는 것을 확인할 수 있다.

JSONEncoder

이제 반대로 MyType 을 JSON으로 인코딩 하려면 JSONEncoder 를 사용 할 수 있다. 아래는 앞의 예제에서 계속 이어지는 코드 예제이다.
let outputData2 = try JSONEncoder().encode(decoded2)

let outputString2 = String(data: outputData2, encoding: .utf8)
outputString2 의 내용을 굳이 적어야 할 필요는 없을 것 같다. 왜냐하면 위의 inputData2 의 내용과 동일하기 때문이다.

이상한 타입(?)을 쓰면 어떻게 될까

제목은 적당한 표현 방법이 없어서 이렇게 적긴 했는데, 그냥 아래 예제를 보는게 좋을 것 같다. 위에서 정의한 MyType 에 하나의 프로퍼티를 더 추가했다.
struct MyType: Codable {
  let name: String
  let value: Int
  let hasJob: Bool
  let secondName: String?
  let userdata: [AnyHashable : Any]
}
마지막의 userdata 라는 사전형 타입이 추가했다. 이 프로퍼티는 어찌보면 상당히 모호한 표현일거라 생각되는데, 역시나 Codable 에서 오류를 내뱉는다. 그것도 두 가지다.
Type 'MyType' does not conform to protocol 'Decodable'
- Protocol requires initializer 'init(from:)' withType 'Decodable' (Swift.Decodable)
- Cannot automatically synthesize 'Encodable' because '[AnyHashable : Any]' does not conform to 'Encodable'
Type 'MyType' does not conform to protocol 'Encodable'
- Protocol requires function 'encode(to:)' with type 'Encodable' (Swift.Encodable)
뭔가 복잡해 보이는 이 에러 메시지는 사실 단순하다. userdata 라는 프로퍼티가 Encodable 및 Decodable 프로토콜을 따르지 않는다는 이야기다. 따라서 이 부분을 구현하면 해결은 가능하겠지만, 이는 왠지 배보다 배꼽이 더 커지는 것 같아 이번 글에서는 포기한다. ;;;;;

쉽게 해결하고자 한다면 구식(?) 방법인 JSONSerialization 을 이용하자.

마무리

혁명(?)같다. 이런 것이 등장했다는 것은 최근 한국 정치사의 쾌거(?) 다음으로 기쁜 일 같다.

사실 Xcode 9 Beta 로 실습해 보면서 글을 정리하고 있는데 이유를 모르게 계속 죽어나간다. 뭔가 해보기가 너무 힘드니 여기서 마무리​ 해야겠다. ;-)

[관련글] 눈에 띄는 Swift 4 변경점들 (Xcode 9 첫 Beta 기준)
[관련글] 스위프트(Swift) 가이드​

댓글

이 블로그의 인기 게시물

소수점 제거 함수 삼총사 ceil(), floor(), round()

버전(Version)을 제대로 이해하기