[Flutter] Monad식 예외처리

고랭지참치·2025년 6월 25일
0

Flutter

목록 보기
23/24

전통적 예외처리

Dart는 자바를 베이스로 만들어졌기 때문에 전통적인 tryCatch구문을 통해 예외처리가 가능하다.

void someTask() {
  try {
    doTask();
  } catch (e) {
    logger.e('Error occurred: $e');
    evaluateError(e);
  }
}

try로 실행시키고, exception이 발생하면 catch에서 받아서 처리한다.
try catch는 짧은 코드에서는 간단하고 직관적이지만,
조금만 코드가 길어지거나 함수가 복잡해져도 가독성이 떨어지게 된다.

Flutter팀의 권장방식은 Monad

Flutter Architecture공식문서가 올해초에 발행되면서 샘플앱에
Monad식 예외처리 방식인 Result클래스가 소개 됐다.

https://github.com/flutter/samples/blob/main/compass_app/app/lib/utils/result.dart

sealed class Result<T> {
  const Result();

  /// Creates a successful [Result], completed with the specified [value].
  const factory Result.ok(T value) = Ok._;

  /// Creates an error [Result], completed with the specified [error].
  const factory Result.error(Exception error) = Error._;
}

주요 핵심 코드는 위와 같다.
매소드의 결과를 ok상태와 error상태로 구분하여,

  final result = await _authRepository.login(
    email: email,
    password: password,
  );
  if (result is Error<void>) {
    _log.warning('Login failed! ${result.error}');
  }

result의 타입이 Error인 경우 분기처리를 해주는 식이다.

해당 방식을 프로젝트에 적용했을때, 우리팀이 얻을 수 있었던 장점은다음과 같았다.

  • 복잡했던 성공/예외 케이스에 대한 보일러 플레이트 코드가 줄어들었다.
  • 합성 함수를 통해(map, when) 코드 의도가 명확해질 수 있었다.

코드 한스푼 더하기

  void when({
    required void Function(T value) ok,
    required void Function(CustomException error) error,
  }) {
    if (this is Ok<T>) {
      ok((this as Ok<T>).value);
      return;
    } else if (this is Error<T>) {
      error((this as Error<T>).error);
      return;
    }
  }

  R map<R>({
    required R Function(T value) ok,
    required R Function(CustomException error) error,
  }) {
    if (this is Ok<T>) {
      return ok((this as Ok<T>).value);
    } else if (this is Error<T>) {
      return error((this as Error<T>).error);
    }
    throw Exception('Unknown result type');
  }

나는 map과 when 메서드를 Flutter팀이 작성한 Result클래스 안에 추가해줘서 사용했다.

final Result<PatientEntity> patientInfoResult = 
await viewModel.getPatientInfo(patientId: patientId);

patientInfoResult.when(
  ok: (PatientEntity data) async {
    patientInfoState.value = AsyncValue.data(data);
  },
  error: (error) {
    patientInfoState.value =
        AsyncValue.error(error, StackTrace.current);
  },
);

겪었던 이슈

우선 여러 Result를 묶어서 작업해야할때, 함수체이닝이 깊어지는 이슈가 있었다. 이런 경우 Result단위의 여러 함수로 분리하는 작업이 추가적으로 필요했다.

또, 객체를 리턴시키는 함수가 아닌 경우 try-catch가 여전히 필요했고, try-catch를 사용해야하는 코드와 아닌 것을 구분하는 팀 컨벤션을 설정할 필요가 있었다.

참고한 글
https://overcurried.com/3%EB%B6%84%20%EB%AA%A8%EB%82%98%EB%93%9C/

profile
소프트웨어 엔지니어 / Flutter 개발자

0개의 댓글