스위프트 JSON — Encoder와 Encodable

스위프트4에는 좀 더 네이티브한 방식으로 인코드/디코드될 수 있도록 개선 되었으며 뿐만 아니라 텍스트 기반에서 선호되는 JSON 인코드/디코드 기능이 내장되었습니다.

여기서는 인코딩/디코딩에 대한 소스 코드를 한 번에 살펴보기 보다는 일단 간단히 하나의 Int 인스턴스가 어떻게 JSONEncoder를 통해서 JSON 데이터로 변환되는지 살펴 보기로 하겠습니다. 그리고 나서 다른 원시 자료형과 배열, 딕셔너리 등의 인코딩 방식에 대해 이해하는 것이 좋을 것입니다.

아카이브

지난 오랜 기간동안 코코아에서 데이터를 저장하고 가져올 때 NSCoding을 사용해 왔습니다. 놀랍게도 지난 15년간 사용해온 NSArchiver를 애플이 드디어 사용 중지 예고deprecate했습니다.

인코딩

스위프트 표준 라이브러리를 살펴보면 encoder와 encodable과 같은 것들을 찾아볼 수 있습니다.

  • Encodable 어떤 데이터 타입이 스스로가 다른 형태의 표현방식으로 인코딩 될 수 있도록 따라야하는 프로토콜입니다.
  • EncoderEncodable 객체를 JSON이나 XML와 같은 다른 형식으로 변경하는 작업을 수행하는 인코더의 프로토콜입니다.

Encodable은 스위프트 프로토콜로 NSCoding과 같은 역할을 합니다. 하지만 다른 점은 스위프트 구조체struct와 열거형enum에도 적용할 수 있다는 점입니다. 동일하게 Encoder는 추상 클래스인 NSCoding에 해당하는 것으로 스위프트 프로토콜입니다.

간단한 단일 정수

스칼라 값에 JSONEncoder를 사용해서 인코딩할 수 없습니다. 대신 최상위 배열이나 딕셔너리를 사용해야합니다. 예를 단순히 하기 위해서 다음과 같이 하나의 정수를 담고 있는 배열을 인코딩해 보겠습니다.

let encoder = JSONEncoder()
let jsonData = try! encoder.encode([42])

위의 코드에서 가장 먼저 JSONEncoder의 인스턴스를 생성한 후 encode()를 호출했습니다. 여기서 어떤 일이 벌어지는지 살펴보면

// JSONEncoder.swift
open func encode<T : Encodable>(_ value: T) throws -> Data {
let encoder = _JSONEncoder(options: self.options)

encode() 메서드는 Encodable 값을 받아서 JSON 형식의 Data를 반환합니다.

실제의 인코딩 작업은 private 클래스인 _JSONEncoder가 수행합니다. JSONEncoder는 편리하게 사용할 수 있는 public 인터페이스 역할을 하고 fileprivate인 _JSONEncoder 클래스가 실제의 Encoder 프로토콜을 구현하는 접근방식이며 이런 형태를 다른 메서드에서도 그대로 유지합니다.

// 위의 코드에 계속해서
try value.encode(to: encoder)

원래의 호출 코드에서는 인코더encoder에게 값value을 넘겨서 호출하였으나 여기서 보듯이 인코더는 값value에게 자신을 스스로 인코딩하도록 요청합니다.

Encodable

이제 앞에서 언급한 프로토콜이 어떤 역할을 하는지 살펴보겠습니다.

먼저 Encodable입니다. 이 프로토콜은 정수와 배열과 같은 인코딩되어야 하는 값들이 따라야합니다.

public protocol Encodable {
func encode(to encoder: Encoder) throws
}

배열 [42] 대신에 이해하기 쉽도록 정수 값 42에 대해서만 고려해 보겠습니다. IntEncodable을 따르며 encode(to:)는 다음과 같습니다.

extension Int : Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self)
}
}

이 코드에서 보듯이 인코더에게 원하는 컨테이너를 요청하고 컨테이너에게 정수 값인 self를 인코드하도록 요청했습니다.

Encoder

다음으로 살펴볼 프로토콜은 Encoder입니다. _JSONEncoder와 같이 값들을 특정 포맷으로 바꾸는 작업을 하는 클래스가 구현해야 하는 프로토콜입니다.

public protocol Encoder {
// [...]
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key>
func unkeyedContainer() -> UnkeyedEncodingContainer
func singleValueContainer() -> SingleValueEncodingContainer
}

여기서 핵심사항은 인코더가 다음과 같은 세 가지 종류의 컨테이너를 만들 수 있어야 한다는 것입니다.

  1. 키가 있는 컨테이너 (딕셔너리)
  2. 키가 없는 컨테이너 (배열)
  3. 단일 값 컨테이너 (스칼라 값)

Int를 인코딩하는 코드로 돌아가 보면

// extension Int : Codable
var container = encoder.singleValueContainer()
try container.encode(self)

먼저 JSON 인코더로 부터 단일 값 컨테이너를 가져오는데 인코더의 코드는 다음과 같습니다.

// _JSONEncoder
func singleValueContainer() -> SingleValueEncodingContainer {
return self
}

일단은 간단합니다. _JSONEncoder 자체가 SingleValueEncodingContainer타입이므로 자신을 반환합니다. 이 프로토콜은 다음과 같습니다.

public protocol SingleValueEncodingContainer {
// [...]
mutating func encode(_ value: Int) throws
}

Bool, String과 같은 모든 종류의 단순한 타입들을 위해 추가적인 encode 메서드들이 존재합니다. Int에 대한 메서드만 살펴보면 다음과 같습니다.

// extension _JSONEncoder : SingleValueEncodingContainer
func encode(_ value: Int) throws {
assertCanEncodeNewValue()
self.storage.push(container: box(value))
}

여기서 알 수 있는 것처럼 어떤 저장소storage가 있어서 여기에 박싱된 값boxed value을 밀어 넣었습니다.

그렇다면 저장소storage는 뭘까요? 그리고 박스box는 뭘까요?

Storage

저장소를 이해하려면 끝까지 살펴봐야하는데 아직 우리는 그 아래로 내려가는 중간에 있습니다. 여기서 가장 처음에 있었던 호출 과정을 다시 떠올려 보면 JSONEncoderencode() 메서드를 호출하면서 정수 값 하나가 들어 있는 배열을 전달하였습니다. 그리고 이 메서드의 마지막 라인은 다음과 같습니다.

// JSONEncoder.swift
return try JSONSerialization.data(withJSONObject: topLevel, options: writingOptions)

즉, 이 메서드의 최종 목적은 최상위 컨테이너(배열 또는 딕셔너리)를 JSONSerialization에 전달하는 것입니다.

저장소는 또 다른 filprivate 구조체로 컨테이너 스택을 저장합니다. 여기서 부터 Objective-C가 보이기 시작합니다.

fileprivate struct _JSONEncodingStorage {
/// 컨테이너 스택입니다.
/// 엘리먼트는 JSON 타입 중의 한 가지이어합니다.
/// 즉, (NSNull, NSNumber, NSString, NSArray, NSDictionary).
private(set) var containers: [NSObject] = []
}

박스box에 대해서 설명하자면 배열 [42]는 NSMutableArray로 변환되고 box()가 호출되고 내부의 정수는 NSNumber로 바뀌어서 배열에 추가됩니다.

// extension _JSONEncoder
fileprivate func box(_ value: Int) -> NSObject {
return NSNumber(value: value)
}

이제부터 더 깊이 내려가면 모두 Objective-C입니다.

스위프트 값valueJSONEncoder 계열에 의해서 그 값에 해당되는 Foundation 객체로 변환되며 이 객체들은 JSONSerialization에 의해서 JSON화 되는 것입니다.

마무리

정리하자면 하나의 정수를 담고 있는 배열 [42]JSONEncoder로 인코딩했을 때 거치는 단계는 다음과 같습니다.

  1. 배열은 _JSONEncoder로 자신을 인코딩합니다.
  2. 배열은 키가 없는 컨테이너 (NSMutableArray 저장소)를 구성합니다.
  3. 내부의 모든 엘리먼트를 순차적으로 인코드합니다.
  4. (1) 정수는 _JSONEncoder로 자신을 인코딩합니다.
  5. (2) 정수는 단일 값 컨테이너 single-value container를 구성합니다.
  6. (3) 정수는 컨테이너에게 자신을 인코딩하도록 요청합니다.
  7. 컨테이너는 정수를 NSNumber로 박싱box하여 배열에 추가합니다.
  8. JSONSerialization을 사용하여 최상위 NSMutableArray를 인코딩합니다.
  9. 완료

관련된 코드는 표준 라이브러리 소스의 JSONEncoder.swiftCodable.swift를 참고하면 됩니다.

반대 방향인 디코딩의 경우는 다음에 기회가 되면 설명해보도록 하겠습니다. 아니면 직접 코드를 살펴보는 것도 좋을 것입니다.