C#을 포함한 모든 .NET 프로그래밍 언어는
.NET Framework의 Exception 메커니즘에 따라 Exception을 처리한다.
.NET의 System.Exception
은 모든 Exception의 Base 클래스이며,
예외 처리는 이 Exception
객체를 기본으로 처리하게 된다.
만약 Exception이 발생하였는데 이를 프로그램 내에서 처리하지 않으면 (이를 Unhandled Exception
이라 부른다)
프로그램은 Crash하여 종료하게 된다.
C#에서는 try, catch, finally
라는 키워드를 사용하여 Exception을 핸들링하게 되며, 또한 throw
라는 C# 키워드를 통해 Exception을 만들어 던지거나
혹은 기존 Exception을 다시 던질 수 있다.
try
{
// 실행하고자 하는 문장들
DoSomething();
}
catch (Exception ex)
{
// 에러 처리/로깅 등
Log(ex);
throw;
}
try
블럭은 실제 실행하고 싶은 프로그램 명령문들을 갖는 블럭이다.
만약 여기서 어떤 에러가 발생하면, 이는 catch 문
에서 잡히게 된다.
catch문
은 모든 Exception을 일괄적으로 잡거나 혹은 특정 Exception을 선별하여 잡을 수 있다.
catch { ... }
or
catch (Exception ex) { ... }
이처럼
모든 Exception의 베이스 클래스인 System.Exception
를 잡으면 된다.
해당 Exception Type을 catch하면 된다.
즉, Argument와 관련된 Exception을 잡고 싶으면,
catch (ArgumentException ex) { ... }
와 같이 잡게된다.
catch
블럭은 하나 혹은 여러 개 일 수 있다.
여러 catch를 사용하는 이유는 각 Exception 유형에 따라 서로 다른 에러 핸들링을 하기 위함이다.
finally
는 Exception이 발생했던 발생하지 않았던 상관없이
마지막에 반드시 실행되는 블럭이다.
예를 들어, try
블럭에서 SQL Connection객체를 만든 경우,
finally
블럭에서 Connection 객체의 Close()
메서드를 호출하면,
에러 발생 여부와 상관없이 항상 Connection 객체가 닫히게 된다.
try
{
//실행 문장들
}
catch (ArgumentException ex)
{
// Argument 예외처리
}
catch (AccessViolationException ex)
{
// AccessViolation 예외처리
}
finally
{
// 마지막으로 실행하는 문장들
}
try
블럭에서 Exception이 발생하였는데 이를 catch 문
에서 잡었다면,
Exception은 이미 처리된 것으로 간주된다.
때때로 catch문
에서 기존의 Exception을 다시 상위 호출자로 보내고 싶을 때가 있는데,
이때 throw
를 사용한다.
throw 문
은 크게 3가지로 구별될 수 있다.
throw 문
다음에 catch
에서 전달받은 Exception 객체를 쓰는 경우이는
throw ex;
와 같이 catch (Exception ex)
에서 전달받은 아규먼트 ex
를 사용하는 경우이다.
이러한 throw 방식은 ex 에 담긴 예외 정보를 보전하지만,
Stack Trace 정보를 다시 리셋하기 때문에
throw문
이전의 콜스택(Call Stack) 정보를 유실하게 된다.
따라서, 일반적으로 이러한 방식은 사용하지 않는 것이 좋다 !!
throw 문
다음에 새 Exception 객체를 생성해서 전달하는 경우새로운 Exception 객체를 만들어 던지기 위해서는
throw new MyException();
와 같이 C#의 new
를 사용하여 새로운 Exception 객체를 만든 후,
이 객체를 throw
하면 된다.
이는 catch
에서 잡은 Exception을 Wrapping 하여
새로운 Exception을 전달할 때 사용하는데,
잘못 사용하면(InnerException을 포함하지 않으면) 기존 Exception 정보를 잃을 수 있다.
따라서, 이러한 방식을 사용하여 새로운 Exception 객체를 만들 때는
catch
에서 얻은 Exception 객체를 새 객체의 InnerException에 포함시켜
에러 정보를 보전하는 것이 좋다.
예를 들어,
throw new MyException(msg, ex);
와 같이 catch
에서 전달받은 ex
를 InnerException으로 전달하는 것이 좋다.
InnerException의 StackTrace 속성은 어느 라인에서 에러가 발생했는지를 알려주는데,
이는 에러가 다른 메서드에서 발생했을 때는 물론 동일 메서드에서 발생했다 하더라도
정확히 어떤 라인에서 에러가 발생했는지를 알게 해 준다.
throw 문
다음에 아무것도 없는 경우throw;
와 같이 뒤에 어떠한 Exception 객체 없이 그냥 throw문
만을 사용할 수 있는데,
이는 catch문에서 잡힌 Exception을 그대로 상위 호출 함수에게 전달하는 일(rethrow
)
을 한다.
즉, 에러를 발생시킨 콜스택 정보를 그대로 상위 호출 함수에 전달하려면 이렇게 throw;
를 호출하면 된다.
한가지 주목할 점은,
throw;
는 에러가 다른 메서드에서 발생했을 때throw문
과 동일한 메서드에서 에러가 발생했을 때아래 예제는
IndexOutOfRangeException
이 발생한 경우
MyException
이라는 사용자 정의 Exception 객체를 만들어 던지게 하는데,
catch
에서 잡은 ex 객체를 MyException의 InnerException에 포함시키고 있다.
FileNotFoundException
이 발생한 경우는 throw ex
와 같이 호출하고 있는데, 이는 throw ex
까지의 이전 콜스택을 제거하기 때문에
throw ex
이후부터의 콜스택 정보만을 전달하게 된다.
마지막으로 그 외의 모든 Exception의 경우는 발생한 Exception을 그대로 상위 호출 함수에게 전달하기 위해 throw;
문을 사용하고 있다.
try
{
// 실행 문장들
Step1();
Step2();
Step3();
}
catch(IndexOutOfRangeException ex)
{
// 새로운 Exception 생성하여 throw
throw new MyException("Invalid index", ex);
}
catch(FileNotFoundException ex)
{
bool success = Log(ex);
if (!success)
{
// 기존 Exception을 throw
throw ex;
}
}
catch(Exception ex)
{
Log(ex);
// 발생된 Exception을 그대로 호출자에 전달
throw;
}
아래 예제는 SQL Server에 연결하여
데이터베이스 내의 테이블, 뷰 등의 SQL Objects 수를 가져온 후 이를 화면에 뿌리는 코드이다.
만약 SQLException
타입의 에러가 발생하면
catch
블럭에서 잡아서 에러 메시지만 콘솔에 표시하고 Exception을 삼키게 된다.
마지막의 finally
블럭은
SqlConnection의 Close()
메서드를 실행하여 Connection을 닫는다.
물론 에러가 발생하지 않더라도 finally
블럭은 실행되며, 따라서 SQL Connection은 항상 닫히게 된다.
string connStr = "Data Source=(local);Integrated Security=true;";
string sql = "SELECT COUNT(1) FROM sys.objects";
SqlConnection conn = null;
try
{
conn = new SqlConnection(connStr);
conn.Open();
SqlCommand cmd = new SqlCommand(sql, conn);
object count = cmd.ExecuteScalar();
Console.WriteLine(count);
}
catch (SqlException ex)
{
Console.WriteLine(ex.Message);
}
finally
{
if (conn != null &&
conn.State == System.Data.ConnectionState.Open)
{
conn.Close();
}
}