소소한 일상과 잡다한 정보

IT/Dart

Dart_9일차 : 예외 처리 (Exception Handling) & Future Error Handling

pandada 2025. 3. 7. 17:12
반응형

 

 이번에는 Dart의 예외 처리(Exception Handling), 비동기 코드(Future)에서 에러를 다루는 방법을 진행해보자


 

1. 예외 처리 기본 ( try-catch-finally )

 Dart에서는 프로그램 실행 중 예상치 못한 오류(Exception)가 발생할 수 있다.
 이때 try-catch-finally를 사용하면 에러를 잡아서 정상적으로 프로그램을 실행할 수 있음!

 ✔️ 기본 예외 처리

void main() {
  try {
    int result = 10 ~/ 0; // 0으로 나누기 → 예외 발생
    print("결과: $result");
  } catch (e) {
    print("⚠️ 예외 발생: $e");
  } finally {
    print("✅ 예외 여부와 상관없이 실행되는 블록");
  }
}
  • try 블록에서 10 ~/ 0 ( 0으로 나누기 ) 때문에 예외 발생
  • catch (e)에서 예외를 잡아서 프로그램이 강제 종료되지 않도록 처리
  • finally 블록은 예외 발생 여부와 관계없이 항상 실행됨

 

 ✔️  예외 메시지 & 스택 트레이스 확인 (on & stackTrace)

void main() {
  try {
    List<int> numbers = [1, 2, 3];
    print(numbers[5]); // 인덱스 범위 초과 → 예외 발생
  } on RangeError catch (e, stackTrace) {
    print("⚠️ RangeError 발생: $e");
    print("🔍 스택 트레이스:\n$stackTrace");
  }
}
  • on RangeError를 사용하면 특정 예외만 잡을 수 있음
  • stackTrace를 활용하면 예외가 어디서 발생했는지 추적 가능

 처음 접하는 사람들이 있을 수도 있기 때문에 sttackTrace가 뭔지도 간단히 정의를 하고 넘어가려고 한다.

 

📌 stackTrace란?

  • stackTrace는 예외(Exception)가 발생한 코드의 실행 경로(Call Stack)를 추적하는 정보야.
  • 즉, 프로그램이 어디에서 예외가 발생했는지를 디버깅할 때 사용하는 데이터야!

 ✔️  stackTrace 출력 예제

void main() {
  try {
    List<int> numbers = [1, 2, 3];
    print(numbers[5]); // 인덱스 범위 초과 → 예외 발생
  } catch (e, stackTrace) {
    print("⚠️ 예외 발생: $e");
    print("🔍 스택 트레이스:\n$stackTrace");
  }
}
  • catch (e, stackTrace)를 사용하면 예외가 발생한 코드의 흐름을 확인할 수 있음.
  • 디버깅할 때 어디에서 문제가 발생했는지 찾는 데 유용함.
  • 예외가 main.dart:5(5번째 줄)에서 발생했다는 걸 알려줌.
  • 추가로 Dart 내부에서 어떻게 호출되었는지도 표시됨.

 

 ✔️ stackTrace를 활용하는 이유

  • 예외 발생 위치 추적 : 예외가 발생한 줄 번호, 파일명을 알려줌.
  • 콜 스택(Call Stack) 분석 : 함수가 호출된 흐름을 보여주므로, 어떤 함수에서 에러가 발생했는지 쉽게 파악 가능.
  • 디버깅에 도움 : 복잡한 프로그램에서 어디에서 문제가 발생했는지 확인 가능.

 

 ✔️ stackTrace를 활용한 예외 로깅

import 'dart:io';

void main() {
  try {
    throw Exception("테스트 예외 발생!");
  } catch (e, stackTrace) {
    File('error_log.txt').writeAsStringSync(
      "예외 발생: $e\n스택 트레이스:\n$stackTrace\n",
      mode: FileMode.append,
    );
    print("⚠️ 예외가 발생했으며, 로그 파일(error_log.txt)에 저장되었습니다.");
  }
}
  • 이렇게 하면 예외가 발생할 때마다 error_log.txt 파일에 기록됨.
  • 운영 환경에서 예외를 기록하고 분석하는 데 유용함.


📌 결론

  • stackTrace는 예외가 발생한 위치와 호출 흐름(Call Stack)을 추적하는 데 사용됨.
  • 예외가 발생했을 때 어떤 코드에서 문제가 생겼는지 정확히 알 수 있음.
  • 디버깅과 로그 기록을 할 때 매우 유용함.

2.  사용자 정의 예외 만들기

 Dart에서는 내장된 예외(Exception) 외에도 **사용자 정의 예외(Custom Exception)**를 만들 수 있다.
 이걸 활용하면 더 의미 있는 예외 메시지를 제공할 수 있음!

 ✔️ 사용자 정의 예외 클래스 만들기

class CustomException implements Exception {
  final String message;
  CustomException(this.message);

  @override
  String toString() => "❌ CustomException: $message";
}

void validateAge(int age) {
  if (age < 0) {
    throw CustomException("나이는 음수가 될 수 없습니다.");
  } else {
    print("✅ 입력된 나이: $age");
  }
}

void main() {
  try {
    validateAge(-5);
  } catch (e) {
    print(e);
  }
}
  • CustomException을 만들어 throw를 통해 직접 예외 발생
  • 예외 메시지를 커스텀할 수 있어 더 명확한 에러 처리가 가능

3. 비동기 예외 처리 ( Future & async-await )

Dart의 Future( 비동기 작업 )에서 발생하는 예외는 try-catch를 활용해서 처리할 수 있어!

 ✔️ Future에서 예외 발생 시 처리 ( try-catch

Future<void> fetchData() async {
  try {
    throw Exception("데이터 로드 실패!");
  } catch (e) {
    print("⚠️ Future 예외 발생: $e");
  }
}

void main() {
  fetchData();
}
  • 비동기 코드에서도 try-catch로 예외를 처리할 수 있음

 

 ✔️  .catchError() 활용 (Future 체이닝)

Future<void> fetchData() {
  return Future.delayed(Duration(seconds: 2), () {
    throw Exception("서버 연결 실패!");
  }).catchError((e) {
    print("⚠️ Future 예외 발생: $e");
  });
}

void main() {
  fetchData();
}
  • catchError()를 활용하면 then()과 함께 체이닝 가능
  • 비동기 예외를 잡아서 안전하게 처리 가능

 

 ✔️  Future.wait()에서 여러 Future의 예외 처리

Future<String> fetchUser() async {
  await Future.delayed(Duration(seconds: 1));
  throw Exception("사용자 데이터 가져오기 실패!");
}

Future<String> fetchPosts() async {
  await Future.delayed(Duration(seconds: 2));
  return "📄 게시물 데이터";
}

void main() async {
  try {
    var results = await Future.wait([fetchUser(), fetchPosts()]);
    print(results);
  } catch (e) {
    print("⚠️ Future.wait() 예외 발생: $e");
  }
}
  • Future.wait()에서 여러 개의 Future를 동시에 실행
  • 하나라도 예외가 발생하면 catch에서 처리

 그럼 이제 추가적으로 Future에서 try-catch와 catchError()의 차이에 대해서 좀 더 확인해보자.

 

📌 Future에서 try-catch와 catchError()의 차이

 Dart에서 비동기 예외를 처리하는 방법에는 try-catch와 catchError() 두 가지가 있다. 둘 다 예외를 잡을 수 있지만, 작동 방식이 다름!

  • try-catch는 await을 사용하는 비동기 코드에서 예외를 잡음.
  • await 키워드가 있는 비동기 함수에서 예외가 발생하면, catch 블록으로 이동.
  • Future.wait()처럼 여러 개의 Future가 있을 때, 하나의 Future에서 예외가 발생하면 전체가 catch로 이동!


 ✔️  try-catch 예제

Future<String> fetchUser() async {
  await Future.delayed(Duration(seconds: 1));
  throw Exception("사용자 데이터 가져오기 실패!");
}

void main() async {
  try {
    String user = await fetchUser();
    print("✅ 사용자 정보: $user");
  } catch (e) {
    print("⚠️ 예외 발생: $e");
  }
}
  • await fetchUser();에서 예외 발생하면 catch 블록으로 이동
  • 따라서 이후 코드(print("✅ 사용자 정보: $user");)는 실행되지 않음!

 

 ✔️ catchError() 사용 ( then 방식 )

  • catchError()Future 체이닝 ( .then() )에서 사용
  • try-catch와 달리, 개별 Future에서 예외를 처리할 수 있음
  • 즉, 각 Future에서 예외가 발생해도 catchError()가 개별적으로 처리하므로, 다른 Future는 정상 실행됨
Future<String> fetchUser() {
  return Future.delayed(Duration(seconds: 1), () {
    throw Exception("사용자 데이터 가져오기 실패!");
  }).catchError((e) {
    print("⚠️ Future에서 예외 발생: $e");
    return "❌ 오류 발생"; // 기본값 반환
  });
}

void main() async {
  String user = await fetchUser();
  print("✅ 사용자 정보: $user");
}
  • catchError()에서 예외를 잡고, 기본값 "❌ 오류 발생"을 반환하므로 프로그램이 멈추지 않음!
  • 따라서 print("✅ 사용자 정보: $user");가 실행됨

 

✔️ try-catch vs catchError() 비교 정리

구분 try-catch (async-await 방식) catchError() (then 방식)
예외 처리 위치 try 블록 내에서만 예외 처리 특정 Future에 대해 개별적으로 예외 처리
비동기 코드 사용 await 키워드 사용 .then() 체이닝 방식 사용
예외 발생 시 동작 await Future.wait([...])에서 예외 발생 시 즉시 catch 블록으로 이동 → 이후 코드 실행 안됨 개별 Future에서 발생한 예외만 처리 → 다른 Future 실행 가능
예외 발생 후 계속 실행 가능? ❌ catch 블록으로 이동하여 이후 코드 실행 안됨 ✅ 예외 발생한 Future만 처리하고 나머지는 정상 실행

 

 ✔️ try-catchcatchError()를 함께 사용하는 예제

  • 둘을 조합하면 Future.wait()에서 개별 예외를 처리하면서도 전체 try-catch 블록을 유지할 수 있음!
Future<String> fetchUser() async {
  await Future.delayed(Duration(seconds: 1));
  throw Exception("사용자 데이터 가져오기 실패!");
}

Future<String> fetchPosts() async {
  await Future.delayed(Duration(seconds: 2));
  return "📄 게시물 데이터";
}

void main() async {
  try {
    var results = await Future.wait([
      fetchUser().catchError((e) => "❌ 오류 발생: $e"),  // 개별적으로 예외 처리
      fetchPosts()
    ]);

    print("✅ 결과: $results");
  } catch (e) {
    print("⚠️ Future.wait() 예외 발생: $e");
  }
}
  • fetchUser()에서 예외가 발생해도 catchError()가 개별적으로 처리 → 프로그램이 멈추지 않음
  • 그래서 print("✅ 결과: $results");가 정상 실행됨!
  • fetchUser()는 예외 발생했지만, fetchPosts()는 정상 실행됨

 

📌 결론

사용법 언제 사용? 특징
try-catch await을 사용하는 비동기 함수에서 예외 처리할 때 Future.wait()에서 예외 발생 시 전체가 catch 블록으로 이동
catchError() 특정 Future에서 예외를 개별적으로 처리할 때 하나의 Future에서 예외가 발생해도 다른 Future 실행 가능
  • try-catch는 전체 Future.wait()에서 예외를 잡음 → 하나라도 예외가 발생하면 catch로 이동
  • catchError()는 개별 Future에서 예외를 잡음 → 나머지 Future는 정상 실행 가능
  • 둘을 조합하면 Future.wait()에서도 유연한 예외 처리가 가능!
반응형

✔️ 테스트 예제 코드 1

 위에서 진행했던 내용을 토대로 충분한 실습을 진행한 후, 테스트 예제를 진행해보도록 하자.

 1. 사용자로부터 숫자를 입력받아 나누기를 수행하는 프로그램 만들기 (0으로 나눌 경우 예외 처리)

 2. 사용자로부터 두 개의 숫자 입력받기

 3. 나눗셈 수행 ( num1 / num2 )

 4. 0으로 나누려고 하면 예외 처리 ( try-catch )

 5. 잘못된 입력(숫자가 아닌 값 입력)도 예외 처리

import 'dart:io';

void main() {
  while (true) {
    try {
      stdout.write("첫 번째 숫자를 입력하세요: ");
      double num1 = double.parse(stdin.readLineSync()!); // 숫자로 변환

      stdout.write("두 번째 숫자를 입력하세요: ");
      double num2 = double.parse(stdin.readLineSync()!); // 숫자로 변환

      // 0으로 나누는 경우 예외 발생
      if (num2 == 0) {
        throw Exception("0으로 나눌 수 없습니다!");
      }

      double result = num1 / num2;
      print("✅ 결과: $num1 / $num2 = $result");
      break; // 정상적으로 실행되면 루프 종료

    } catch (e) {
      print("⚠️ 오류 발생: $e");
      print("🔄 다시 입력해주세요.");
    }
  }
}

 

 

✔️ 테스트 예제 코드 2

 1. BankAccount 클래스를 만들어, 잔액보다 큰 금액을 출금하려고 하면 예외를 발생시키는 기능 추가

 2. BankAccount 클래스 생성

 3. 잔액(balance)을 관리하는 속성 추가
 4. 입금(deposit()) 및 출금(withdraw()) 메서드 추가
 5. 잔액보다 큰 금액을 출금하려고 하면 예외 클래스 (InsufficientBalanceException) 발생
 6. 예외 처리 (try-catch) 추가하여 프로그램이 강제 종료되지 않도록 보호

 7. 출금 시 잔액이 부족하면 예외 발생 (throw) → catch에서 처리

 8. 사용자 입력을 받아 출금 처리 (stdin.readLineSync())

 9. 0 입력 시 프로그램 종료

import 'dart:io';

// 사용자 정의 예외 클래스 (잔액 부족 예외)
class InsufficientBalanceException implements Exception {
  final String message;
  InsufficientBalanceException(this.message);

  @override
  String toString() => "❌ InsufficientBalanceException: $message";
}

// 은행 계좌 클래스
class BankAccount {
  String owner;
  double balance;

  BankAccount(this.owner, this.balance);

  // 입금 메서드
  void deposit(double amount) {
    balance += amount;
    print("💰 ${owner}님이 ${amount}원을 입금했습니다. 현재 잔액: ${balance}원");
  }

  // 출금 메서드 (음수 입력 방지 추가)
  void withdraw(double amount) {
    if (amount <= 0) {
      throw Exception("출금 금액은 0보다 커야 합니다!");
    }
    if (amount > balance) {
      throw InsufficientBalanceException("잔액이 부족합니다! 현재 잔액: ${balance}원");
    }
    balance -= amount;
    print("🏦 ${owner}님이 ${amount}원을 출금했습니다. 현재 잔액: ${balance}원");
  }
}

void main() {
  BankAccount account = BankAccount("Kim", 50000); // 초기 잔액: 50,000원

  while (true) {
    try {
      stdout.write("\n💰 출금할 금액을 입력하세요 (종료하려면 0 입력): ");
      double amount = double.parse(stdin.readLineSync()!);

      if (amount == 0) {
        print("🚪 프로그램을 종료합니다.");
        break;
      }

      account.withdraw(amount); // 출금 실행
    } catch (e) {
      print("⚠️ 오류 발생: $e");
    }
  }
}

 

✔️ 테스트 예제 코드 3

 1. Future를 사용해 서버에서 데이터를 가져오는 비동기 함수 만들고, 네트워크 오류 시 예외 처리

 2. 서버에서 데이터를 가져오는 비동기 함수(fetchData()) 구현 ( 30% 확률로 네트워크 오류 발생 )
 3. 일정 확률로 네트워크 오류가 발생하도록 예외(throw Exception()) 추가
 4. 예외 발생 시 try-catch를 사용해 프로그램이 멈추지 않도록 처리
 5. 성공하면 서버 데이터를 출력, 실패하면 오류 메시지 출력

 6. 서버에서 가져온 데이터를 JSON으로 변환하여 출력 가능 (jsonDecode())

 7. 네트워크 재시도를 추가하여 오류 발생 시 다시 요청 가능 ( 3번까지 재시도 진행 )

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

// 서버에서 데이터를 가져오는 함수 (30% 확률로 네트워크 오류 발생)
Future<String> fetchData() {
  return Future.delayed(Duration(seconds: 1), () {
    if (Random().nextInt(10) < 3) {
      throw Exception("🚨 네트워크 오류 발생!");
    }
    return '{"id": 101, "name": "Dart Future"}'; // 정상 JSON 데이터 반환
  });
}

// 네트워크 재시도 기능 (최대 3번 시도)
Future<Map<String, dynamic>> retryFetchData({int retries = 3}) async {
  for (int attempt = 1; attempt <= retries; attempt++) {
    print("🔄 데이터 요청 (시도 $attempt/$retries)...");
    try {
      String jsonString = await fetchData();
      return jsonDecode(jsonString); // JSON 변환 후 반환
    } catch (e) {
      print("⚠️ 요청 실패 (시도 $attempt): $e");

      if (attempt == retries) {
        print("🚨 최대 재시도 횟수 초과, 기본 데이터 반환");
        return {"id": 0, "name": "❌ 네트워크 오류"}; // 마지막 시도 실패 시 기본값 반환
      }

      await Future.delayed(Duration(seconds: 1)); // 재시도 전 딜레이 추가
    }
  }

  return {}; // 여기까지 도달하지 않음 (예외 처리용)
}

void main() async {
  print("⏳ 데이터 요청 시작...");
  Map<String, dynamic> data = await retryFetchData();
  print("✅ 결과: $data");
}
  • 이제 fetchData()가 실패해도, 최대 3번까지 자동으로 재시도함
  • 모든 시도가 실패하면 기본 데이터를 반환하여 프로그램이 멈추지 않음
  • JSON 변환까지 적용하여, 서버 데이터가 Map<String, dynamic> 형태로 출력됨

 


 이렇게 예외 처리 (Exception Handling) & Future Error Handling에 대해서 알아보았다. 추가 적인 내용이 필요한 경우에는 댓글을 요청드리고, 틀린 부분이 있다면 이것 또한 댓글로 알려주시면 수정하도록 하겠습니다!


 

반응형