Swift Codable Protocol & 실전상황 대처

Codable

A type that can convert itself into and out of an external representation.

자신을 변환하거나 외부표현(external representation)으로 변환할 수 있는 타입이다.

여기서 외부표현(external representation)이란 JSON과 같은 데이터 형태를 말할 수 있다.

typealias Codable = Decodable & Encodable

Codable 은 Decodable 과 Encodable 프로토콜을 준수하는 타입(프로토콜) 이다.

image

  • Decodable : 자신을 외부표현(external representation)에서 디코딩 할 수 있는 타입
  • Encodable : 자신을 외부표현(external representation)에서 인코딩 할 수 있는 타입

Codable JSON Encoding

웹 서버에 해당 JSON 데이터를 POST 할때 Encoding 작업이 필요하다.

  1. JSONEncoder 선언
  2. JSONEncoder의 encode 메서드를 사용하여 인스턴스를 Data 타입으로 만든다.
  3. Data 타입을 String 타입으로 만든다.
struct Person: Codable {
  var name: String
  var age: Int
}

Person 구조체는 Codable 을 채택했다. 그렇기 떄문에 JSON(외부 표현 중 하나)로 변환할 수 있다.

// JSONEncoer 객체 생성
let encoder = JSONEncoder()
// Encoding 할 구조체 타입 인스턴스 생성
let gaki = Person(name: "Gaki", age: 25)

let jsonData = try? encoder.encode(gaki)

생성한 Codable 프로토콜을 준수하는 Person 구조체 타입의 인스턴슻를 JSONEncoder 의 encode 함수에 넘겨준다.

// JSONEncoder 의 encode 메서드 
open func encode<T>(_ value: T) throws -> Data where T : Encodable

위의 encode 함수를 보면 제네릭 타입으로 어떤 값이든 들어와도 되는데 Encodable 프로토콜을 준수하는 전제 하에 말이다. 그리고 throws 가 명시 되 어있어 encoding 중에 에러를 발생시킬 수 있기 떄문에 반드시 try 와 함께 사용해야한다.

if let jsonData = jsonData, let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)	// {"name":"Gaki","age":25}
}
{
    "name" : "Gaki",
    "age" : 25
}
// 위와 같이 JSON 출력
encoder.outputFormatting = .prettyPrinted
// Key로 정렬 즉 사전순서대로 age -> name 순으로 나오게 된다.
encoder.outputFormatting = .sortedKeys
// 두개의 옵션을 아래와 같이 동시에 줄 수 있다.
encoder.ouputFormatting = [.sortedKeys, .prettyPrinted]

Codable JSON Decoding

웹 서버 등에 있는 JSON 데이터를 파싱해야할 경우 Decoding 작업을 한다.

//  서버에 아래와 같이 JSON data  있다.
{
    "name" : "Gaki",
    "age" : 25
}
  1. JSONDecoder 선언
  2. String 타입을 Data 타입으로 만든다.
  3. Data 타입을 JSONDecoder의 decode 메서드를 사용하여 인스턴스로 만든다.
let decoder = JSONDecoder()

var data = jsonString.data(using: .utf8)

if let data = data, let myPerson = try? decoder.decode(Person.self, from: data) {
  print(myPerson.name) // Gaki
  print(myPerson.age) // 25
}

위의 decode 함수에서 decoding 대상이 되는 타입을 매개변수로 넘겨 주었다. 아래와 같이 Decode 할 값의 타입을 요구하고 있다.

func decode<T>(_ type: T.Type, from: Self.Input) throws -> T where T : Decodable

이렇게 하게 되면 구조체 프로퍼티 하나하나에 매핑 시킬 필요 없이 타입이 자동으로 매치가 되어 해당 되는 프로퍼티에 저장이 된다.

CodingKey 를 사용

인코딩 및 디코딩을 위한 key를 사용하기 위한 프로토콜 이다.


{
    "name" : "Gaki",
    "age" : 25,
    "birth_day" : "2017-01-22T23:16:50+0000",
    "phone_number" : "123-4567-8910",
    "resident_registration_number" : "123456-1234567"
}

위와 같이 웹 서버에는 JSON 데이터가 4가지로 존재한다. phone_number 는 전화번호를 resident_registration_number 는 주민등록번호를 나타낸다. _ 와 같이 스네이크케이스가 있고 또 주민번호는 너무 길기 때문에 가독성이 떨어진다. 이를 조금더 가독성 있고 효율적으로 사용하기 위해 CodingKey 프로토콜을 이용하여 키를 부여한 다음 사용하는 것이 바람직하다.

struct Person: Codable {
  var name: String
  var age: Int
  var birthDay: Date
  var phoneNumber: String
  var rrn: String
	
	enum CodingKeys : String, CodingKey {
    case name, age
    case birthDay = "birth_date"
    case phoneNumber = "phone_number"
    case rrn = "resident_registration_number"
	}
}

위에서 확인 할 수 있듯이 JSON 에서 Key는 항상 String 이기에 Raw Value타입으로 String 타입을 선택하였고 CodingKeys 라는 열거형이 CodingKey라는 프로토콜을 채택한다. 이렇게하면 컴파일러가 알아서 프로퍼티에 명시한 JSON String Key 값을 매핑 시켜 값을 넣을 수 있도록 길을 잡아준다.

실전 Codable 사용법

특정 Key, Value가 없는 경우

// before
{
  "name": "Gaki",
  "age" : 25,
  "birth_day" : "2017-01-22T23:16:50+0000"
}

// after
{
  "name": "Gaki",
  "age" : 25
}

위의 상황처럼 birth_day 키 값이 데이터가 유실되어 받을 수 없는 상황에서 Codable 을 사용하게 되면 KeyNotFound 에러가 발생한다.

struct Person: Codable {
  var name: String
  var age: Int
  var birthDay: Date
  
  enum CodingKeys: String, CodingKey {
    case name, age
    case birthDay = "birth_day"
  }
  
  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CondingKeys.self)
    name = (try? values.decode(String.self, forKey: .name)) ?? ""
    age = (try? values.decode(String.self, forKey: .age)) ?? ""
    birthDay = (try? values.decode(String.self, forKey: .birthDay)) ?? ""
  }
}

JSON을 decoding 할때 init(from decoder: Decoder) 를 호출하며, 해당 부분에 위와 같이 수행 작업을 넣을 수 있다. 위 처럼 기본값 "" 를 넣어서 keyNotFound 에러를 대처할 수 있다.

사실 위의 경우는 구조체 내부의 모든 프로퍼티를 아래와 같이 옵셔널 타입으로 선언 한다면 에러가 발생하지 않고 nil 이 들거 갈 것이다.

struct Person: Codable {
  var name: String?
  var age: Int?
  var birthDay: Date?
}

값이 null 인 경우

JSON의 값은 null을 제공하기 때문에 Swift.DecodingError.valueNotFound 에러가 발생 할 것이다. 앞서 본 init(form decoder: Decoder) 에는 null이 걸러지지 않기 때문에 에러가 여전히 발생한다. 그렇기 때문에 null 이 될 거 같으면 애초에 옵셔널 타입으로 파싱할 key를 선언 해주어야 한다.

JSON이 같은 타입의 값을 가진 배열인 경우

JSON은 아래와 같이 같은 타입의 값을 가진 배열이 될 수 있다.

["apple", "banana", "orange"]

decoder의 singleValueContainer 를 통해 처리하고, 반드시 init(from decoder: Decoder) 를 구현해서 처리해야한다.

struct Fruit {
  var list: [String]
  
  init(from decoder: Decoder) throws {
    list = try decoder.singleValueContainer().decode([String].self)
  }
}

JSON이 지정되지 않은 타입인 경우

JSON의 타입이 지정 되지 않고 아래 처럼 주어져 있다. 이런 상황에서 우리는 하나하나 값의 타입을 지정해서 파싱을 해야한다.

["gaki", 25, 1515083815000.0, "iOS Developer", true]

아래는 위의 데이터를 파싱한다고 가정하고 decoding을 수행한 예제이다.

struct Person: Codable {
    var name: String
    var age: Int
    var birthDay: Float
    var isKorean: Bool
    var job: String
    
    init(from decoder: Decoder) throws {
        var unkeyedContainer = try decoder.unkeyedContainer()
        name = try unkeyedContainer.decode(String.self)
        age = try unkeyedContainer.decode(Int.self)
        birthDay = try unkeyedContainer.decode(Float.self)
        job = try unkeyedContainer.decode(String.self)
        isKorean = try unkeyedContainer.decode(Bool.self)
    }
}


let jsonString = """
["gaki", 25, 1515083815000.0, "iOS Developer", true]
"""


let decoder = JSONDecoder()

var data = jsonString.data(using: .utf8)

if let data = data, let myPerson = try? decoder.decode(Person.self, from: data) {
    print(myPerson)
}

// Person(name: "gaki", age: 25, birthDay: 1.5150838e+12, isKorean: true, job: "iOS Developer")

여기서 주의할 점은 완벽하게 key의 순서가 일치해야한다. init 함수 내부에서 차례로 타입을 찾아 매핑하기 때문에 확실하게 순서를 정해주어야한다.


Reference





© 2020. by Gaki

Powered by gaki