[Flutter] Map에 확장함수(Extension method)를 만들어보자

2021. 8. 19. 08:58Programming/Flutter

반응형

Map의 확장함수 tryGet를 만들어보자

이미 이전에 Kotlin을 사용하면서 Kotlin에서 JSONObject .get*의 확장 함수를 만들어봤다.라는 글을 작성한 적이 있다. JSONObject를 참조할 때마다 매번 getInt(), getString()처럼 타입에 맞는 메소드를 호출해야했는데, 주어진 데이터에 따라 타입캐스팅 예외가 발생할 위험성이 있기 때문에 만든 확장함수였다.

이번에는 Dart의 확장함수에 대해 정리해보고, Map의 인자에 접근하기 위한 확장함수를 만들어보도록 하자.

확장 함수(Extension methods)

Dart 2.7부터 지원하기 시작한 확장 메소드(Extension methods)를 사용하면 제공되는 라이브러리에 기능을 추가할 수 있다. 자세한 내용은 Extension method 페이지를 참조하도록 하자.

왜 Map에다 확장 함수를 만들어?

Map은 정말 이래저래 많이 쓰이는 컬렉션이다. 아무래도 키-값 구조다보니 JSON으로 만들어진 데이터에 접근할때도 사용되고, 이래저래 많이 사용된다. 하지만 JSON을 파싱하면 값의 타입이 dynamic으로 설정되는 경우가 대부분인데, 타입을 고정하기 위해 String을 사용하면 Int형 데이터를 위해서는 다시 형변환을 해줘야한다. 으으, 아무리 생각해도 귀찮다. 차라리 실패했을 시에는 기본값을 넣어주는 함수가 있으면 좋을텐데. 그래서 만든김에, 확장 함수에 대해 설명하는 척 작성한 코드를 기록해놓기로 했다.

확장 함수 tryGet을 만들어보자!

extension mapParser on Map {
  T tryGet<T>(String key, T defaultValue) {
    if (this[key] == null) return defaultValue;

    T result = this[key] as T;

    return result ?? defaultValue;
  }
}

간단하게 확장 함수 tryGet을 작성해봤다. nullable을 적용하지 않은 상태라면 defaultValue는 Optional Positional Parameter로 지정해줘도 되지만, nullable이 적용된 상태라면 필수값으로 들어가야한다. T를 nullable로 설정하면 Optional Positional Parameter로 설정하는 것도 가능하지만, 그렇게되면 null에 대한 처리를 tryGet 외부에서 해줘야하므로 패스.

tryGetKeydefaultValue를 인자로 받아서, Map[key]값이 존재할 경우(null이 아닌 경우)에는 Map[key]를 반환한다. 그렇지 않은 경우 defaultValue를 반환한다. Key는 보통 String으로 사용하기 때문에 타입을 고정해놨다. 아마 KeyString이 아닌 경우까지 고려한다면, 다음과 같이 수정하면 될 것이다.

T fetch<K, T>(K key, T defaultValue)

여기서 K, T가 지정되지 않을 경우 K는 인자 key에 의해 추론되며, T는 반환값과 인자 defaultValue에 의해 추론된다.

고작 이거 하려고 확장함수같은걸 써? 뭐 좀 더 할 수 있는거 없어?

단순히 파싱을 한다면 Map[key] ?? defaultValue 형태로 코드를 작성하는게 훨씬 더 간단하다. 하지만 앞서 말했듯 타입을 지정하지 않으면 값의 타입은 반환값에 의해 추론되고, 데이터에 따라서 타입 캐스팅 에러가 발생할 수 있다. 예를 들어 서버-클라이언트간에 int형 데이터로 전달하기로 한 데이터가, String 타입으로 파싱되는 경우에 그런 상황이 발생할 수 있다. '아니, 그런 일이 있을 수 있다고?'싶을 수 있다. 누구나 당해보기 전까지는 그런 생각이 들 수 있다. 아무튼 그런 경우에도 파싱이 가능하도록 확장함수를 수정해보자.

extension mapParser on Map {
  T tryGet<K, T>(K key, T defaultValue) {
    if (this[key] == null) return defaultValue;

    T result;

    if (T == int && this[key] is String) {
      result = int.tryParse(this[key]) as T;
    } else if (T == double && this[key] is String) {
      result = double.tryParse(this[key]) as T;
    } else if (T == bool && this[key] is String) {
      result = bool.fromEnvironment(
        this[key],
        defaultValue: defaultValue as bool,
      ) as T;
    } else {
      result = this[key] as T;
    }

    return result ?? defaultValue;
  }
}

Tint, double, bool로 추론되고 this[key]String일 경우 추론된 값으로 파싱하는 코드를 추가했다. 이걸로 this[key]String이지만 반환값은 다른 값일 경우, tryGet은 파싱을 시도할 것이다. tryParse 메서드는 파싱에 실패한 경우에는 null값을 반환하므로, 파싱에 실패하면 defaultValue를 반환할 것이다. 만약 defaultValue를 null로 설정하고싶다면, T를 nullable로 지정하거나 혹은 반환값이 할당되는 값의 타입을 nullable로 설정하면 된다.

위에서 작성한 확장함수의 실행 예시는 아래를 참고하자.

void main() {
  Map<String, dynamic> map = {
    "foo": "bar",
    "foo2": true,
    "foo3": 1,
    "foo4": "1",
  };

  String nonNullableFoo = map.tryGet("foo", "hello");
  print("nonNullableFoo: ${nonNullableFoo}"); // nonNullableFoo: bar
  String? nullableFoo = map.tryGet("foo5", null);
  print("nullableFoo: ${nullableFoo}"); // nullableFoo: null
  bool foo2 = map.tryGet("foo2", false);
  print("foo2: ${foo2}"); // foo2: true
  int foo3 = map.tryGet("foo3", -1);
  print("foo3: ${foo3}"); // foo3: 1
  int foo4 = map.tryGet("foo4", -1);
  print("foo4: ${foo4}"); // foo4: 1
  String foo5 = map.tryGet("foo3", "-1"); // 에러가 발생한다.
}

마지막에 foo5String으로 정의할 경우 int값을 String으로 할당하려고 했기 때문에 에러가 발생한다. 이 경우는 TString이고 this[key]int인 경우에 대해서 처리를 해주면 되겠지만...

여기까지 왔다면 이미 확장함수에 대해서 이해했을테니, 나머지는 직접 해 보는 것으로 하고 마무리짓도록 하자. ' ㅈ')/

반응형