Dart의 built_value: 직렬화(Serialization)
Dart의 built_value: 직렬화(Serialization)
지난주에 불변 객체 모델을 위한 built_value를 소개했어요. built_value에서 객체 모델을 정의하는 방법을 봤는데, 그건 불변이고 다루기 쉬우며, 그런 걸 좋아하는 분이라면 꽤 재미있어요.
이 글에서는 built_value 패키지의 나머지를 다룰게요. 가장 큰 항목은, 제목에서 눈치채셨겠지만, 직렬화도 지원한다는 거예요.
이 글은 David Morgan님이 2016년 12월 7일에 작성한 글로, 읽는 데 약 4분 정도 걸려요.
본문
built_value 직렬화를 쓰는 모습은 이래요:
// Value type defined using built_value.
abstract class Login implements Built<Login, LoginBuilder> {
// Add serialization support by defining this static getter.
static Serializer<Login> get serializer => _$loginSerializer;
...
}
// Once per app, define a top level "Serializer" to gather together
// all the generated serializers.
Serializers serializers = _$serializers;
// Use it!
var login = new Login((b) => b
..username = 'johnsmith'
..password = '123456');
print(JSON.encode(serializers.serialize(login)));
-->
["Login", "username", "johnsmith", "password", "123456"]
"JSON.encode"가 보이죠? 직렬화기는 사실 문자열까지 직렬화하지 않아요. 대신 Dart의 내장 JSON 직렬화가 처리할 수 있는 **원시 타입(primitive)**으로 변환해줘요. 그래서 원하면 JSON이 아닌 다른 것을 쓸 수도 있어요.
직렬화를 보통 "그냥 알아서 잘 돌아가는 것"이라고 생각하겠지만, 여기엔 몇 가지 미묘한 트레이드오프가 있어요. built_value의 직렬화를 파고들어 볼게요.
다형성(Polymorphism)
built_value 직렬화에서 가장 중요한 측면은 다형성을 지원한다는 거예요. 구체적으로, 추상 타입의 필드를 가질 수 있고:
- 그 추상 타입의 아무 직렬화 가능한 구현이든 직렬화할 수 있어요.
- 역직렬화할 때 올바른 타입으로 만들 수 있을 만큼의 정보가 와이어(wire)에 기록돼요.
가장 단순한 예는 List<Object>를 직렬화하는 거예요:
serializers.serialize(new BuiltList<Object>([1, 'two', 3]));
-->
['list', ['int', 1, 'string', 'two', 'int', 3]]
역직렬화할 때 모호함을 없애야 할 때만 추가 정보가 와이어에 실려요. 그래서 "BuiltList
결론은, 객체 모델을 어떻게 정의하든 built_value가 직렬화해준다는 거예요. 더 자세히 보고 싶다면 맵 직렬화기 테스트에서 모든 가능성을 다뤄요.
여러 구현(Multiple Implementations)
모든 직렬화 메커니즘이 맞닥뜨리는 또 다른 문제는 직렬화 가능한 타입의 *우주(universe)*를 어떻게 정의할 것이냐는 거예요. 여기서 built_json은 한 "타입"의 여러 구현을 허용하는, 좀 색다른 일을 해요.
이게 가능한 이유는 타입이 와이어 위에서 클래스 이름만으로 정의되기 때문이에요. 예를 들어 "Login"이라는 서로 다른 클래스들을 구분하려고 시도하지 않아요. 발신자와 수신자 모두 "Login"이라는 클래스에 호환 가능한 직렬화기를 가지고 있다고 가정할 뿐이에요.
이건 유용한 유연성을 더해줘요. 예를 들어 서버와 클라이언트 양쪽에서 Dart를 쓴다면, 객체 모델의 각 클래스에 대해 선택권이 생겨요:
- 클라이언트와 서버에서 같은 클래스를 쓸 수 있어요.
- 또는 다른 클래스를 쓸 수 있어요. 구현은 같은 이름과 호환 가능한 필드를 가져야 해요.
예를 들어, 렌더링과 파싱을 담당하는 클라이언트용 "Login" 클래스와, 인증과 데이터베이스를 담당하는 별도의 서버용 "Login" 클래스를 가질 수 있어요. 물론 서버 전용 구현은 "dart:io" 같은 패키지를, 클라이언트 전용 구현은 "dart:html" 같은 패키지를 자유롭게 쓸 수 있죠.
여러 언어(Multiple Languages)
built_value 직렬화가 타입을 클래스 이름만으로 식별하기 때문에, 직렬화된 데이터는 어떤 객체 지향 언어에도 잘 매핑돼요. Java 지원도 AutoValue를 통해 계획되어 있어요.
여러 버전(Multiple Versions)
직렬화된 built_value 데이터는 아주 단순한 방식으로 **하위/상위 호환(backwards/forwards compatible)**이 돼요. 클래스 이름과 필드 이름에 의존하거든요. 클래스 이름 변경과 필수 필드 이름 변경은 breaking이에요.
nullable 필드는 더 유연해요. 직렬화할 때는 null이 아닐 때만 기록되고, 역직렬화할 때는 찾지 못하면 기본값인 null이 돼요. 그래서 nullable 필드는 추가하거나, 제거하거나, 이름을 바꿔도 breaking change가 아니에요.
인식되지 않는 필드는 그냥 무시돼요.
미러(Mirrors) 없음
마지막으로, 성능에 결정적인 점은 built_value가 어떤 형태로도 mirrors를 쓰지 않는다는 거예요. 모든 분석은 코드 생성 시점에 이루어져서, 최소한이고 성능 좋은 직렬화 코드만 남겨요.
이상이 built_value로 하는 직렬화였어요. 앉아서 객체 모델을 작성하면 바로 RPC나 장기 저장용으로 사용할 수 있는 직렬화가 된다는 뜻이에요.
EnumClass
마지막으로 built_value에는 기능이 하나 더 있어요. 바로 EnumClass예요. Dart의 enum은 클래스가 아니지만, 강력한 객체 모델에는 클래스처럼 행동하는 enum이 필요해요. 가장 명확한 패턴은 "static const" 필드를 가진 클래스를 만드는 건데, EnumClass가 이걸 조금 더 쉽게 만들어줘요. 제공하는 것은:
- "values"와 "valueOf"를 위한 생성 코드.
built_value직렬화기를 통한 직렬화.- Angular나 Angular2 사용자를 위한 보너스: 코드 생성이 선택적으로 enum을 템플릿에서 쓰는 데 도움을 주는 mixin을 만들어줘요.
이 모든 기능은 예제에서 확인할 수 있어요.
이번 주는 여기까지예요! built_value의 기초를 다뤘으니, 다음 주에는 chat 예제를 자세히 파고들 준비가 됐어요. 기대해주세요!