예외 처리하기
이번에는 예외(오류)가 발생했을 때 처리하는 예외 처리 구문에 대해서 학습합니다.
> // 예외 처리(Exception Handling): try-catch-finally와 throw를 사용하여 예외 처리하기
예외(Exception)와 예외 처리(Exception Handling)
C# 프로그래밍에서 예외(Exception)는 프로그램이 실행되는 동안 발생하는 오류(에러, Error)를 의미합니다. 잘못 작성된 코드 등으로 발생된 예외는 프로그램을 강제적으로 종료하거나 잘못된 결과를 나타내는 식으로 발생합니다. 이러한 예외에 대한 대비로 예외 처리(Exception Handling)를 해주어야 합니다.
에러는 또 다른 용어로 버그(Bug)로도 부릅니다.
오류(Error)의 종류
오류(에러)의 종류는 문법(컴파일) 오류, 런타임(실행) 오류, 논리(알고리즘) 오류 등으로 분류됩니다.
- 문법 오류(Syntax Error): 잘못된 명령어를 입력했거나, 타이핑의 실수로 발생되는 오류입니다. 문법 오류는 컴파일 오류로도 불리며 대부분 C# 컴파일러가 잡아줍니다. 문법 오류를 방지하려면 많은 예제를 접해가면서 C#의 기초문법을 확실하게 이해하여야 할 것입니다.
- 런타임 오류(Runtime Error): 런타임 오류는 프로그램 작성 후 실행할 때 발생하는 오류입니다. 컴파일 과정에서는 발생하지 않고 실행시에 발생하기에 많은 테스트를 진행하면서 잡을 수 있습니다.
- 알고리즘 오류(Logic Error): 주어진 문제에 대한 잘못된 해석으로 잘못된 결과를 초래하는 에러를 알고리즘 오류 또는 로직 오류라 합니다. 문법 오류나 런타임 오류는 쉽게 발견해 낼 수 있지만, 알고리즘 오류는 처리 결과가 틀리게 나왔는데도 알 수 없는 경우가 많기 때문에 이 알고리즘 오류를 해결하기가 가장 어렵습니다. 알고리즘 오류를 해결하기 위해서는 많은 책과 강의 그리고 오픈 소스 등을 통해서 코드 분석 및 많은 코드를 직접 만들어 보는 등 오류를 찾아내는 능력을 키워야 합니다.
try ~ catch ~ finally
구문
C#에서는 예외 처리를 위해서 try
, catch
, finally
와 같은 3가지의 키워드가 준비되어 있습니다. try
블록은 혹시 모를 예외가 발생할 만한 구문을 묶어주고, catch
블록은 예외가 발생했을 때 처리해야 하는 구문을 묶어주며, finally
블록은 예외가 발생 또는 발생하지 않아도 마무리 관련 처리해야 할 구문을 묶어주는데 사용됩니다.
try
{
// 예외가 발생할 만한 코드를 작성
}
catch
{
// 예외가 발생할 때 처리해야 할 코드 블록
}
finally
{
// 예외가 발생하거나 정상일 때 모두 처리해야 할 코드 블록
}
try
와 catch
구문으로 예외 처리하기
C#에서는 try
, catch
, finally와
같은 키워드를 사용하여 예외가 발생했을 때 그에 대한 처리를 담당하는 구문을 작성할 수 있습니다. 에러 발생할 때 비정상적으로 종료되는 것을 막고 정상 종료시키려면 try catch
구문을 사용하면 됩니다.
코드: TryCatch.cs
using System;
class TryCatch
{
static void Main()
{
try
{
int[] arr = new int[2];
arr[100] = 1234; // 예외(에러) 발생: System.IndexOutOfRangeException
}
catch
{
Console.WriteLine("에러가 발생했습니다.");
}
}
}
에러가 발생했습니다.
정수형 배열인 arr
은 2개의 요소를 담을 수 있습니다. 그런데 arr[100]
형태로 없는 인덱스에 값을 입력하면 예외가 발생됩니다. try
로 묶인 코드 내에서 에러가 발생하면 catch
절이 실행됩니다.
위 코드는 try
절에서 일부러 에러를 발생시켜 catch
절이 실행되는 것을 확인할 수 있습니다.
Exception
클래스로 예외 처리하기
닷넷에서 모든 예외에 대해서 처리할 주요 기능을 담아 놓은 클래스가 Exception
클래스입니다. Exception
클래스 외에 상당히 많은 예외 관련 클래스가 있지만, 처음 C# 개발자로 들어서기에는 Exception
클래스만을 사용해도 무관할 듯합니다. Exception
클래스의 주요 속성에는 Message
속성이 있는데 현재 예외에 대한 설명을 출력합니다.
예외 처리 구문에 Exception
클래스 사용하기
Exception
클래스를 사용하여 에러 내용을 출력하는 내용을 살펴보겠습니다.
코드: ExceptionDemo.cs
using System;
class ExceptionDemo
{
static void Main()
{
try
{
int[] arr = new int[2];
arr[100] = 1234;
}
catch (Exception ex) // ex 변수에는 예외에 대한 상세 정보가 담김
{
Console.WriteLine(ex.Message);
}
}
}
인덱스가 배열 범위를 벗어났습니다.
TryCatch.cs의 내용과 동일한데 catch (Exception ex)
형태로 catch
절의 모양을 변경하였습니다. 이렇게 하면 예외의 내용을 Exception
클래스 형식의 변수인 ex
에 담기게 됩니다. Exception
클래스는 Message
속성을 통해서 예외에 대한 내용을 알려줍니다.
우리가 일부러 배열의 인덱스의 범위를 벗어난 구문을 만들었기에 이에 대한 공식 에러 메시지를 Exception
클래스로부터 알 수 있습니다.
catch
절의 모양은 일반적으로 다음처럼 e
또는 ex
변수로 사용합니다.
catch (Exception e)
catch (Exception ex)
FormatException
클래스 형식의 예외 받아 처리하기
Exception
클래스와 마찬가지로 FormatException
과 같은 클래스들은 각각 고유의 예외 발생시 해당 예외에 대한 정보를 담고 있습니다.
다음에서 inputNumber
에 정수 문자열이 아닌 실수 문자열을 입력했을 때 Convert.ToInt32()
메서드는 FormatException
형태의 에러를 발생시킵니다.
코드: FormatExceptionDemo.cs
using System;
using static System.Console;
class FormatExceptionDemo
{
static void Main()
{
string inputNumber = "3.14";
int number = 0;
try
{
number = Convert.ToInt32(inputNumber);
WriteLine($"입력한 값: {number}");
}
catch (FormatException fe)
{
WriteLine($"에러 발생: {fe.Message}");
WriteLine($"{inputNumber}는 정수여야 합니다.");
}
}
}
에러 발생: 입력 문자열의 형식이 잘못되었습니다.
3.14는 정수여야 합니다.
위 코드 실행 결과 잘못된 값이 입력되어 FormatException
예외가 발생하였고 이를 catch
절에서 잡아서 사용한 예입니다.
간헐적으로 발생되는 예외 처리하기
프로그램 컴파일 후 실행할 때인 런타임시에 try
절에서 발생한 예외를 catch
절에서 처리하는 내용을 살펴보겠습니다.
다음 프로그램은 DateTime.Now.Second
API를 통해서 현재 프로그램을 실행하는 시점의 초를 구해옵니다. 만약 구해진 값이 짝수이면 (now % 2)
코드 부분이 0
으로 되어, 2 / 0;
의 모양이 됩니다. 모든 수는 0
으로 나눌 수 없기에 이 부분에서 에러가 발생합니다. 에러가 발생하면 catch
절이 실행되고 프로그램을 정상 종료합니다. 만약, 구해진 초가 홀수라면 정상적으로 메시지가 출력되고 프로그램이 정상 종료됩니다.
코드: TryCatchDemo.cs
using System;
class TryCatchDemo
{
static void Main()
{
try
{
int now = DateTime.Now.Second;
Console.WriteLine($"[0] 현재 초: {now}");
//[!] 실행시간이 짝수이면 0으로 나누기에 에러가 발생
int result = 2 / (now % 2);
Console.WriteLine("[1] 홀수 초에서는 정상 처리");
}
catch
{
Console.WriteLine("[2] 짝수 초에서는 런타임 에러 발생");
}
}
}
실행시키는 시점의 초(Second)의 값이 홀수이면 다음과 같이 try
절의 내용이 정상 출력됩니다.
[0] 현재 초: 25
[1] 홀수 초에서는 정상 처리
실행시키는 시점의 초의 값이 짝수이면 에러가 발생하기에 catch
절의 내용이 출력됩니다.
[0] 현재 초: 8
[2] 짝수 초에서는 런타임 에러 발생
이처럼 예외 처리 구문을 사용하면 런타임시에 발생할지 모르는 예외에 대해서 정상적으로 예외 처리를 할 수 있습니다.
[실습] 예외 처리 연습하기
소개
이번에는 예외 처리를 단계별로 살펴보겠습니다. 참고로, 이번 실습의 소스는 책 소스의 DotNet - 28_Exception – TryCatchFinallyDemo 폴더에 위치해 있습니다.
따라하기 0: TryCatchFinallyDemo 콘솔 응용 프로그램 프로젝트 생성
(1) TryCatchFinallyDemo 이름으로 콘솔 응용 프로그램 프로젝트를 생성합니다. 기본 작성된 Program.cs 파일을 제거합니다. 이곳에는 총 4개의 cs 파일과 각각의 cs 파일에는 Main()
메서드를 두도록 하겠습니다. Visual Studio를 사용하여 .NET Framework 기반으로 프로젝트를 생성하면 프로젝트에 여러 개의 Main()
메서드를 생성할 수 있습니다. 다만, 프로젝트 속성 창에서 반드시 시작 개체를 지정해야만 합니다. 원칙적으로는 한 프로젝트에는 하나의 Main()
메서드만 있어야 하지만, Visual Studio에서는 이를 테스트할 수 있도록 클래스 이름을 다르게 만든 여러 Main()
메서드에서 시작 개체 하나를 선택할 수 있습니다.
따라하기 1: 에러 안 나는 코드 작성하기
(1) 프로젝트에 TryCatchFinallyDemo1.cs 파일을 생성합니다.
코드: TryCatchFinallyDemo1.cs
using System;
class TryCatchFinallyDemo1
{
static void Main()
{
int x = 5;
int y = 3;
int r;
r = x / y;
Console.WriteLine($"{x} / {y} = {r}");
}
}
5 / 3 = 1
5 / 3
과 같은 식은 프로그램 내에서 정상적으로 실행됩니다. 현재 코드는 아무런 문제없이 잘 실행됩니다.
참고: 시작 개체 설정하기
하나의 프로젝트에 클래스 이름이 다른 여러 개의 Main()
메서드 코드를 넣을 수 있습니다. 이때 원하는 Main()
메서드를 시작 개체로 설정하려면 솔루션 탐색기의 프로젝트에 마우스 오른쪽 버튼 클릭 후 나타나는 상황 메뉴에서 속성을 선택하여 다음 그림과 같이 시작 개체를 클래스 이름으로 변경합니다.
그림: 시작 개체 변경하기
.NET Framework 환경이 아닌 경우에는 솔루션 탐색기의 프로젝트에 마우스 오른쪽 버튼 클릭 후 나타나는 상황 메뉴에서 프로젝트 파일 편 집 또는 프로젝트를 더블 클릭합니다. 이곳에서 StartObject 항목의 값을 실행하고자 하는 클래스의 이름으로 넣어주면 됩니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<StartupObject>TryCatchFinallyDemo1</StartupObject>
</PropertyGroup>
</Project>
따라하기 2: 무조건 에러 발생시키기
프로젝트에 TryCatchFinallyDemo2.cs 파일을 생성하고 다음 예제 코드를 작성한 후 프로젝트 속성 창에서 시작 개체를 TryCatchFinallyDemo2로 설정 후 실행합니다.
코드: TryCatchFinallyDemo2.cs
using System;
class TryCatchFinallyDemo2
{
static void Main()
{
int x = 5;
int y = 0;
int r;
r = x / y; // 0으로 나누기 시도
Console.WriteLine($"{x} / {y} = {r}");
}
}
처리되지 않은 예외: System.DivideByZeroException: 0으로 나누려 했습니다.
위치: TryCatchFinallyDemo2.Main()
파일 C:\C#\TryCatchFinallyDemo\TryCatchFinallyDemo\TryCatchFinallyDemo2.cs:줄 11
모든 수는 0
으로 나눌 수 없습니다. 런타임시에 y
의 값이 0
이므로 0
으로 나누어지기에 무조건 예외가 발생합니다.
따라하기 3: try~catch~finally
구문으로 예외 처리하기
프로젝트에 TryCatchFinallyDemo3.cs 파일을 생성한 후 다음 코드를 작성한 다음에 프로젝트 속성 창에서 시작 개체를 TryCatchFinallyDemo3으로 설정 후 실행합니다.
코드: TryCatchFinallyDemo3.cs
using static System.Console;
class TryCatchFinallyDemo3
{
static void Main()
{
int x = 5;
int y = 0;
int r;
try // 예외가 발생할만한 구문이 들어오는 곳
{
r = x / y; // 0으로 나누기 시도
WriteLine($"{x} / {y} = {r}");
}
catch // try 절에서 예외가 발생하면 실행
{
WriteLine("예외가 발생했습니다.");
}
finally // 예외가 발생하던 안하던 실행
{
WriteLine("프로그램을 종료합니다.");
}
}
}
예외가 발생했습니다.
프로그램을 종료합니다.
try
절에서 에러가 발생하면 catch
절에 실행됩니다. 특정 에러에 대한 정보가 필요없이 에러가 발생했을 때 이에 대한 에러 처리를 위한 catch
절을 실행할 때에는 이와 같은 모양으로 사용합니다.
따라하기 4: Exception
클래스로 예외 정보 얻기
프로젝트에 TryCatchFinallyDemo4.cs 파일을 생성하고 다음 코드를 작성한 후에 프로젝트 속성 창에서 시작 개체를 TryCatchFinallyDemo4으로 설정 후 실행합니다.
***코드: ***
// TryCatchFinallyDemo4.cs
using System;
using static System.Console;
class TryCatchFinallyDemo4
{
static void Main()
{
int x = 5;
int y = 0;
int r;
try // 예외가 발생할만한 구문이 들어오는 곳
{
r = x / y; // 0으로 나누기 시도
WriteLine($"{x} / {y} = {r}");
}
catch (Exception ex)
{
WriteLine($"예외 발생: {ex.Message}");
}
finally // 예외가 발생하던 안하던 실행
{
WriteLine("프로그램을 종료합니다.");
}
}
}
예외 발생: 0으로 나누려 했습니다.
프로그램을 종료합니다.
catch
절에서 예외에 대한 좀 더 자세한 정보를 얻고자 할 때에는 Exception
클래스의 개체를 받아서 사용합니다.
마무리
C#에서 제공하는 예외 처리 구문인 try~catch~finally
구문은 예외 발생시 비정상 종료되는 프로그램을 정상 종료되는 프로그램으로 변경합니다.
throw
구문으로 직접 예외 발생시키기
C#에서 throw
구문은 그 이름에서도 알 수 있듯이 무엇인가를 던지는 구문입니다. 여기서 무엇인가는 바로 인위적으로 예외(에러)를 발생시킴을 말합니다.
try~catch~finally
절과 함께 예외 처리시 throw
구문을 사용할 수 있는데 throw
는 무조건 특정 예외를 발생시킵니다. throw
키워드 뒤에 특정 예외 관련 클래스(Exception
, ArgumentException
, …)의 인스턴스를 넘겨주면 해당 예외를 직접 발생시킵니다.
코드: ThrowNote.cs
> throw new Exception();
'System.Exception' 형식의 예외가 Throw되었습니다.
+ <Initialize>.MoveNext()
> throw new ArgumentException();
값이 예상 범위를 벗어났습니다.
throw
구문으로 무작정 에러를 발생시키기
이번에는 throw
절을 사용해보겠습니다.
코드: TryFinallyDemo.cs
using System;
class TryFinallyDemo
{
static void Main()
{
Console.WriteLine("[1] 시작");
//[!] 에러가 발생할만한 코드 들어오는 영역
try
{
Console.WriteLine("[2] 실행");
throw new Exception(); // 무작정 에러 발생
}
//[!] try절에서 에러가 발생하던 안하던 반드시 실행하는 영역(마무리 영역)
finally
{
Console.WriteLine("[3] 종료");
}
}
}
[1] 시작
[2] 실행
처리되지 않은 예외: System.Exception: 'System.Exception' 형식의 예외가 Throw되었습니다.
위치: TryFinallyDemo.Main()
파일 C:\C#\TryFinallyDemo\TryFinallyDemo\TryFinallyDemo.cs:줄 13
[3] 종료
다음 구문에 의해서 try
절에서 무조건 에러가 발생됩니다.
> throw new Exception();
위 구문은 다음 구문의 줄임 형태입니다.
> Exception ex = new Exception();
> throw ex;
예외 처리 관련 키워드인 try
, catch
, finally
, throw
를 모두 사용해보겠습니다.
코드: ExceptionHandling.cs
using System;
class ExceptionHandling
{
static void Main()
{
int a = 3;
int b = 0;
try
{
//[1] b가 0이므로 런타임 에러 발생
a = a / b;
}
catch (Exception ex)
{
Console.WriteLine($"예외(에러)가 발생됨: {ex.Message}");
}
finally
{
Console.WriteLine("try 구문을 정상 종료합니다.");
}
try
{
//[2] Exception 클래스에 에러 메시지 지정하여 무조건 에러 발생
throw new Exception("내가 만든 에러");
}
catch (Exception e)
{
Console.WriteLine($"예외(에러)가 발생됨: {e.Message}");
}
finally
{
Console.WriteLine("try 구문을 정상 종료합니다.");
}
}
}
예외(에러)가 발생됨: 0으로 나누려 했습니다.
try 구문을 정상 종료합니다.
예외(에러)가 발생됨: 내가 만든 에러
try 구문을 정상 종료합니다.
예외 처리를 위해서는 try~catch~finally
가 하나의 쌍으로 사용됩니다. 코드로는 다루지는 않았지만, catch
절은 Exception
클래스와 같은 예외 형식을 다르게 하여 여러 번 지정할 수 있습니다. 그리고 특정 경우에 무조건 예외를 발생시키고자 할 때에는 throw
절을 사용하면 됩니다.
장 요약
프로그래밍에서 예외 처리는 중요한 부분입니다. 프로그램 코드를 좀 더 안정화하기 위해서는 기본 코드 작성 후 예외 처리 코드를 추가하는 식으로 점진적으로 향상시켜 가는 걸 추천합니다.
이 강의에서는 코드 사용을 아끼기 위해서 앞으로 나오는 코드에서는 예외 처리 코드 부분은 많이 사용하지 않지만, 독자 스스로 필요하다고 판단되는 부분은 try~catch~finally
절로 묶어서 관리하기 바랍니다.