Codable

많은 프로그래밍 작업은 데이터를 어딘가로 전달하는 것과 관련된다. 네트워크를 통해서 전송하거나, 디스크와 같은 영구 저장소로 전달하여 기록할 수도 있다. 프로그램이 실행시간에 사용하는 데이터는 그 필요와 목적에 맞게 구조화되고 각 단위가 연결되어 입체적인 그래프를 형성하기도 하지만, 전달과정에서 만큼은 일렬로 늘어선 비트의 연속체가 되어야 한다. 따라서 필연적으로 어떤 데이터가 유용성을 가지려면 직렬화 될 수 있어야 하고 그 반대로 역직렬화도 가능해야 한다.

이러한 직렬화 / 역직렬화와 관련하여 Foundation은 오래전부터 NSCoding 이라는 기술을 보유하고 있었다. 이는 간단하게 가장 기본이 되는 몇 가지 타입이 인코딩이 가능하다면, 각 객체의 속성을 키-값 쌍으로 묶어서 인코딩할 수 있다는 가정하에 일반적으로 사용될 수 있는 임의의 타입의 데이터까지 간단하게 확장할 수 있는 기술이다. (물론, 현재도 쓰인다. 단 보안상의 문제로 NSSecureCoding으로 대체되었다.)

Swift의 표준 라이브러리는 이와 같은 원리를 확장하여 클래스 뿐만 아니라 모든 Swift의 표준 타입을 인코딩/디코딩할 수 있도록 준비하였고 이러한 특성을 Codable 이라는 프로토콜에 정의하였다. 기술적으로 CodableEncodableDecodable을 동시에 만족하는 타입으로 정의된다.

놀라운 점은 이 Codable 프로토콜은 클래스뿐만 아니라 Struct, Enum 타입의 객체에도 적용이 가능하다는 것과, 그 확장이 매우 간단하다는 것이다. 먼저 Swift의 원시 데이터 타입들은 모두 Codable이다. Int, Float, Double, String, Data 와 같은 기본 타입 및 이들 기본 타입을 원소로 갖는 Array, Dictionary, Set 등의 컬렉션 타입들이 모두 인코딩/디코딩이 가능하다.

또한 임의의 Struct나 클래스의 모든 프로퍼티가 Codable한 타입으로 구성된다면, 해당 타입이 Codable 하다고 선언해주는 것만으로도 별도의 구현 없이 커스텀 타입을 Codable하게 만들 수 있다. 예전 NSCoding 시절에 엄청나게 귀찮고 지루한 반복작업을 해야 했었던 것에 비하면 놀랄만큼 편하다.

다음은 간단한 좌표값을 표현하기 위한 자료형이다. 두 개의 좌표 값을 각각 Double 타입의 값으로 사용한다.

public struct Coordinate 
{
  let x: Double
  let y: Double
}

이 Coordinate 타입은 모든 프로퍼티가 인코딩 가능한 타입이기 때문에 그 자체로 인코딩이 가능하다. 다음과 같이 선언해주기만 하면 된다.

extension Coordinate: Codable { }

이 Coordinate 타입이 인코딩 가능하기 때문에, 역시 이 타입을 프로퍼티로 갖는 타입도 인코딩이 가능할 것이다.

public struct Rectangle: Codable 
{
  var origin: Coordinate
  var width: Double
  var height: Double
}

그러면 실제로 어떤 식으로 인코딩할 수 있을까? 기본적으로 파운데이션이 제공하는 인코더/디코더 중에서는 JSONEncoderJSONDecoder가 있다. 먼저 데이터를 하나 만들고 실제로 인코딩해보자.

let a = Coordinate(x: 0, y: 0)
let rect = Rectangle(origin: a, width: 10.0, height: 25.0)

let encoder = JSONEncoder()
encoder.outputFormat = .prettyPrinted
let data: Data? = try? encoder.encode(rect)

// 출력
if let data = data, let string = String(data: data, encoding:.utf8) {
  print(string)
}

데이터를 이용해서 원래의 객체로 복원하는 것은 디코더를 사용한다. 이 때, 디코더는 원래 타입에 대해서 알지 못하므로 타입 그 자체를 인자로 넘겨줄 필요가 있다. 디코딩에는 decode(_: from:) 을 사용한다. 이 메소드는 예외를 던지는 메소드이다

let decoder = JSONDecoder()
if let data = data, let q = try? decoder.decode(Rectangle.self, from: data)
{
  print(q.origin.x)
}

프로퍼티 리스트 데이터로 만들기

흔한 케이스는 아니지만 프로퍼티 리스트 데이터로 만드는 경우가 종종 있을 수 있다. 프로퍼티 리스트 데이터로 변환하기 위해서는 NSPropertyListSerialization 클래스를 사용하기도 했었지만, 지금은 PropertyListEncoder를 사용해서 변환할 수 있다. 이러한 인코더/디코더들은 모두 Encoder, Decoder 프로토콜에 그 동작이 정의되어 있기 때문에, 변환되는 결과의 포맷만 달라진다 뿐이지 사용 방법은 동일하다.

let enc = PropertyListEncoder()
enc.outputFormat = .xml
let data = try? enc.encode(p)

if let data = data, let s = String(data: data, encoding: .utf8)
{
  print(s) // print xml contents
}