[앱개발] 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

[시리즈 2편] 실무로 배우는 메시지 큐 - RabbitMQ

[시리즈 2편] 실무로 배우는 메시지 큐 - RabbitMQ

들어가며 [시리즈1]에서는 프로세스 내부 메시지 큐를 다뤘습니다. 이번엔 네트워크 메시지 큐인 RabbitMQ를 다룹니다. RabbitMQ 공식 문서나 기술 블로그는 많지만, 실무에서 어떻게 사용하는지에 대한 글은 의외로 적습니다. "Producer가 뭐고 Consumer가 뭔지는 알겠는데, 그래서 실제로는 어떻게 쓰는데?" 이번 글에서는 우리 MES 시스템에서 RabbitMQ를 어떻게 활용하고 있는지 실제 코드와 함께 공유합니다. 우리

By Jeonggil
[시리즈 1편] 실무로 배우는 메시지 큐 - Windows Message Loop

[시리즈 1편] 실무로 배우는 메시지 큐 - Windows Message Loop

들어가며 이 글은 "실무로 배우는 메시지 큐" 시리즈의 첫 번째 글입니다. 실무에서 발견한 문제를 해결하는 과정에서, IME 입력 문제와 해결 과정을 공유합니다. 메시지 큐는 RabbitMQ, Kafka 같은 네트워크 레벨만 있는 게 아닙니다. 우리가 매일 쓰는 Windows 애플리케이션도 메시지 큐 기반으로 동작합니다. * 시리즈1 (이 글): 프로세스 내부의 메시지 큐 - Windows

By Jeonggil
[시리즈 2편] 그림으로 풀어낸 SaaS 알림 시스템

[시리즈 2편] 그림으로 풀어낸 SaaS 알림 시스템

이 글은 1편 - 그림으로 풀어낸 SaaS 알림 시스템의 후속편입니다. 들어가며 1편에서는 설비 연속 OFF 알림 기능의 핵심 로직과 어떤식으로 해결했는지 그림으로 알아봤습니다. 이번 글에서는 실무에서 마주한 진짜 고민들을 공유합니다: * 왜 3개의 새로운 테이블이 필요했나? * 어떻게 확장 가능한 구조를 만들었나? * SMS 14원짜리 알림이 왜 무서운가? * 운영 레벨로 나가기까지 무엇을 준비했나?

By Jeonggil
[시리즈 1편] 그림으로 풀어낸 SaaS 알림 시스템

[시리즈 1편] 그림으로 풀어낸 SaaS 알림 시스템

들어가며 제조업 IoT 플랫폼에서 N대 이상의 설비를 실시간으로 모니터링하고, 설비가 연속으로 꺼졌을 때 담당자에게 즉시 알림을 보내는 기능을 개발하게 되었습니다. 데이터는 실시간으로 쌓이지만, 설비이상을 체크하는 스케줄러 주기는 1분으로 설정하였습니다. 시스템 아키텍처 기존 인프라와 Push 기능은 이미 구축되어 있었습니다. 저는 중간에 들어가는 Alert Scheduler만 구현하면 되는 상황이었습니다. ┌──────────────────────────────────────────────────────────┐ │ 설비 IoT 센서 (실시간)

By Jeonggil