Dart 불변 값으로 빠르게 움직이기

Dart 불변 값으로 빠르게 움직이기

Dart의 built_value는 Dart에 강력하고 편리한 불변 값(immutable values)을 제공해요. 값은 불변일지 몰라도, 패키지 자체는 가만히 있지 않았어요! 제가 마지막으로 built_value에 대해 글을 쓴 이후 6개월 동안 패키지에 열 가지 주요 개선이 있었어요.

또한 Dart 커뮤니티에서 built_value의 흥미로운 사용 사례 몇 가지도 발견했어요.

  • built_redux 패키지는 built_value를 사용하는 redux에서 영감을 받은 패키지예요. flutter, angular2, react-dart에 대한 바인딩과 예시가 함께 제공돼요.
  • built_highcharts 패키지는 built_value를 사용하는 highcharts 래퍼예요. 데모가 정말 괜찮아요.
  • 그리고 여기 Flutter와 함께 built_value를 사용하는 또 다른 방법이 있어요.

여하튼, 더 지체하지 않고 built_value의 모든 훌륭한 새 기능을 소개할게요.

출처: Moving Fast with Dart Immutable Values

본문

1. 제네릭 클래스(Generic classes)

정교한 객체 모델은 제네릭을 사용하므로, 당연히 built_value에도 제네릭이 필요해요. 이제 제네릭을 사용해서 타입을 선언할 수 있고, 생성된 빌더 클래스도 제네릭을 지원해요:

abstract class GenericValue<T>
    implements Built<GenericValue<T>, GenericValueBuilder<T>> {
  T get value;
  ...
}

var value = new GenericValue<String>((b) => b..value = 'string');

제네릭을 사용하는 값 타입은 평소와 같이 직렬화 가능해요.

2. 직렬화 플러그인(Serializer plugins)

새 SerializerPlugin API를 사용하면 모든 객체가 직렬화되거나 역직렬화되기 전후에 임의의 코드를 실행해서, 직렬화에 횡단 관심사(cross-cutting)를 가진 수정을 가할 수 있어요.

예를 들어, 와이어 호환이 안 되는 프로토콜의 두 버전 사이에서 매핑하는 데 사용할 수 있어요. 하지만 더 좋은 예시는 다음 개선에서 볼 수 있어요...

3. "표준 JSON" 직렬화 플러그인

기본적으로 built_value는 성능과 유연성을 위해 자체의 리스트 기반 JSON 형식으로 직렬화하고 역직렬화해요.

하지만 많은 사람들이 기존 JSON 데이터와 API와의 상호 운용이 필요해요. 그들은 어떻게 해야 할까요?

이제 StandardJsonPlugin을 설치하고 표준 맵 기반 JSON 형식으로 전환할 수 있어요.

final standardSerializers =
    (serializers.toBuilder()
        ..addPlugin(new StandardJsonPlugin())).build();

예시(끝부분)를 참조하세요.

4. 게터 메모이제이션(Getter memoization)

불변 클래스는 훌륭해요 — 하지만 파생 필드(derived fields)는 어떻게 해야 할까요? 파생 필드 계산이 비싸다면, 불변 클래스는 두 가지 나쁜 선택지밖에 주지 않아요. 처음부터 계산해서 어쩌면 불필요한 비용을 항상 지불하거나, 필요할 때마다 다시 계산해서 그 큰 비용을 여러 번 지불할 가능성이 있는 것요.

이때 @memoized 게터가 등장해요. built_value 게터에 이 어노테이션을 추가하면, 그것은 지연(lazily) 계산되지만 최대 한 번만 계산돼요. 그런 다음 인스턴스의 숨겨진 필드에 저장돼요.

1.1.2 이후로 built_value 자체가 @memoized 게터의 훌륭한 예시예요. built_value는 분석기(analyzer)의 Element를 감싸는 데 사용되고, 계산은 게터에서 지연 수행돼요:

abstract class ValueSourceClass
    implements Built<ValueSourceClass, ValueSourceClassBuilder> {
  ClassElement get element;

  @memoized
  BuiltList<String> get genericParameters =>
      new BuiltList<String>(element.typeParameters
          .map((element) => element.computeNode().toString()));

  @memoized
  BuiltList<ValueSourceField> get fields =>
      ValueSourceField.fromClassElements(element, builderElement);

  // 더 많은 @memoized 게터들이 있습니다.
}

5. 더 빠른 코드 생성

...그리고 메모이제이션된 게터로 전환한 결과, built_value는 이제 코드 생성이 상당히 빨라졌어요.

6. 더 단순한 팩토리(Simpler factories)

0.5.5 이전에는 모든 built_value 클래스가 표준 템플릿을 따르는 팩토리 메서드를 가져야 했어요. 이 요구 사항은 완화됐어요. 이 더 큰 자유를 활용할 수 있는 몇 가지 좋은 방법이 있어요.

한두 개의 필드를 가진 클래스라면, 생성자에서 정확히 그 필드들만 받으면 돼요:

abstract class Value implements Built<Value, ValueBuilder> {
  ClassElement get element;

  factory Value(ClassElement element) =>
      new _$Value._(element: element);
}

void main() {
   var element = getElement();
   var value = new Value(element);
}

또는 필드에 기본값을 설정하고 싶은 곳에서는, 직접 빌더 클래스를 작성할 필요 없이 이제 팩토리에서 할 수 있어요:

abstract class Value implements Built<Value, ValueBuilder> {
  int get x;
  int get y;
  bool get awesome;

  factory Value([updates(ValueBuilder b)]) =>
      new _$Value((b) => b
          ..awesome = True
          ..update(updates));
}

void main() {
  var value = new Value((b) => b
      ..x = 10
      ..y = 20);
}

마지막으로, 명명된 인자를 좋아한다면, 빌더 기반 생성자 대신 명명된 인자 기반 생성자를 노출할 수 있어요:

abstract class Value implements Built<Value, ValueBuilder> {
  int get x;
  int get y;

factory Value({int x, int y}) = _$Value._;

void main() {
  var value = new Value(x: 10, y: 20);
}

7. 더 예쁘고 사용자화 가능한 toString 출력

1.0.0 이전에는 built_value의 toString 출력이 상당히 평평했어요. 이제는 기본적으로 들여쓰기가 되어서 읽기 쉬워요:

CompoundValue {
  simpleValue=SimpleValue {
    anInt=1,
  },
}

하지만 그것도 최상위 전역 변수인 newBuiltValueToStringHelper를 통해 사용자화할 수 있어요. 옛 형식을 선호한다면:

newBuiltValueToStringHelper =
    (className) => new FlatBuiltValueToStringHelper(className);

그러면:

CompoundValue {simpleValue=SimpleValue {anInt=1}}

그리고 직접 작성하고 싶다면 — 물론 할 수 있어요. BuiltValueToStringHelper 클래스는 구현하기 정말 쉽습니다:

abstract class BuiltValueToStringHelper {
  void add(String field, Object value);
  String toString();
}

8. 직렬화 가능한 DateTime과 JsonObject

SDKDateTime 클래스는 이제 직렬화 가능해요 — 로컬 시간대가 아니라 UTC를 사용한다면 말이죠. (로컬 시간대로 직렬화하는 것은 재앙의 레시피예요!)

그리고 기존 JSON 데이터와 API와의 상호 운용을 더 돕기 위해, 이제 json 값, 리스트, 맵을 감싸는 JsonObject 클래스가 있어요. JsonObject 타입의 필드는 원시 JSON으로 직접 직렬화되고 역직렬화돼요.

9. 테스트용 커스텀 매처(Custom matcher for tests)

built_value에서 생성된 operator== 메서드는 테스트에 이미 아주 유용하지만, 한 가지 주지 않는 것이 있어요. 바로 쉽게 읽을 수 있는 오류 메시지예요. 대신 테스트에서 'equals' 검사가 실패하면 기대값과 실제값의 전체 toString을 받게 돼요.

이제는 더 이상 그렇지 않아요! 이제 새 pub 패키지인 built_value_test를 사용할 수 있어요. 그것을 테스트에서 사용하면 이제 이런 오류를 보게 돼요.

was <3> instead of <5> at location ['simpleValue']['anInt']

이것이 테스트에서 맵 비교가 실패할 때 받는 메시지와 많이 비슷해 보인다면, 그건 같은 것이기 때문이에요. 값을 맵으로 변환한 다음 비교하는 방식으로 구현됐거든요.

10. implicit-dynamic·implicit-cast 친화적인 코드; 더 나은 오류 메시지

built_value가 생성하는 코드는 두 개의 매우 엄격한 린트를 통과하도록 상당히 정리됐어요. 더 이상 어디에서도 dynamic를 암시적으로 사용하지 않고, 암시적 캐스트에도 의존하지 않아요.

이 두 가지 개선은 컴파일러가 더 나은 코드를 만들게 할 수도 있고(아닐 수도 있고) 있어요.

그리고 serializer 필드를 잘못된 타입으로 선언할 때 더 나은 오류 메시지도 추가됐어요.

그게 전부예요! 읽어 주셔서 감사해요. built_value에 대한 아이디어나 기능 요청이 있다면 이슈 트래커로 오세요.