이번에는 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-catch와 catchError()를 함께 사용하는 예제
- 둘을 조합하면 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에 대해서 알아보았다. 추가 적인 내용이 필요한 경우에는 댓글을 요청드리고, 틀린 부분이 있다면 이것 또한 댓글로 알려주시면 수정하도록 하겠습니다!
'IT > Dart' 카테고리의 다른 글
Dart_11일차 : 파일 및 디렉터리 관리 (File & Directory Handling) (0) | 2025.03.10 |
---|---|
Dart_10일차: 파일 입출력 (File I/O) & 스트림 (Stream) 활용 (0) | 2025.03.09 |
Dart_8일차 : 클래스 심화 (생성자, Getter/Setter, 연산자 오버로딩) (0) | 2025.03.06 |
Dart_7일차 : 파일 입출력 & JSON 데이터 처리 (0) | 2025.03.05 |
Dart_6일차 : 고급 Stream 활용 (listen(), StreamController, Broadcast Stream) (2) | 2025.03.04 |