예외 처리 기초
try-catch-finally 로 잘못된 상황을 다룹니다.
잘못될 수 있는 일 다루기
문법이 맞고 컴파일도 되었는데 실행 중에 무너지는 일이 있습니다. 숫자가 아닌 글을 숫자로 바꾸려 하거나, 배열에 없는 자리를 건드리거나, 열려던 파일이 없을 때입니다. 이런 일이 생기면 C#은 예외(Exception)를 던집니다.
던져진 예외를 아무도 받지 않으면 프로그램은 그 자리에서 멈춥니다.
try-catch 는 그것을 받아 내는 그물입니다. 받아 두면
무너지는 대신 어떻게 할지 정할 수 있습니다.
try— 잘못될 수 있는 코드를 여기 둡니다.catch— 문제가 생겼을 때 할 일을 적습니다.finally— 잘되든 잘못되든 반드시 할 일을 적습니다.
멈추지 않고 넘어가기
아래 코드는 브라우저 안에서 실제로 실행됩니다. 고쳐서 눌러 보셔도 됩니다.
string input = "abc"; try { int n = int.Parse(input); Console.WriteLine(n); } catch (FormatException) { Console.WriteLine("숫자가 아닙니다."); } Console.WriteLine("계속 실행됩니다.");
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
눈여겨볼 것은 마지막 줄이 실행되었다는 점입니다.
catch 가 없었다면 프로그램은
int.Parse 에서 멈추고 그 아래로 내려오지 못했을 것입니다.
try 안에서 문제가 생기면 그 줄에서 곧바로
catch 로 건너뜁니다. 위 코드에서
Console.WriteLine(n) 이 실행되지 않은 것은 그 때문입니다.
자주 만나는 예외
| 예외 | 언제 나는가 | 예시 |
|---|---|---|
| FormatException | 숫자로 바꿀 수 없는 글을 바꾸려 할 때 | int.Parse("abc") |
| IndexOutOfRangeException | 배열에 없는 자리를 건드릴 때 | arr[5] (크기가 3) |
| NullReferenceException | 값이 없는 것을 통해 무언가 할 때 | name.Trim() (name 이 null) |
| DivideByZeroException | 정수를 0으로 나눌 때 | 10 / 0 |
| ArgumentException | 넘긴 값이 규칙에 맞지 않을 때 | 메서드가 스스로 던집니다 |
10 / 0 은 예외가 나지만
10.0 / 0 은 나지 않고
무한대가 됩니다. 정수와 실수가 0을 다르게 다루기
때문입니다. 1.4에서 본 정수 나눗셈과 같은 결의 이야기입니다.
여러 가지를 받을 때는 좁은 것부터
catch 는 여러 개를 이어 둘 수 있습니다. 이때
위에서부터 차례로 맞는 것을 찾아 하나만 실행합니다. 그래서
좁은 것(구체적인 예외)을 먼저, 넓은 것을 나중에 두어야 합니다.
try { int n = int.Parse(input); } catch (FormatException) { Console.WriteLine("숫자가 아닙니다."); } catch (Exception ex) { // 그 밖의 모든 것 Console.WriteLine(ex.Message); }
순서를 뒤집으면 Exception 이 모든 것을 먼저 받아
버려 아래가 쓸모없어집니다. 다행히 이 경우는 컴파일 오류로 걸리므로
실행 전에 알 수 있습니다.
ex.Message 는 무엇이 잘못됐는지 적힌 글입니다. 무슨 일이
있었는지 남길 때 사용합니다.
반드시 해야 할 일은 finally 에
파일이나 연결처럼 다 쓰고 반드시 닫아야 하는 것이 있습니다.
finally 는 잘되든 예외가 나든 실행되므로 그 자리에
알맞습니다. try 안에서 값을 돌려주더라도
finally 는 실행된 뒤에 돌아갑니다.
try { // 파일을 열고 읽습니다 } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { Console.WriteLine("여기는 어떤 경우에도 지납니다."); }
예외로 다룰 일과 아닌 일
예외는 뜻밖의 일을 위한 것입니다. 사람이 숫자가 아닌 것을 적는 일은
뜻밖이 아니라 늘 있는 일이므로, 그때는 예외를 던지고 받는 대신 1.4에서 본
TryParse 로 미리 걸러 내는 편이 낫습니다. 예외를 던지고
받는 데는 값이 들기도 하고, 무엇보다 코드가 말하는 바가 달라집니다.
try { int n = int.Parse(input); Console.WriteLine(n); } catch (FormatException) { Console.WriteLine("숫자가 아닙니다."); }
if (int.TryParse(input, out int n)) { Console.WriteLine(n); } else { Console.WriteLine("숫자가 아닙니다."); }
그리고 받아 놓고 아무것도 하지 않는 것은 가장 나쁩니다. 아래처럼 비워 두면 문제가 조용히 묻혀, 나중에 엉뚱한 곳에서 원인을 찾게 됩니다. 적어도 무슨 일이 있었는지는 남기세요.
try { // ... } catch (Exception) { // 아무것도 하지 않음 — 문제가 사라진 것이 아니라 감춰집니다 }
이 문법, 몇 버전부터 사용할 수 있나요
- 1.0try · catch · finally
- 6.0when — 조건이 맞을 때만 받기
- 8.0using 선언 — 닫는 일을 맡기기
try-catch-finally 는 처음부터 있었고 지금까지
문법이 바뀌지 않았습니다. 뒤에 더해진 것은 받는 조건을 좁히는
when(C# 6.0)과, 다 쓴 것을 닫는 일을 아예
맡겨 버리는 using 선언(C# 8.0)입니다.
using 을 사용하면 finally 에
닫는 코드를 손수 적지 않아도 되는데, 그 이야기는 3부의 파일 다루기에서
다시 만납니다.
직접 해보기
int a = 10, b = 0; 을 두고
a / b 를 시도하되, 0으로 나누는 경우에는
0으로 나눌 수 없습니다. 를 출력하고 프로그램이 멈추지
않게 해보세요.
string[] inputs = { "10", "abc", "7" }; 를 탐색하면서
숫자인 것만 더하고, 숫자가 아닌 것은 건너뛰어 합계를 출력해보세요. 결과는
17 이 됩니다.
- 예외를 받지 않으면 프로그램은 그 자리에서 멈춥니다.
catch는 좁은 것부터 적습니다. 넓은 것을 위에 두면 아래가 쓸모없어집니다.finally는 어떤 경우에도 지납니다. 닫아야 할 것을 여기서 닫습니다.- 늘 있는 일은 예외로 다루지 않습니다. 미리 걸러 내는 쪽이 낫습니다.
C# 버전별 변경 이력 에서 각 버전이 무엇을 더했는지 볼 수 있습니다.