# 예외의 정의와 발생

#### 예외란 무엇인가?

프로그래밍에서 '예외'는 프로그램 실행 중 예상치 못한 상황이 발생하여 정상적인 흐름을 방해할 때 나타나는 오류를 의미한다. 예외는 코드 실행 중에 발생할 수 있으며, 이를 처리하지 않으면 프로그램이 비정상적으로 종료될 수 있다. 예외는 시스템 자원의 부족, 잘못된 사용자 입력, 네트워크 문제 등 다양한 원인으로 발생할 수 있으며, 개발자는 이를 예측하고 적절히 처리하여 프로그램의 안정성을 유지해야 한다.

예외를 정의하는 가장 큰 이유는 예상치 못한 오류로 인해 프로그램이 중단되는 것을 방지하고, 오류가 발생했을 때도 프로그램이 정상적인 흐름을 유지하도록 돕기 위함이다. 예외 처리는 이와 같은 비정상적인 상황을 '처리할 수 있는 상황'으로 전환해 준다.

#### 예외 발생의 예시

Dart에서는 예외를 던지는 방식으로 예외를 발생시킬 수 있다. 예를 들어, 다음과 같은 코드에서는 0으로 나누는 상황이 예외를 발생시킨다.

```dart
int divide(int a, int b) {
  if (b == 0) {
    throw Exception('Cannot divide by zero');
  }
  return a ~/ b;
}
```

위 코드에서는 `b`가 0일 경우 `Exception`을 발생시키도록 되어 있다. 이는 함수 호출자가 잘못된 입력을 제공했을 때 이를 알려주고, 이를 통해 오류를 처리할 수 있는 기회를 제공한다.

#### 수학적 모델링

예외 발생을 수학적으로 표현해 보면, 특정 함수의 정의역에서 정의되지 않는 상황이 발생할 때 예외가 던져진다고 할 수 있다. 예를 들어, 다음과 같은 나눗셈 함수 $f(x, y)$를 고려해 봅시다:

$$
f(x, y) = \frac{x}{y}, \quad \text{단 } y \neq 0
$$

이때, $y = 0$일 경우 정의되지 않으므로, 이를 예외 상황으로 처리할 수 있다. 프로그래밍에서는 이와 같은 조건에서 예외를 던져야 한다.

#### 예외 발생 조건

일반적으로 예외가 발생하는 조건은 다음과 같다:

1. **논리적 오류**: 잘못된 값이 전달될 때.
   * 예를 들어, 음수 값을 받을 수 없는 함수에 음수 값이 전달되는 경우.
2. **리소스 부족**: 시스템 자원이 부족한 경우.
   * 예를 들어, 메모리 부족이나 네트워크 연결 실패.
3. **외부 시스템 오류**: 외부 시스템과의 통신에서 문제가 발생할 때.
   * 파일을 찾을 수 없거나, API 호출이 실패하는 경우.

Dart에서는 이와 같은 상황에서 `throw` 키워드를 사용하여 예외를 발생시킬 수 있으며, 이는 프로그램의 실행 흐름을 바꾸는 역할을 한다.

#### 실행 흐름 제어와 예외

프로그래밍에서 예외는 함수나 코드 블록에서 발생하여 호출 스택을 거슬러 올라가면서 적절한 예외 처리기를 찾는다. 예를 들어, Dart의 경우 예외가 발생하면 해당 예외를 처리할 수 있는 `try-catch` 블록으로 흐름이 이동한다. 만약 예외가 처리되지 않으면 프로그램은 비정상 종료된다.

다음은 Dart에서 예외 발생 흐름을 설명하는 다이어그램이다:

{% @mermaid/diagram content="graph TD
A\[프로그램 시작] --> B\[함수 호출]
B --> C\[정상 처리]
B --> D\[예외 발생]
D --> E\[예외 처리기 탐색]
E -->|예외 처리기 발견| F\[예외 처리]
E -->|예외 처리기 없음| G\[프로그램 종료]
F --> H\[프로그램 계속 실행]" %}

이 흐름에서, 예외가 발생하면 처리기가 있는지 확인하고, 없다면 프로그램이 종료되는 구조를 띕니다.

#### 예외 처리기 탐색

예외가 발생하면 Dart는 호출 스택을 탐색하며 예외 처리기를 찾는다. 이 과정은 다음과 같은 단계로 이루어진다:

1. **예외 발생**: 함수 내부에서 `throw` 키워드에 의해 예외가 발생한다.
2. **호출 스택 탐색**: 예외가 발생한 함수에서부터 호출된 함수들로 스택을 거슬러 올라가며, 각 함수가 예외를 처리할 수 있는지 확인한다.
3. **예외 처리기 확인**: 예외 처리기가 있는지 확인하는데, 이는 `try-catch` 구문에 의해 설정된다. 만약 해당 구문이 발견되면, 예외는 처리된다. 그렇지 않으면 계속해서 상위 호출 스택으로 탐색이 이어진다.
4. **최상위 도달**: 호출 스택을 모두 거슬러 올라가서도 예외 처리기가 없으면 프로그램은 비정상적으로 종료된다.

이를 수식으로 표현해 보면, 예외가 발생하는 함수 $f(x)$가 있고, 이 함수가 다른 함수 $g(y)$에 의해 호출된 경우를 생각할 수 있다. 이때 예외 처리기는 함수 호출 관계를 따라 탐색된다.

$$
f(x) \rightarrow g(y) \rightarrow \text{Exception Handler}
$$

#### 예외 처리 구문

Dart에서 예외는 `try-catch` 블록을 사용하여 처리할 수 있다. 다음은 예외 처리의 일반적인 형태이다.

```dart
try {
  // 예외가 발생할 가능성이 있는 코드
} catch (e) {
  // 예외 처리 코드
}
```

`catch` 블록은 예외가 발생했을 때 실행되며, 발생한 예외 객체를 받아 처리할 수 있다. Dart에서 예외 객체는 `Exception` 클래스의 인스턴스로 전달되며, 이 객체를 통해 예외에 대한 자세한 정보를 얻을 수 있다.

#### 수학적 예외 처리 예시

수학적으로 예외 처리는 함수의 정의역이 특정 조건에서 유효하지 않음을 알릴 때 유용하다. 예를 들어, 나눗셈 함수에서 분모가 0일 경우 예외를 던진다고 할 수 있다. 함수 $f(x, y)$의 경우, $y = 0$일 때 예외를 던지는 조건을 아래와 같이 설정할 수 있다:

$$
f(x, y) = \begin{cases} \frac{x}{y} & \text{if } y \neq 0 \ \text{Exception} & \text{if } y = 0 \end{cases}
$$

이를 프로그래밍으로 구현하면 다음과 같다:

```dart
double divide(double x, double y) {
  if (y == 0) {
    throw Exception('Cannot divide by zero');
  }
  return x / y;
}
```

위 코드에서 분모가 0일 때 예외를 발생시켜, 나눗셈이 정의되지 않은 상황을 처리하게 된다. 수학적으로는 정의역에 포함되지 않는 값을 예외로 처리하는 방식이다.

#### 사용자 정의 예외

개발자가 직접 예외를 정의하고 발생시킬 수 있다. Dart에서는 기본 예외 외에도 개발자가 자신의 예외 클래스를 만들어 사용할 수 있다. 예를 들어, 계산기 프로그램에서 잘못된 연산이 들어왔을 때 발생시키는 사용자 정의 예외는 다음과 같이 정의할 수 있다:

```dart
class InvalidOperationException implements Exception {
  final String message;
  InvalidOperationException(this.message);
  
  @override
  String toString() {
    return "InvalidOperationException: $message";
  }
}

void performOperation(String operation) {
  if (operation != '+' && operation != '-') {
    throw InvalidOperationException('Invalid operation: $operation');
  }
}
```

이 코드는 잘못된 연산이 입력되었을 때 `InvalidOperationException`을 던진다. 이를 통해 개발자는 예외를 보다 의미 있게 관리할 수 있다.

#### 예외 처리의 중요한 원칙

1. **예외는 예측 가능한 상황에만 사용해야 한다**: 예외는 정말로 예상치 못한 상황을 처리할 때 사용해야 하며, 일반적인 흐름 제어에 사용해서는 안 된다.
2. **예외 처리는 성능에 영향을 줄 수 있다**: 예외가 발생하고 처리되는 과정은 상당한 자원을 소모할 수 있으므로, 지나치게 많은 예외를 발생시키는 코드는 성능 저하를 초래할 수 있다.
3. **적절한 메시지를 제공하라**: 예외가 발생할 때 적절한 메시지를 제공함으로써 디버깅이 더 쉬워지고, 사용자가 예외 상황을 이해할 수 있다.
