사용자 정의 예외(Custom Exception)는 프로그램에서 발생하는 예외 상황을 더 구체적으로 처리하고, 사용자의 의도에 맞는 예외 처리를 구현하기 위해 사용된다. Dart에서는 예외 처리를 위해 표준 라이브러리에서 제공하는 예외 클래스가 있지만, 프로그램의 복잡성에 따라 표준 예외 클래스로는 처리하기 어려운 상황이 발생할 수 있다. 이때, 사용자 정의 예외를 통해 보다 명확하고 의미 있는 예외를 정의할 수 있다.
예외 클래스 생성
Dart에서 사용자 정의 예외는 일반 클래스를 상속받거나 Exception
클래스를 상속받아 구현할 수 있다. Exception
클래스는 기본적으로 Dart에서 제공하는 예외 클래스이다. 사용자 정의 예외를 만들기 위해서는 이 클래스를 상속받아야 하며, 생성자(constructor)를 통해 예외에 대한 메시지나 추가적인 데이터를 전달할 수 있다.
예를 들어, 특정 값이 범위를 벗어났을 때 발생하는 예외를 처리하는 클래스를 만들어 보자.
class OutOfRangeException implements Exception {
final String message;
OutOfRangeException(this.message);
@override
String toString() => 'OutOfRangeException: $message';
}
이 예제에서 OutOfRangeException
클래스는 Exception
인터페이스를 상속받고, 생성자를 통해 예외 메시지를 받는다. toString()
메소드를 재정의하여 예외 발생 시 출력할 메시지를 명시적으로 지정한다. 이처럼 예외를 구체화하여, 발생한 오류가 어떤 이유로 발생했는지 명확히 나타낼 수 있다.
사용자 정의 예외 던지기
정의된 예외를 발생시키기 위해서는 throw
키워드를 사용한다. 이는 일반적인 예외 처리와 동일하게 try-catch
블록 안에서 사용될 수 있다. 다음은 위에서 정의한 OutOfRangeException
을 발생시키는 예제이다.
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
블록을 작성하여, 필요한 경우 사용자 정의 예외에 맞는 처리를 수행할 수 있다.
void main() {
try {
checkValue(150);
} catch (e) {
print(e);
}
}
위 코드에서는 checkValue(150)
을 호출하여 OutOfRangeException
이 발생하고, catch
블록에서 해당 예외를 잡아 출력하는 방식으로 동작한다.
예외에 데이터 추가
사용자 정의 예외는 예외 상황에 맞는 추가적인 데이터를 포함할 수 있다. 예를 들어, 어떤 함수가 잘못된 값을 입력받았을 때, 입력된 값이나 그 외의 정보를 예외 객체에 포함시켜 사용할 수 있다.
다음은 입력된 값과 함께 범위 정보를 포함하는 예외 클래스의 예이다.
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
클래스를 활용하여, 범위를 벗어난 값에 대한 예외 발생과 그 예외를 처리하는 예시를 작성할 수 있다.
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
블록을 사용하여 특정 예외를 구분하고 처리할 수 있다.
다음은 사용자 정의 예외와 함께 다른 일반적인 예외를 처리하는 예제이다.
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
을 다른 함수에서도 재사용할 수 있다.
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. 추가적인 정보 제공
사용자 정의 예외는 예외 발생 시 그 상황에 대한 추가적인 정보를 함께 전달할 수 있다. 예를 들어, 입력된 값, 허용되는 범위, 발생한 오류 메시지 등 다양한 데이터를 포함할 수 있다. 이렇게 추가적인 정보를 제공함으로써 예외 발생 원인을 더 쉽게 파악할 수 있다.
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에서 예외 클래스는 일반적인 클래스이기 때문에 상속을 통해 더 복잡하고 다양한 예외를 정의할 수 있다. 상속을 사용하여 여러 예외를 그룹화하거나, 공통의 속성을 가지는 예외들을 정의할 수 있다.
다음은 상속을 사용하여 더 구체적인 예외를 정의하는 예시이다.
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
을 사용하여 다형성을 구현한 예시이다.
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
함수는 부모 클래스 타입을 인수로 받아, 여러 예외를 동일한 방식으로 처리할 수 있다.
장점
- 코드의 중복을 줄일 수 있으며, 공통적인 예외 처리를 하나의 함수에서 처리할 수 있다.
- 필요할 경우 자식 클래스의 타입을 체크하여 특정 예외에 맞는 추가적인 처리를 할 수 있다.
사용자 정의 예외의 실제 사용 사례
사용자 정의 예외는 다양한 실제 상황에서 유용하게 사용된다. 특히, 데이터 처리나 입출력, 네트워크 통신과 같은 모듈에서 발생할 수 있는 특수한 예외를 처리하는 데 효과적이다. 예를 들어, 웹 애플리케이션에서 서버와 통신할 때 네트워크 오류가 발생할 수 있는데, 이때 사용자 정의 예외를 사용하여 보다 명확한 메시지와 함께 문제를 처리할 수 있다.
예시: 파일 처리 예외
파일을 열거나 읽는 과정에서 파일이 존재하지 않거나 권한이 없을 때 발생하는 예외를 사용자 정의 예외로 처리할 수 있다.
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
블록에서 예외를 처리하여 문제를 출력한다.
예시: 네트워크 예외
네트워크 통신 도중 발생할 수 있는 예외를 사용자 정의 예외로 처리할 수 있다. 예를 들어, 서버와의 연결이 끊어지거나 응답이 없을 때 발생하는 문제를 다룰 수 있다.
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
를 통해 로그를 출력하거나, 파일에 기록하는 방식을 사용할 수 있다.
다음은 사용자 정의 예외와 로깅을 결합한 예시이다.
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
함수를 확장하여 파일에 기록하거나, 원격 서버에 예외 데이터를 보내는 등의 방식으로 사용할 수 있다. 이를 통해 프로그램에서 발생하는 예외를 추적하고 분석하는 데 도움을 줄 수 있다.
사용자 정의 예외와 다단계 예외 처리
경우에 따라 하나의 작업에서 여러 단계의 예외 처리가 필요할 수 있다. 예를 들어, 데이터베이스 연결, 네트워크 요청, 파일 읽기 등 다양한 작업이 한 함수 내에서 연속적으로 이루어질 때 각각의 작업에서 발생할 수 있는 예외를 따로따로 처리하거나, 상위 예외로 감싸서 처리하는 방식으로 구현할 수 있다.
다음은 여러 단계의 예외를 처리하는 예시이다.
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
블록에서 모든 예외를 처리할 수 있다.
이처럼, 다단계 예외 처리를 통해 예외 발생 시 세부적인 처리를 한 후에도 필요할 경우 상위 계층으로 예외를 전달할 수 있다.
사용자 정의 예외 클래스의 확장
복잡한 시스템에서는 다양한 예외를 관리하기 위해 사용자 정의 예외 클래스를 확장할 수 있다. 예를 들어, 예외를 세부적으로 분류하여 여러 예외 클래스를 정의하고, 상속을 통해 공통적인 예외 처리 로직을 재사용할 수 있다.
다음은 예외를 세분화하여 다양한 상황을 처리하는 예시이다.
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
을 정의하고, 각각 나이와 이메일에 대한 유효성 검사를 수행한다. 이를 통해 입력 값에 따라 더 구체적인 예외 메시지를 던지고 처리할 수 있다.