[Dart] Async Init

장성호·2022년 8월 25일
2

[Flutter]

목록 보기
1/2

개발하다보면 비동기 생성자를 간절히 쓰고 싶을 때가 있다. 시도해보면 생성자에는 비동기를 적용시키는 것이 불가능하다며 빨간 줄이 나타난다. 그래서 보통 다음처럼 쓰곤 했다.

class MyClass {
	var something;
    
	MyClass({required this.something});
    
    Future<MyClass> init(var data) async {
    	final result = await data task...
        
        var something = result.data...
        
        return MyClass({something});
    }
}

일단 코드가 돌아가면 안심하며 지내오다가, 이번 개발에서는 되도록이면 클린 코드&재사용성 위주로 해보고 싶었다. 그래서 flutter future constructor라는 키워드로 검색을 해보니, 생각보다 아주 핫한 주제였다. 찾아낸 것은 다음과 같다.

1. _create(), create()를 활용한 private, public 생성자 분리
2. Future를 활용
3. Reflection과 proxy를 활용
4. Future을 활용한 mixin

간추리면 총 4가지였다. 찾아보면서 생성자를 비동기로 호출하는 것은 옳지 않다, 객체는 빠르게 생성한 뒤에 비동기 함수를 호출하는 것이 옳다 등 다양한 의견도 살펴볼 수 있었다. 심지어 closed 된 issue를 왜 닫냐며, 다시 open 되었던 것도 볼 수 있었다. 일단 하나씩 살펴보려고 한다.

한 가지씩 만나보자

시작하기 전에 두 가지 링크를 먼저 참고하면 도움이 될 것이다. 예시 코드는 두 링크에서 가져올 것이다.
1. Calling an async method from component constructor in Dart
2. Allow async constructors (or at least factory constructors)

생성자 분리

import 'package:flutter/cupertino.dart';
import 'package:health/health.dart';

class HealthHelper {
  HealthFactory health = HealthFactory();

  late List<HealthDataType> types;
  late List<HealthDataAccess> permissions;

  HealthHelper._create({required this.types, required this.permissions}) {
    debugPrint("_create() (private constructor");
  }

  static Future<HealthHelper> create(
      {required List<HealthDataType> types,
      required List<HealthDataAccess> permissions}) async {
    debugPrint("create() (public factory)");

    HealthHelper healthHelper =
        HealthHelper._create(types: types, permissions: permissions);

	// this line causes static instance error
    await health.requestAuthorization(types, permissions: permissions);

    return healthHelper;
  }
}

// use
HealthHelper healthHelper = await HealthHelper.create();

일단 처음 마주쳤을 때 static 키워드를 사용하는 것은 내가 주로 쓰는 방식과는 맞지 않는 방식이라고 생각했다. 주로 instance member에 접근해서 비동기 처리를 진행하는 편인데, static은 instance memeber가 아니라 class member에 접근하는 것이기 때문이다.

class MyClass {
  static int a = 0;
  int b = 0;
}

void main() {
  MyClass first = MyClass();
  MyClass second = MyClass();
  
  MyClass.a = 1;
  
  first.b = 1;
  second.b = 2;
  
  print("static variable : ${MyClass.a}");
  print("first : ${first.b}");
  print("second : ${second.b}");
}

사용하고 있는 것만 봐도, static member는 instance를 만들어서 접근하는 것이 아니라 MyClass 자체로 접근하고 있다. instance로 접근하면 에러가 난다.

// The static setter 'a' can't be accessed through an instance. 
// Try using the class 'MyClass' to access the setter.
first.a = 1; 

// The static getter 'a' can't be accessed through an instance. 
// Try using the class 'MyClass' to access the getter.
print(first.a);

이렇게 에러 메세지만 봐도, instance를 통해 접근하지 말고 class를 통해 접근하라고 이야기한다. 그렇다면 현재 문제가 되는 코드는 다음과 같다. 그렇다면 HealthFactory를 static member로 등록하면 어떨까?

// 이 부분의 error; access non-static member in static method 해결을 위해서
await health.requestAuthorization(types, permissions: permissions);

class HealthHelper {
	// health member를 static member로 변경한다.
    // 변경할 이유는 없으므로 final로 선언한다.
    static final HealthFactory health = HealthFactory();
    ...
 }

HealthHelper instance는 모두 HealthFactory를 통해서 건강 데이터에 접근한다. HealthFactory에는 딱히 member는 없고, 건강 데이터 관련 method만 있다. 그리고 접근 권한이나 데이터 읽기, 쓰기 등은 앱 내부에서 진행되는 것이 아니고, 외부에서 진행되는 것이기 때문에 static으로 선언해서 공용으로 써도 괜찮다고 생각한다.

첫인상은 별로 내키지 않은 방식이라고 생각했는데, 고민하다보니 static으로 선언해서 공용으로 써도 괜찮다고 여겨졌다. 소소하지만 이런거 하나하나가 모여서 메모리를 아끼는 것 아닐까라는 생각이 든다.

Future 활용

class HealthHelper {
	// static final은 좋아보여서 재사용, 굳이 static final로 선언하지 않아도 됌.
  static final HealthFactory health = HealthFactory();

  late List<HealthDataType> types;
  late List<HealthDataAccess> permissions;
  late Future _doneFuture;

  HealthHelper({required this.types, required this.permissions}) {
    _doneFuture = _init();
  }

  Future _init() async {
    await health.requestAuthorization(types, permissions: permissions);
  }
  
  Future get initializationDone => _doneFuture
}

// use
HealthHelper healthHelper = HealthHelper();
await healthHelper.initializationDone;

굉장히 간소해졌다. 굳이 생성자를 나눌 필요가 없으며, static&factory에 얽매이지 않아도 된다. 철저하게 비동기로만 이루어져있다. 대신에 제대로 초기화되었는지 꼭 확인해야한다. 확인하지 않았다가는 대참사가 날 수 있다.

근데 남이 이러한 코드를 가져다가 쓴다고 생각해보면, initializationDone을 체크 안하고 넘어갈 수도 있지 않을까 싶다. 안에 있는 코드를 전혀 모른채로 가져다쓰면, 나 같아도 체크하지 않을 것 같다...!

StackOverflow 원문에서는 RAII 원칙을 준수하지 않는다고 의견을 남기신 분이 있었다. C++에서는 garbage collection이 없어서, 프로그래머 스스로 관리해야하는 탓에 생긴 원칙인 것으로 보인다. 객체를 초기화 할 때, 모든 member가 할당이 되어야하는 원칙으로 보인다. 과거 프로젝트 할 때 late를 남발하다가 객체를 생성했지만, 초기화되지 않은 변수가 있어서 호되게 당했던 기억이 떠오른다.

이 방식보다는 Factory를 이용하는 방법이 더 환영받는 것으로 보인다.

Reflection과 proxy를 활용

제일 이해 안 가는 방법이다. 일단 Reflection이라는 개념이 생소해서 그런 듯하다. 어플리케이션 개발보다는 스프링 같은 프레임워크 개발에서 주로 쓰인다고 한다. 프레임워크 입장에서는 개발자가 무슨 타입을 사용할지 모르니까, Reflection을 통해 타입을 추측하고 member와 method를 가져오는 것이라고 한다. 코드는 StackOverFlow에서 가져왔다.

// by Ticore
// https://github.com/dart-lang/sdk/issues/23115

import 'dart:async';
import 'dart:mirrors';


class AsyncFact implements Future {
  factory AsyncFact() {
    return new AsyncFact._internal(new Future.delayed(
        const Duration(seconds: 1), () => '[Expensive Instance]'));
  }

  AsyncFact._internal(o) : _mirror = reflect(o);

  final InstanceMirror _mirror;
  
  noSuchMethod(Invocation invocation) => _mirror.delegate(invocation);
}

main() async {
  print(await new AsyncFact());
}

언젠가 이해할 날이 올거라 믿는다... 😂

AsyncInitMixin

Future를 활용한 방식을 확장한 것이다. mixin으로 만들었기 때문에, 어디서나 상속받아서 사용할 수 있다. 코드는 원문에서 발췌했다.

// by dovecheng
// https://github.com/dart-lang/sdk/issues/23115

import 'dart:async';
import 'dart:math';

import 'package:meta/meta.dart';

mixin AsyncInitMixin<T extends Future> implements Future {
  bool _isReady;

  Future<T> _onReady;

  bool get isReady => _isReady ?? false;

  Future<T> get onReady => _onReady ??= _init();

  Future<T> _init() async {
    await init();
    _isReady = true;
    return this as T;
  }

  
  Stream<T> asStream() => onReady.asStream();

  
  Future<T> catchError(Function onError, {bool Function(Object error) test}) => onReady.catchError(onError, test: test);

  
  Future<R> then<R>(FutureOr<R> Function(T value) onValue, {Function onError}) =>
      onReady.then(onValue, onError: onError);

  
  Future<T> timeout(Duration timeLimit, {FutureOr Function() onTimeout}) =>
      onReady.timeout(timeLimit, onTimeout: onTimeout);

  
  Future<T> whenComplete(FutureOr<void> Function() action) => onReady.whenComplete(action);

  
  Future<void> init();
}

class MyClass with AsyncInitMixin {
  MyClass();

  
  Future<void> init() async {
  	// await code
  }
}

// use case 1
MyClass first = await MyClass();

// use case 2
MyClass second = MyClass();
await second.onReady;

결국 사용하는 방식이 Future를 활용한 방식과 비슷한 걸 볼 수 있다. 어디서나 쓰이기 용이하게 만든 것이랄까?

Factory vs Future

의견 대립은 Factory와 Future, 두 가지 무엇을 사용할 것인지에 대한 토론이었다. 주로 Factory를 지지하는 사람이 많았는데, 그 이유는 생성자의 이름 때문이었다. 사실 비동기 생성자를 써야하는 이유가 보통 네트워크 작업 때문이다.

// factory
MyClass myClass = await MyClass.fromNetworkWithData(data: data);

// future
MyClass first = await MyClass(data: data);

조금 factory에게 힘을 실어준(?) 예시이긴 하지만, 생성자 이름을 커스터마이징 할 수 있냐 없냐가 크게 갈리는 것 같다. 최대한 구체적으로 표기하자는 의견에 동감하기 때문에, 앞으로는 factory나 static 키워드를 활용해서 비동기 생성자를 만들어야겠다. 특히 Api나 Repository 같은 경우는 Singleton으로 쓰는 경우가 많으니까, 나한테는 이 방법이 더 와닿는 것 같다!

근데 웃긴 건 factory 생성자는 비동기가 안 된다. 당연한 것이지만 factory 생성자도 어쨌든 생성자니까 안 되는 것이 맞다. 돌고 돌아 static vs future이 되었지만, '제발 async factory를 달라!'라는 말이 많이 나와서 factory vs future이 된 것 같다.

Future을 활용해 mixin을 만든 것처럼 static도 가능하면 참 좋을텐데, 가능한 지 한 번 알아봐야겠다.

References

Calling an async method from component constructor in Dart
Allow async constructors (or at least factory constructors)
개체 수명 및 리소스 관리(RAII)
dart: proxy annotation usage
자바 리플렉션이란?

profile
일벌리기 좋아하는 사람

0개의 댓글