[앱개발] flutter에 함수형 예외처리 적용해보기.

[앱개발] flutter에 함수형 예외처리  적용해보기.

들어가는 말


쓰리뷰에서는 몇 차례 flutter를 이용한 앱개발 프로젝트를 진행해 왔습니다.

프로젝트로 개발한 앱의 주요 기능이 Spring Boot로 구동되는 백엔드 서버와 API통신을 하면서 이루어졌기 때문에 좀 더 개선된 형태의 API 호출 구조를 만들고자 노력하였습니다.

이러한 노력의 일환으로 flutter 프로젝트에 함수형 예외처리 방식(Fuctional exception handling)을 도입하였습니다.

이번 글에서는 flutter 프로젝트에 함수형 예외처리를 도입한 방식과 이로 인해 어떠한 이득이 있었는지 공유하고자 합니다.


예외처리(Exception Handling)이란?


앱개발 뿐만 아니라 다른 프로그래밍에서도 예외처리는 중요한 개념입니다.

애플리케이션은 다양한 이유로 실패할 수 있으며, 이러한 실패를 적절히 관리하지 않으면 사용자 경험이 저하되고 애플리케이션의 신뢰성이 떨어집니다.

예를 들어, 진행했던 프로젝트 중 하나는 앱이 내부망과 연결되어 통신하는 형태였는데, 이러한 경우 네트워크 연결 오류 예외처리를 제대로 해주지 않게 된다면 빈번한 경우로 프로그램이 충돌이 발생하거나 예기치 않게 종료될 수 있습니다.

또한 네트워크 에러가 발생했을 때 사용자에게 적절한 에러 메시지나 상태 업데이트를 제공하지 않으면, 사용자는 앱이 정상적으로 동작하고 있다고 오해할 수 있습니다.

전통적인 예외 처리 방식은 주로 try-catch 블록을 사용하여 예외가 발생 시 추가 로직을 실행하는 것입니다. 예외가 발생할 수 있는 코드를 try 블록 안에 배치하고, 해당 예외를 catch 블록에서 잡아서 처리하는 구조로 되어 있습니다. flutter에서 try-catch 블록을 사용하는 방식은 간단하게 아래 코드 예시에서 살펴 볼 수 있습니다. flutter는 Dart 라는 객체지향 언어로 작성합니다.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyButton(),
        ),
      ),
    );
  }
}

class MyButton extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: const Text('예외버튼'),
      onPressed: () => _simulateError(context),
    );
  }
  
  void _simulateError(BuildContext context) {
    try {
      throw Exception('예외발생을 통한 예외입니다.');
    } catch (e) {
      _showDialog(context, e.toString());
    }
  }
  
  void _showDialog(BuildContext context, String message) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("에러"),
          content: Text(message),
          actions: <Widget>[
            OutlinedButton(
              child: const Text("닫기"),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

위 코드 예시는 아래 DartPad 링크에서 실행해 볼 수 있습니다.

버튼을 클릭하면 try 블록 내에서 Exeption을 발생시키고 해당 Exceptioncatch 블록 내에서 처리하여 에러 메세지를 담은 다이얼로그를 사용자에게 보여줍니다.

DartPad
An online Dart editor with support for console, web, and Flutter apps.

함수형 예외처리(Fuctional exception handling)방식이란?

함수형 프로그래밍 패러다임은 검색해보면 프로그램의 동작을 순수 함수의 집합으로 표현하는 것을 중심으로 한다고 되어 있습니다. 개념적인 부분은 여기 저기 찾아봐도 명확하게 이해되지 않았습니다.

여러 프로젝트를 수행하면서 경험을 통해 이해하기로는 데이터의 불변성(immutability)을 기반으로 한다는 점입니다.

예를 들어 프로그램 내의 강아지를 표현하는 객체가 있을 때, 강아지 (흰둥이, 5살) 객체의 데이터를 강아지(흰둥이, 6살)과 같이 변경하고자 합니다.

이때 해당 객체에 접근하여 나이 값을 직접 변경하는 것이 아니라 함수 호출을 통해 새로운 객체(흰둥이, 6살)를 생성하여 반환해주는 방식을 떠올려보았습니다.

이러한 방식은 코드 내의 여기 저기서 해당 객체를 수정하는 것보다 예측이 가능하고 디버깅이 용이해집니다.

함수형 예외처리는 이러한 패러다임에 맞게 발생하는 에러를 예외로 처리는 대신 일반적인 값으로 취급합니다(Error as a value).

물론 프로젝트 진행시에는 코드 구조상 가장 하위 Layer(API 호출 담당)에서는 try-catch 를 사용하여 Exception 을 핸들링 하였고, try-catch 구문 내에서는 프로그램의 다른 상태 들을 변경할 수 있는 어떠한 로직도 실행하지 않고, 곧바로 exception에 해당하는 값(Error as a value)을 반환합니다.

여기서 값이란 아래처럼 간단하게 에러라는 것을 나타낼 수 있는 객체이면 됩니다.

class Failure {
  final String message;
  Failure(this.message);
}

이를 통해 가장 하위 Layer(API 호출)을 제외하고 핵심 비즈니스 로직이나 UI를 그려주는 코드를 작성할 때에는 try-catch를 더이상 사용하지 않아도 되고 에러를 함수의 반환값으로 활용할 수 있게 되므로 예외처리를 프로그램의 나머지 부분과 동일한 방식으로 다룰 수 있게 됩니다.

예를 들어 API를 호출 하는 과정에서 예외가 발생할 수 있다고 가정했을 때 기존 코드는 아래와 같습니다.

try {
  // 예외가 발생할 수 있는 코드
  var result = await apiCallOperation();
} catch (e) {
  // 예외 처리 로직
  handleError(e);
}

아래 코드에서는 더이상 try-catch 구문을 사용하지 않고 단순히 result 변수에 성공적으로 반환된 데이터 또는 Fail 객체가 담기게 되어 프로그램의 다른 부분에 영향을 미치는 추가 코드들이 작성될 여지가 없게 되고, 성공한 데이터가 반환 되었을 때 처럼 예외 처리 역시 반환된 값을 기반으로 동일한 방식의 처리가 가능합니다.

var result = await apiCallOperation();

// result는 apiCallOperation인 반환하는 데이터 객체일 수도 있고
// 에러를 나타내는 값. Fail 객체일 수도 있다.

결과적으로 2가지 형태 중 하나의 값을 갖게 되는 result를 처리하는 함수형 방식은 아래 예시에서 살펴보겠습니다.


함수형 예외처리 예시


위 예시에서 result는 API 호출 결과가 담긴 데이터 일 수도 있고 Fail 객체일 수도 있습니다. 프로젝트를 진행하면서 역시 Fail 객체를 만들었었는데 복잡한 부분을 빼고 단순화 시키면 아래와 같은 형태가 됩니다.

abstract class Failure {
  final String message;
  Failure(this.message);
}

// 서버 에러를 나타내는 클래스
class ServerFailure extends Failure {
  ServerFailure(String message) : super(message);
}

// 네트워크 에러를 나타내는 클래스
class NetworkFailure extends Failure {
  NetworkFailure(String message) : super(message);
}

다음으로 flutter 프로젝트에 dartz 패키지를 추가해줍니다. 이 패키지는 Dart 언어에서 함수형 프로그래밍 개념을 적용할 수 있게 해주는 라이브러리입니다. 이 라이브러리는 Either 라는 타입을 제공하여 함수의 결과를 성공 데이터 또는 Fail로 표현 할 수 있게 합니다.

먼저, 필요한 패키지를 프로젝트의 pubspec.yaml 파일에 추가합니다.

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.3
  dartz: ^0.10.0

다음으로 API 호출 결과를 담을 클래스를 정의합니다. 간단하게 id와 이름을 가진 User 클래스입니다.

class User {
  final int id;
  final String name;

  User({required this.id, required this.name});

  // JSON을 User 객체로 변환
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
    );
  }
}

다음으로 API를 호출하고 결과를 Either 타입으로 반환하는 함수를 구현합니다. JSONPlaceholder 의 fake API를 사용한 간단한 GET 요청 입니다.

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:dartz/dartz.dart;

// API 호출 함수
Future<Either<Failure, User>> fetchUser(int userId) async {
  try {
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users/$userId'));

    if (response.statusCode == 200) {
      // 성공적으로 사용자 데이터를 받아옴
      return Right(User.fromJson(json.decode(response.body)));
    } else {
      // 서버 에러
      return Left(ServerFailure('유저데이터 검색 실패'));
    }
  } catch (e) {
    // 네트워크 에러
    return Left(NetworkFailure('네트워크 에러'));
  }
}

위 코드에서 fetchUser 함수는 Future<Either<Failure, User>> 타입을 반환합니다. `Either` 타입은 두 가지 경우 중 하나를 나타냅니다. Left 는 에러를, right 는 성공적인 결과를 나타냅니다. 이를 통해 함수가 성공 또는 실패한 경우를 명확하게 처리할 수 있습니다.

이제 main 함수에서 API 호출 함수를 호출해보겠습니다.

void main() async {

  Either<Failure, User> result = await fetchUser(1);

  result.fold(
    (failure) {
      // 에러처리
      if (failure is ServerFailure) {
        print('Server Error: ${failure.message}');
      } else if (failure is NetworkFailure) {
        print('Network Error: ${failure.message}');
      }
    },
    (user) {
      print('User: ${user.name}'); // 성공 결과 처리
    }
  );
}

위 코드에서 fold 메서드는 Either 타입의 값을 처리할 때 사용됩니다. 첫 번째 인자는 Left (에러)를 처리하는 함수이며, 두 번째 인자는 Right (성공결과) 를 처리하는 함수입니다.

위 예제 코드에 flutter UI를 적용한 방식은 아래 DartPad 링크에서 테스트 해볼 수 있습니다.

DartPad
An online Dart editor with support for console, web, and Flutter apps.

함수형 예외처리를 통해 개선된 점


위의 예제에서 처럼 함수형 예외처리는 에러를 값으로 취급하고, 이를 통해 프로그램의 실행 흐름을 제어합니다. 즉, 에러 발생 시 예외를 던지는 대신, 에러 정보를 담고 있는 값(객체)를 반환하여 호출자가 이를 기반으로 적절한 조치를 취할 수 있습니다. 이러한 접근 방식으로 이해 예측 가능한 프로그램 흐름을 유지하고, 사이드 이펙트를 최소화 할 수 있었습니다.

프로젝트에 함수형 예외처리를 적용하면서 개선된 점을 돌아보면 크게 아래와 같습니다.

  1. 명시적인 에러처리
    ​위의 예제에서 처럼 함수형 예외 처리 방식에서는 Either 를 통해 명시적으로 성공 타입과 실패타입을 알 수 있습니다.
Future<Either<Failure, User>> fetchUser(int userId) async {
 // ...
}

Either<Failure, User> result = await fetchUser(1);

이처럼 에러가 발생할 수 있는 모든 지점이 명시적으로 표현되어 디버깅 및 유지보수 시 프로그램의 어느 부분에서 에러 발생 가능성이 있는지, 그리고 어떻게 대응해야 하는지를 더 명확하게 알 수 있었습니다.

  1. 향상된 가독성과 유지보수성
    에러 처리 로직이 흩어지는 것이 아니라, 함수의 반환값으로 명확하게 표현되므로 코드를 이해하고 유지보수하기 쉬워졌습니다. 특히 실제 프로젝트에서는 API 호출 Layer, 비즈니스 로직 처리 Layer, UI Layer 등 작게 잡아도 3개의 Layer 이상으로 코드 구조가 생기는데, 이러한 경우 에러를 값으로 표현하고 다룬 다는 점에서 패턴의 동일성을 유지 할 수 있었습니다.

Read more

ISACA 2024 학술대회 발표자료

ISACA 2024 학술대회 발표자료

ISACA_2024_공동_학술대회_발표자료ISACA_2024_공동_학술대회_발표자료.pdf125 MBdownload-circle[2024. 11. 20.] 연세대, 2024 바른ICT연구소 리서치 콘퍼런스 성료 | 연세대학교연세대학교 * 키워드 * 데이터 * 신뢰 * 윤리 * 보안 * 2024 노벨상 * 물리학상, 제프리 힌튼 (인공지능, 컴퓨터 과학자) * 화학상, 데미스 허사비스 (구글 딥마인드) * 라인 경영권 이슈 * 데이터 주권 확보에 대한 노력 (경영권 분쟁이 아님.

By Hyonsok
3회 Digital ESG Conference 세미나 자료, 누빅스, 2024/10/24

3회 Digital ESG Conference 세미나 자료, 누빅스, 2024/10/24

DEA 컨퍼런스 행사 프로그램 순서DEA 컨퍼런스 행사 프로그램 순서.pdf503 KBdownload-circle[공유용] DEA 컨퍼런스 오프닝 및 DEA 업데이트_강명구[공유용] DEA 컨퍼런스 오프닝 및 DEA 업데이트_강명구.pdf3 MBdownload-circle[공유용] 콜린스_케이에너지세이브 발표자료[공유용] 콜린스_케이에너지세이브 발표자료.pdf3 MBdownload-circle[공유용]Digital ESG Alliance Conference_김정연[공유용]Digital ESG Alliance Conference_

By Hyonsok

Datadog Live Seoul 2024 (발표자료)

3개월 차 DevOps팀의 Datadog 활용 - 이병호 박상욱 김경보 김태수, 뉴넥스 & 이성욱, Datadog3개월 차 DevOps팀의 Datadog 활용 - 이병호 박상욱 김경보 김태수, 뉴넥스 & 이성욱, Datadog.pdf2 MBdownload-circle12년차 스타트업의 아키텍처 리팩토링 돌아보기 - 이정민, 드라마앤컴퍼니12년차 스타트업의 아키텍처 리팩토링 돌아보기 - 이정민, 드라마앤컴퍼니.pdf3 MBdownload-circleAsset-HighlightsDatadogLiveSeoul2024Asset-HighlightsDatadogLiveSeoul2024.pdf3 MBdownload-cir

By Hyonsok