# 사용자 정의 예외

사용자 정의 예외(Custom Exception)는 프로그램에서 발생하는 예외 상황을 더 구체적으로 처리하고, 사용자의 의도에 맞는 예외 처리를 구현하기 위해 사용된다. Dart에서는 예외 처리를 위해 표준 라이브러리에서 제공하는 예외 클래스가 있지만, 프로그램의 복잡성에 따라 표준 예외 클래스로는 처리하기 어려운 상황이 발생할 수 있다. 이때, 사용자 정의 예외를 통해 보다 명확하고 의미 있는 예외를 정의할 수 있다.

#### 예외 클래스 생성

Dart에서 사용자 정의 예외는 일반 클래스를 상속받거나 `Exception` 클래스를 상속받아 구현할 수 있다. `Exception` 클래스는 기본적으로 Dart에서 제공하는 예외 클래스이다. 사용자 정의 예외를 만들기 위해서는 이 클래스를 상속받아야 하며, 생성자(constructor)를 통해 예외에 대한 메시지나 추가적인 데이터를 전달할 수 있다.

예를 들어, 특정 값이 범위를 벗어났을 때 발생하는 예외를 처리하는 클래스를 만들어 보자.

```dart
class OutOfRangeException implements Exception {
  final String message;

  OutOfRangeException(this.message);

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

이 예제에서 `OutOfRangeException` 클래스는 `Exception` 인터페이스를 상속받고, 생성자를 통해 예외 메시지를 받는다. `toString()` 메소드를 재정의하여 예외 발생 시 출력할 메시지를 명시적으로 지정한다. 이처럼 예외를 구체화하여, 발생한 오류가 어떤 이유로 발생했는지 명확히 나타낼 수 있다.

#### 사용자 정의 예외 던지기

정의된 예외를 발생시키기 위해서는 `throw` 키워드를 사용한다. 이는 일반적인 예외 처리와 동일하게 `try-catch` 블록 안에서 사용될 수 있다. 다음은 위에서 정의한 `OutOfRangeException`을 발생시키는 예제이다.

```dart
void checkValue(int value) {
  if (value < 0 || value > 100) {
    throw OutOfRangeException('Value must be between 0 and 100.');
  } else {
    print('Value is in range.');
  }
}
```

이 함수는 전달된 값이 0과 100 사이에 있지 않으면 `OutOfRangeException`을 발생시키고, 그렇지 않으면 정상적인 값을 출력한다.

#### 사용자 정의 예외 처리

사용자 정의 예외를 처리하는 방법은 Dart에서 기본 제공하는 예외 처리와 동일하다. 예외 발생 시 이를 잡아내는 `catch` 블록을 작성하여, 필요한 경우 사용자 정의 예외에 맞는 처리를 수행할 수 있다.

```dart
void main() {
  try {
    checkValue(150);
  } catch (e) {
    print(e);
  }
}
```

위 코드에서는 `checkValue(150)`을 호출하여 `OutOfRangeException`이 발생하고, `catch` 블록에서 해당 예외를 잡아 출력하는 방식으로 동작한다.

#### 예외에 데이터 추가

사용자 정의 예외는 예외 상황에 맞는 추가적인 데이터를 포함할 수 있다. 예를 들어, 어떤 함수가 잘못된 값을 입력받았을 때, 입력된 값이나 그 외의 정보를 예외 객체에 포함시켜 사용할 수 있다.

다음은 입력된 값과 함께 범위 정보를 포함하는 예외 클래스의 예이다.

```dart
class OutOfRangeException implements Exception {
  final int input;
  final int min;
  final int max;
  final String message;

  OutOfRangeException(this.input, this.min, this.max, this.message);

  @override
  String toString() => 
      'OutOfRangeException: $message (Input:$input, Range: $min -$max)';
}
```

이 클래스는 입력된 값(`input`)과 허용되는 범위(`min`, `max`), 그리고 예외 메시지(`message`)를 포함한다. 이를 통해 예외가 발생한 상황을 더 구체적으로 설명할 수 있다.

#### 예외 발생과 처리 예시

위에서 정의한 `OutOfRangeException` 클래스를 활용하여, 범위를 벗어난 값에 대한 예외 발생과 그 예외를 처리하는 예시를 작성할 수 있다.

```dart
void checkValue(int value, int min, int max) {
  if (value < min || value > max) {
    throw OutOfRangeException(value, min, max, 'Value is out of range.');
  } else {
    print('Value is within range.');
  }
}

void main() {
  try {
    checkValue(150, 0, 100);
  } catch (e) {
    print(e);
  }
}
```

이 예제에서 `checkValue` 함수는 전달된 값이 지정된 범위(`min`, `max`) 내에 있지 않으면 `OutOfRangeException`을 발생시킨다. `main` 함수에서는 `try-catch` 블록을 사용하여 예외를 처리하고, 발생한 예외에 포함된 정보를 출력한다. 결과적으로, 예외 메시지와 함께 입력된 값과 허용되는 범위가 출력된다.

#### 중첩된 예외 처리

프로그램이 더 복잡해지면 여러 예외 상황이 중첩되어 발생할 수 있다. 이때 하나의 `catch` 블록에서 여러 유형의 예외를 처리하거나, 각 예외마다 다른 처리를 해야 할 수 있다. Dart에서는 여러 `catch` 블록을 사용하여 특정 예외를 구분하고 처리할 수 있다.

다음은 사용자 정의 예외와 함께 다른 일반적인 예외를 처리하는 예제이다.

```dart
void main() {
  try {
    checkValue(-10, 0, 100);
    int result = 100 ~/ 0;  // ZeroDivisionError 발생
  } on OutOfRangeException catch (e) {
    print('Caught an OutOfRangeException: $e');
  } on IntegerDivisionByZeroException catch (e) {
    print('Caught a division by zero: $e');
  } catch (e) {
    print('Caught an unknown exception: $e');
  }
}
```

이 코드는 먼저 `checkValue` 함수에서 발생할 수 있는 `OutOfRangeException`을 처리하고, 그 후 정수 나누기에서 발생할 수 있는 `IntegerDivisionByZeroException`을 처리한다. 마지막 `catch` 블록에서는 모든 예외를 처리하는 기본 예외 처리기가 동작한다.

이 방식으로, 프로그램에서 발생할 수 있는 다양한 예외 상황을 효과적으로 처리할 수 있다.

#### 재사용 가능한 예외 클래스

사용자 정의 예외 클래스는 여러 모듈에서 재사용될 수 있다. 예를 들어, 특정 값의 범위를 체크하는 코드를 여러 곳에서 사용해야 한다면, 해당 예외 클래스를 한 번 정의한 후 여러 곳에서 예외를 발생시키는 방식으로 코드의 재사용성을 높일 수 있다.

다음 예시에서 동일한 `OutOfRangeException`을 다른 함수에서도 재사용할 수 있다.

```dart
void checkTemperature(int temp) {
  if (temp < -50 || temp > 50) {
    throw OutOfRangeException(temp, -50, 50, 'Temperature is out of range.');
  } else {
    print('Temperature is normal.');
  }
}

void checkSpeed(int speed) {
  if (speed < 0 || speed > 200) {
    throw OutOfRangeException(speed, 0, 200, 'Speed is out of range.');
  } else {
    print('Speed is normal.');
  }
}
```

이처럼 하나의 사용자 정의 예외 클래스를 여러 상황에 맞춰 재사용할 수 있다. 각 함수에서는 적절한 범위와 메시지를 설정하여 예외를 발생시키고, 같은 `catch` 블록에서 이들을 처리할 수 있다.

#### 사용자 정의 예외의 장점

사용자 정의 예외를 사용하면 프로그램에서 발생하는 예외 상황을 더 구체적이고 직관적으로 처리할 수 있는 여러 가지 장점이 있다. 이러한 장점을 통해 예외 처리의 가독성을 높이고, 디버깅을 더 용이하게 만들 수 있다.

**1. 명확한 예외 분류**

표준 예외 클래스는 일반적인 예외 상황을 처리하기 위한 것이며, 예외가 발생했을 때 특정 상황을 명확히 구분하기 어렵다. 그러나 사용자 정의 예외를 사용하면 프로그램에서 발생하는 다양한 예외 상황을 구체적으로 분류할 수 있다. 예를 들어, `OutOfRangeException`과 같은 클래스는 값의 범위가 벗어난 상황을 구체적으로 표현하므로, 예외 처리 코드에서 해당 상황을 더 쉽게 파악할 수 있다.

**2. 추가적인 정보 제공**

사용자 정의 예외는 예외 발생 시 그 상황에 대한 추가적인 정보를 함께 전달할 수 있다. 예를 들어, 입력된 값, 허용되는 범위, 발생한 오류 메시지 등 다양한 데이터를 포함할 수 있다. 이렇게 추가적인 정보를 제공함으로써 예외 발생 원인을 더 쉽게 파악할 수 있다.

```dart
class OutOfRangeException implements Exception {
  final int input;
  final int min;
  final int max;
  final String message;

  OutOfRangeException(this.input, this.min, this.max, this.message);

  @override
  String toString() => 
      'OutOfRangeException: $message (Input:$input, Range: $min -$max)';
}
```

위 예제에서는 예외 발생 시 입력된 값과 허용되는 범위를 함께 출력하여, 디버깅 과정에서 문제를 쉽게 찾을 수 있도록 도와준다.

**3. 특정 기능에 맞는 예외 처리**

프로그램의 특정 기능에서만 발생할 수 있는 특수한 예외 상황을 처리할 때 사용자 정의 예외가 유용하다. 예를 들어, 파일 입출력, 네트워크 통신, 데이터베이스 연결 등의 모듈에서 발생할 수 있는 다양한 예외를 각 모듈에 맞게 구체화하여 처리할 수 있다.

#### 사용자 정의 예외와 상속

Dart에서 예외 클래스는 일반적인 클래스이기 때문에 상속을 통해 더 복잡하고 다양한 예외를 정의할 수 있다. 상속을 사용하여 여러 예외를 그룹화하거나, 공통의 속성을 가지는 예외들을 정의할 수 있다.

다음은 상속을 사용하여 더 구체적인 예외를 정의하는 예시이다.

```dart
class ApplicationException implements Exception {
  final String message;

  ApplicationException(this.message);

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

class FileNotFoundException extends ApplicationException {
  FileNotFoundException(String message) : super(message);
}

class NetworkException extends ApplicationException {
  NetworkException(String message) : super(message);
}
```

위 코드에서 `ApplicationException` 클래스는 모든 사용자 정의 예외의 부모 클래스 역할을 하며, `FileNotFoundException`과 `NetworkException`은 각각 파일 입출력과 네트워크 관련 예외를 나타낸다. 상속을 통해 공통적인 속성(`message`)을 재사용하면서, 각 예외에 특화된 처리를 할 수 있다.

이와 같은 구조를 사용하면, 프로그램의 예외 처리 구조가 더욱 체계적이고 일관성 있게 유지될 수 있다.

#### 사용자 정의 예외와 다형성

상속을 통해 정의한 예외 클래스는 다형성을 통해 더욱 유연하게 사용할 수 있다. 예외 처리 코드에서 부모 클래스 타입으로 예외를 처리하면, 여러 자식 클래스의 예외를 동일한 방식으로 처리하거나, 특정 자식 클래스에만 맞는 처리를 할 수 있다.

다음 예제는 `ApplicationException`을 사용하여 다형성을 구현한 예시이다.

```dart
void handleException(ApplicationException e) {
  print(e);
}

void main() {
  try {
    throw FileNotFoundException('File not found.');
  } catch (e) {
    if (e is ApplicationException) {
      handleException(e);
    }
  }

  try {
    throw NetworkException('Network is down.');
  } catch (e) {
    if (e is ApplicationException) {
      handleException(e);
    }
  }
}
```

이 예제에서, `FileNotFoundException`과 `NetworkException`은 모두 `ApplicationException`을 상속받기 때문에, `catch` 블록에서 `ApplicationException` 타입으로 예외를 처리할 수 있다. `handleException` 함수는 부모 클래스 타입을 인수로 받아, 여러 예외를 동일한 방식으로 처리할 수 있다.

**장점**

* 코드의 중복을 줄일 수 있으며, 공통적인 예외 처리를 하나의 함수에서 처리할 수 있다.
* 필요할 경우 자식 클래스의 타입을 체크하여 특정 예외에 맞는 추가적인 처리를 할 수 있다.

#### 사용자 정의 예외의 실제 사용 사례

사용자 정의 예외는 다양한 실제 상황에서 유용하게 사용된다. 특히, 데이터 처리나 입출력, 네트워크 통신과 같은 모듈에서 발생할 수 있는 특수한 예외를 처리하는 데 효과적이다. 예를 들어, 웹 애플리케이션에서 서버와 통신할 때 네트워크 오류가 발생할 수 있는데, 이때 사용자 정의 예외를 사용하여 보다 명확한 메시지와 함께 문제를 처리할 수 있다.

**예시: 파일 처리 예외**

파일을 열거나 읽는 과정에서 파일이 존재하지 않거나 권한이 없을 때 발생하는 예외를 사용자 정의 예외로 처리할 수 있다.

```dart
class FileReadException implements Exception {
  final String fileName;
  final String message;

  FileReadException(this.fileName, this.message);

  @override
  String toString() => 'FileReadException: $message (File:$fileName)';
}

void readFile(String fileName) {
  if (!File(fileName).existsSync()) {
    throw FileReadException(fileName, 'File not found');
  }
  // 파일 읽기 로직
}

void main() {
  try {
    readFile('data.txt');
  } catch (e) {
    print(e);
  }
}
```

이 코드에서 `FileReadException`은 파일을 읽는 도중 발생할 수 있는 문제를 구체적으로 정의하고, 파일 이름과 함께 예외 메시지를 전달한다. `readFile` 함수에서 파일이 존재하지 않으면 `FileReadException`을 던지고, `catch` 블록에서 예외를 처리하여 문제를 출력한다.

**예시: 네트워크 예외**

네트워크 통신 도중 발생할 수 있는 예외를 사용자 정의 예외로 처리할 수 있다. 예를 들어, 서버와의 연결이 끊어지거나 응답이 없을 때 발생하는 문제를 다룰 수 있다.

```dart
class NetworkException implements Exception {
  final String url;
  final String message;

  NetworkException(this.url, this.message);

  @override
  String toString() => 'NetworkException: $message (URL:$url)';
}

void fetchData(String url) {
  if (url.isEmpty) {
    throw NetworkException(url, 'Invalid URL');
  }
  // 네트워크 요청 처리 로직
}

void main() {
  try {
    fetchData('');
  } catch (e) {
    print(e);
  }
}
```

`NetworkException`은 네트워크 관련 예외를 처리하기 위한 클래스이다. `fetchData` 함수에서 잘못된 URL을 전달하면 `NetworkException`이 발생하고, `catch` 블록에서 해당 예외를 처리하여 문제를 알린다.

#### 사용자 정의 예외와 로깅

실제 애플리케이션에서는 예외 발생 시 로그를 남겨 디버깅 및 문제 분석에 사용될 수 있다. 사용자 정의 예외를 사용하면 예외가 발생한 구체적인 상황과 관련된 정보를 로그에 기록할 수 있다. Dart에서는 `print`를 통해 로그를 출력하거나, 파일에 기록하는 방식을 사용할 수 있다.

다음은 사용자 정의 예외와 로깅을 결합한 예시이다.

```dart
class DatabaseException implements Exception {
  final String operation;
  final String message;

  DatabaseException(this.operation, this.message);

  @override
  String toString() => 'DatabaseException: $message (Operation:$operation)';
}

void performDatabaseOperation(String operation) {
  // 데이터베이스 작업 시 오류가 발생하는 가정
  if (operation == 'insert') {
    throw DatabaseException(operation, 'Failed to insert data');
  }
}

void main() {
  try {
    performDatabaseOperation('insert');
  } catch (e) {
    print('Error occurred: $e');
    logError(e);
  }
}

void logError(Exception e) {
  // 여기서는 간단히 콘솔에 기록하지만, 실제로는 파일이나 외부 시스템에 로그 기록 가능
  print('Logging error: $e');
}
```

이 예제에서 `DatabaseException` 클래스는 데이터베이스 작업 중 발생한 예외를 나타내며, `performDatabaseOperation` 함수는 데이터베이스 작업을 수행하다가 오류가 발생했을 때 예외를 던진다. `main` 함수에서는 `try-catch` 블록을 사용해 예외를 처리하고, 발생한 예외를 로그에 기록한다.

실제 프로젝트에서는 `logError` 함수를 확장하여 파일에 기록하거나, 원격 서버에 예외 데이터를 보내는 등의 방식으로 사용할 수 있다. 이를 통해 프로그램에서 발생하는 예외를 추적하고 분석하는 데 도움을 줄 수 있다.

#### 사용자 정의 예외와 다단계 예외 처리

경우에 따라 하나의 작업에서 여러 단계의 예외 처리가 필요할 수 있다. 예를 들어, 데이터베이스 연결, 네트워크 요청, 파일 읽기 등 다양한 작업이 한 함수 내에서 연속적으로 이루어질 때 각각의 작업에서 발생할 수 있는 예외를 따로따로 처리하거나, 상위 예외로 감싸서 처리하는 방식으로 구현할 수 있다.

다음은 여러 단계의 예외를 처리하는 예시이다.

```dart
void performOperations() {
  try {
    performDatabaseOperation('insert');
    fetchData('invalid_url');
  } catch (e) {
    print('Error in performOperations: $e');
    rethrow;  // 예외를 다시 상위로 전달
  }
}

void main() {
  try {
    performOperations();
  } catch (e) {
    print('Caught at top level: $e');
  }
}
```

이 예제에서는 `performOperations` 함수 내에서 두 가지 작업(`performDatabaseOperation`과 `fetchData`)을 수행하고, 각각의 작업에서 예외가 발생할 경우 이를 처리한 후 예외를 다시 상위로 전달한다(`rethrow`). 최종적으로 상위 `catch` 블록에서 모든 예외를 처리할 수 있다.

이처럼, 다단계 예외 처리를 통해 예외 발생 시 세부적인 처리를 한 후에도 필요할 경우 상위 계층으로 예외를 전달할 수 있다.

#### 사용자 정의 예외 클래스의 확장

복잡한 시스템에서는 다양한 예외를 관리하기 위해 사용자 정의 예외 클래스를 확장할 수 있다. 예를 들어, 예외를 세부적으로 분류하여 여러 예외 클래스를 정의하고, 상속을 통해 공통적인 예외 처리 로직을 재사용할 수 있다.

다음은 예외를 세분화하여 다양한 상황을 처리하는 예시이다.

```dart
class InvalidUserInputException implements Exception {
  final String message;

  InvalidUserInputException(this.message);

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

class InvalidAgeException extends InvalidUserInputException {
  InvalidAgeException(String message) : super(message);
}

class InvalidEmailException extends InvalidUserInputException {
  InvalidEmailException(String message) : super(message);
}

void validateUserInput(int age, String email) {
  if (age < 0 || age > 120) {
    throw InvalidAgeException('Age must be between 0 and 120.');
  }
  if (!email.contains('@')) {
    throw InvalidEmailException('Email is invalid.');
  }
}

void main() {
  try {
    validateUserInput(150, 'invalid_email');
  } catch (e) {
    print(e);
  }
}
```

이 예제에서는 `InvalidUserInputException`을 상속받아 `InvalidAgeException`과 `InvalidEmailException`을 정의하고, 각각 나이와 이메일에 대한 유효성 검사를 수행한다. 이를 통해 입력 값에 따라 더 구체적인 예외 메시지를 던지고 처리할 수 있다.
