Dart 언어에서 예외 처리는 프로그램이 실행 중에 발생할 수 있는 오류나 예외 상황을 처리하기 위해 사용된다. 예외는 예상치 못한 상황을 처리할 수 있는 메커니즘을 제공한다. 예외가 발생했을 때, 프로그램이 바로 종료되는 대신 예외를 처리할 수 있는 코드를 작성함으로써 프로그램의 안정성을 높일 수 있다.

try 블록

try 블록은 예외가 발생할 수 있는 코드를 감싸는 데 사용된다. try 블록 내에서 발생한 예외는 catch 또는 finally 블록에서 처리할 수 있다. try 블록이 완료되면, catch 또는 finally 블록이 실행된다.

try {
  // 예외가 발생할 가능성이 있는 코드
}

catch 블록

catch 블록은 try 블록에서 발생한 예외를 처리한다. 예외가 발생하면, 프로그램의 흐름이 try 블록을 중단하고 즉시 catch 블록으로 이동한다. 예외 객체를 사용하여 어떤 종류의 예외가 발생했는지 확인할 수 있다. Dart에서는 catch 블록에 예외 객체와 스택 추적 정보를 받을 수 있다.

catch (e) {
  // 예외 처리 코드
}

또한, 예외 객체와 스택 추적 정보를 모두 받을 수도 있다.

catch (e, s) {
  // 예외 처리 코드
  print('예외: $e');
  print('스택 추적: $s');
}

finally 블록

finally 블록은 예외가 발생하든 발생하지 않든 항상 실행되는 코드이다. 예외가 발생하더라도 반드시 실행되어야 할 코드가 있을 때 유용하다. 예를 들어, 파일을 열었다면, 파일을 닫는 작업을 반드시 해야 하는데, 이때 finally 블록에서 해당 작업을 처리할 수 있다.

finally {
  // 예외 발생 여부와 상관없이 항상 실행되는 코드
}

이러한 finally 블록은 자원 해제 작업을 포함한 후처리 작업에 주로 사용된다.

전체 예시

다음은 try, catch, finally 블록을 모두 사용하는 예시이다:

void main() {
  try {
    int result = 12 ~/ 0;
    print(result);
  } catch (e) {
    print('예외 발생: $e');
  } finally {
    print('이 코드는 항상 실행된다.');
  }
}

이 코드에서 12 ~/ 0은 정수 나눗셈이므로, 0으로 나누는 예외가 발생하여 catch 블록으로 이동한다. catch 블록에서 예외를 출력한 후 finally 블록이 실행된다.

예외 흐름 다이어그램

try-catch-finally의 동작 흐름을 다이어그램으로 나타내면 다음과 같다:

graph TD; A[try 블록 실행] --> B{예외 발생 여부}; B -- 예외 발생 --> C[catch 블록 실행]; B -- 예외 없음 --> D[finally 블록 실행]; C --> D; D --> E[프로그램 계속 실행];

여러 개의 catch 블록

Dart에서는 catch 블록을 여러 개 사용할 수 있으며, 각 블록은 다른 예외 유형을 처리하도록 지정할 수 있다. 이를 통해 더 세부적으로 예외를 처리할 수 있다. Dart의 모든 예외는 Exception 클래스 또는 그 하위 클래스의 인스턴스로 처리된다.

try {
  // 예외 발생 가능 코드
} on FormatException {
  // 특정 예외 유형 처리 (FormatException)
} catch (e) {
  // 다른 모든 예외 처리
}

위 코드는 FormatException 예외가 발생하면 해당 예외를 처리하는 on 블록이 실행되고, 그 외의 다른 예외는 catch 블록에서 처리된다.

on 키워드를 사용하면 예외 유형을 명확하게 지정할 수 있다. 반면, catch는 모든 예외를 처리하는데 적합한다.

rethrow를 통한 예외 재발생

때로는 catch 블록에서 예외를 처리한 후, 그 예외를 다시 던져 다른 상위 코드에서 추가로 처리해야 할 때가 있다. 이때 rethrow 키워드를 사용하여 예외를 재발생시킬 수 있다.

try {
  // 예외 발생 가능 코드
} catch (e) {
  print('예외 처리 중: $e');
  rethrow; // 예외를 다시 던짐
}

이 코드는 예외를 한 번 처리한 후, 그 예외를 상위 호출 스택으로 다시 던져 상위 코드에서 추가 처리가 가능하도록 한다.

비동기 함수에서의 예외 처리

비동기 함수에서도 동일한 방식으로 예외 처리를 할 수 있지만, Future와 함께 사용될 경우에는 awaittry-catch를 결합해야 한다. 비동기 함수에서 예외가 발생할 경우, 해당 예외는 Future 객체에 전달되기 때문에 try-catch 블록에서 이를 처리해야 한다.

Future<void> asyncFunction() async {
  try {
    await someAsyncTask();
  } catch (e) {
    print('비동기 작업 중 예외 발생: $e');
  } finally {
    print('비동기 작업 종료');
  }
}

비동기 함수에서 발생한 예외도 일반적인 catch 블록에서 처리할 수 있으며, finally 블록은 여전히 작업 완료 후에 실행된다.

예외 처리의 장점

예외 처리는 프로그램의 오류를 우아하게 처리할 수 있도록 하며, 프로그램이 예외로 인해 중단되지 않고 계속 실행되도록 도와준다. 예외 처리를 사용하지 않을 경우, 프로그램은 예기치 않은 오류로 인해 강제 종료될 수 있지만, try-catch-finally 구조를 사용하면 예외 상황을 처리하면서도 프로그램의 흐름을 제어할 수 있다.

다음으로 Dart의 예외 처리에서 유용하게 사용되는 사용자 정의 예외와 예외의 다양한 활용 방법에 대해 다룰 것이다.

사용자 정의 예외

Dart에서는 기본적으로 제공되는 예외 외에도 개발자가 직접 예외를 정의할 수 있다. 이를 통해 특정 상황에서 발생할 수 있는 예외를 세분화하고, 더 명확하게 예외를 처리할 수 있다.

사용자 정의 예외는 Exception 클래스를 상속받아 정의할 수 있다. Exception 클래스는 Dart에서 예외를 처리하는 데 사용되는 기본 클래스이다. 사용자 정의 예외 클래스는 주로 예외 메시지나 추가적인 예외 정보를 담는 필드를 포함할 수 있다.

class CustomException implements Exception {
  final String message;

  CustomException(this.message);

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

위의 예제는 CustomException이라는 사용자 정의 예외를 정의하는 코드이다. toString() 메소드를 재정의하여 예외가 발생했을 때 출력될 메시지를 지정할 수 있다.

사용자 정의 예외를 사용하여 예외를 발생시키는 예는 다음과 같다.

void validateInput(int input) {
  if (input < 0) {
    throw CustomException('입력 값은 음수가 될 수 없다.');
  }
}

throw 키워드를 사용하여 예외를 발생시킬 수 있으며, 이 예외는 try-catch 블록으로 처리된다.

void main() {
  try {
    validateInput(-1);
  } catch (e) {
    print(e);
  }
}

이 코드는 validateInput(-1)을 호출하여 예외를 발생시키고, catch 블록에서 해당 예외를 처리한다. 출력 결과는 다음과 같이 나타난다.

CustomException: 입력 값은 음수가 될 수 없다.

이처럼 사용자 정의 예외는 특정 도메인이나 비즈니스 로직에 맞는 예외 처리를 가능하게 하여, 코드의 가독성을 높이고, 문제를 보다 쉽게 디버깅할 수 있다.

예외 객체의 추가 정보 포함

때로는 예외 객체에 추가적인 정보를 포함시켜야 할 때가 있다. Dart에서는 이를 위해 예외 객체에 필요한 데이터를 전달할 수 있다. 예를 들어, 오류 코드나 관련 데이터를 함께 제공함으로써 예외 상황을 더 구체적으로 설명할 수 있다.

class DetailedException implements Exception {
  final String message;
  final int errorCode;

  DetailedException(this.message, this.errorCode);

  @override
  String toString() => 'Error $errorCode:$message';
}

이 예제에서는 errorCode라는 필드를 추가하여 예외의 구체적인 오류 코드를 함께 전달할 수 있다. 이를 통해 예외 상황을 더욱 구체적으로 파악할 수 있다.

void main() {
  try {
    throw DetailedException('파일을 찾을 수 없다.', 404);
  } catch (e) {
    print(e);
  }
}

이 코드는 다음과 같은 결과를 출력한다:

Error 404: 파일을 찾을 수 없다.

이와 같은 방식을 사용하면 예외 처리 시 추가적인 정보를 제공할 수 있어, 문제를 디버깅하고 해결하는 데 도움이 된다.