최근 수정 시각 : 2024-11-15 01:43:54

예외 처리

프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all"<colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C(포인터 · 구조체 · size_t) · C++(자료형 · 클래스 · 이름공간 · 상수 표현식 · 특성) · C# · Java · Python(함수 · 모듈) · Kotlin · MATLAB · SQL · PHP · JavaScript · Haskell(모나드)
마크업 문법 HTML · CSS
개념과 용어 함수(인라인 함수 · 고차 함수 · 람다식) · 리터럴 · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 #! · == · === · deprecated · NaN · null · undefined · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}

1. 개요2. 예외 발생 방식3. 예외의 종류4. 프로그래밍 언어에서 예외 처리 방법

1. 개요

예외 처리(Exception Handling) 혹은 오류 처리(Trouble Shooting)란 실행 흐름상 오류가 발생했을 때 오류를 그대로 실행시키지 않고 오류에 대응하는 방법을 제시하는 개념이나 하드웨어 구조를 의미한다. 일반적으로 프로그래밍에서 프로그램이 실행 중 특정 문제가 발생했을 때 다른 처리 방식으로 흐름을 옮기는 개념으로 사용한다.

유명한 미싱노도 게임에 내장된 예외 처리 스크립트의 결과물이다. 정확히는 불완전한 예외 처리의 결과물인데, 대응 방법에 해당하는 데이터에 문제가 있어 일부 데이터에 영향을 끼치기 때문이다.

2. 예외 발생 방식

예외는 운영체제나 프로그램이 아닌 CPU에 의해 발생한다.

CPU는 적절하지 않는 명령(0으로 나누기, 잘못된 op code, 오버플로우/언더플로우 등)을 받으면 내부 인터럽트를 발생시킨다. OS는 CPU가 발생시킨 인터럽트를 처리하여 예외 정보(예외 종류, 예외가 발생한 주소 등)를 프로세스에 전달하여 처리하도록 한다.

일반적으로 프로세스에서 예외를 처리하지 않을 경우 기본 예외 처리기를 실행하여 그 즉시 종료된다. 만약 예외가 커널 모드에서 일어날 경우 그대로 커널 패닉으로 이어진다. 따라서 프로세스나 커널 모드 드라이버는 최대한 예외를 피하여 발생하면 적절하게 처리해줘야 한다.

참고로 OS가 예외를 처리하고 있던 도중에 다른 예외가 발생할 경우가 있다. 이를 Double Fault라고 부른다. 그리고 이를 처리하는 중에 다른 예외가 또 발생할 경우 Triple Fault가 발생하며 이는 CPU Reset(재부팅을 의미한다.)으로 이루어진다.

3. 예외의 종류

예외는 언어에 따라 다른데 예를 들어 Java의 경우 다음과 같은 예외가 있다.
  • NullPointerException: 흔히 초보 프로그래머들이 자주 하는 실수 중 하나다. 물론 숙련자도 가끔 실수하는 편. null 레퍼런스를 참조할 때 발생한다. 보통 try ~ catch로 예외처리하기 보단 if (a != null)처럼 해당 레퍼런스가 null인지 판단하는 방식으로 처리한다.
  • ClassCastException: 변환할 수 없는 타입으로 객체를 변환할 때 발생한다.
  • OutOfMemoryException: 메모리가 부족한 경우 발생한다.
  • IOException: 입출력 동작 중 인터럽트가 발생할때 나타난다. 자바에서 입출력 함수를 사용하는 경우 반드시 이 예외처리를 해주도록 되어있다.
  • NumberFormatException: 문자열이 나타내는 숫자와 일치하지 않는 타입의 숫자로 변환하는 경우 발생한다.
  • IllegalArgumentException: 잘못된 인자를 전달하는 경우 발생한다.
  • ArrayIndexOutOfBoundsException: 배열의 범위를 벗어나서 접근을 하는 경우 발생한다.
  • NoSuchMethodError: 런타임 시점에 존재하지 않는 메서드(method)를 호출할때 발생한다.

위 목록은 자주 나타나는 예외들만 명시해놓았으므로 위 목록 이외에도 수많은 예외가 있다.

개념적인 원인은 다음과 같다.
  • 잘못된 메모리 장치 접근
  • 불가능한 연산[1]
  • 자료형의 불일치[2][3]
  • 비논리적인 알고리즘
  • 컴퓨터 하드웨어의 손상
  • 운영체제 손상
  • 존재하지 않는 파일 호출

이 역시 위 목록은 가장 보편적인 예외들만 명시했으므로 위 목록 이외에도 많은 사례가 있다.

4. 프로그래밍 언어에서 예외 처리 방법

4.1. Java

#!syntax java
try (Scanner input = new Scanner(System.in)) {
    int temp = input.nextInt();
} catch (InputMismatchException e) {
    e.printStackTrace();
} catch (Exception e) {
    if (e instanceof IOException) {
        // TODO: IOException
    }
    e.printStackTrace();
} finally {
    // TODO: Finally
}

3번 줄에서 정수형 값을 받아 temp 변수에 넣고 있다. 3번 줄을 실행하면서 오류가 발생했다. 이 오류의 클래스에 해당되는 인스턴스가 생성된다. 인스턴스가 생성되면, catch문의 위에서부터 순서대로 해당 catch문에서 매개변수로 취하는 클래스의 Exception 인스턴스로 형변환 가능한지(정확히는 instanceof 연산자를 통해 형변환 가능한지) 판단한다. 해당 예외 클래스의 인스턴스로 형변환 가능하다고 판단되면 해당 catch문의 구현부에 삽입되어 있는 구문을 수행하고 try-catch 문 밖으로 빠져나간다. 그렇지 않다면 다음 catch문에 넘어간다. InputMismatchException 이 발생하여, 해당 인스턴스가 생성되었다면 5번 줄을 실행하고, 그 후 7번 줄을 실행한다. 만약 catch(Exception e) 문이 catch(InputMismatchException e) 문보다 위에 있었다면 catch(Exception e)문이 먼저 수행되고 try-catch문을 빠져나갔을 것이다.

Exception은 모든 오류를 통합한 형태로, 예외처리가 귀찮은 경우 Exception으로 모두 받아 처리하는 경우가 있는데, 프로그램의 견고함을 떨어뜨릴 수 있으니 되도록 상세한 예외의 명칭을 작성하는 것이 좋다.

다만 이것이 워낙 귀찮다보니 catch (Exception e) {throw e;} 처럼 받은 예외를 되던져서 떠넘겨 버리는 사람도 있다. 다만 이런 방법은 권장되는 방법은 아니다.

4.2. C#

#!syntax csharp
using System;
using System.IO;

string context;

try
{
    context = File.ReadAllText("test.txt");
} catch (FileNotFoundException)
{
    Console.WriteLine("'test.txt' 파일을 찾을 수 없습니다.");
} catch (IOException ex)
{
    Console.WriteLine("I/O 오류가 발생했습니다.\n오류 > {0}", ex.Message);
} finally
{
    if (context == null)
        context = "NO DATA";
}

Console.WriteLine(context);

C#는 Java와 다르게 catch에 Exception 클래스만 넣으면 된다.[4] 또한 finally가 있는데 try 안에 있는 코드 실행이 끝나거나 실행 중에 오류가 발생하여 예외처리를 끝마치면 finally가 호출된다. 이를 통해 간편하게 뒷처리가 가능하다. 이 점은 Java와 동일하다.

C#에서는 Exception 클래스를 상속하는 객체만 예외로 던지고 받을 수 있게 강제되어 있다. 만약 그 외의 변수나 객체를 던지려고 하면 컴파일러에서 거부한다. 그리고 Java와 같이 finally문이 추가되었는데 필수는 아니지만 예외를 받았건 받지 않았건 메모리 해제처럼 반드시 실행시켜야만 하는 명령이 있을 경우 사용한다.

4.3. C++

#!syntax cpp
#include <iostream>
int any_value = 999;
int temp;

try 
{   
    std::cin >> temp;
    if ( temp == 0 ) throw temp;
} 
catch (int exception) 
{
    std::cout << exception << " 으로 나눌 수 없음" << std::endl;
    return;
}
std::cout << any_value / temp << std::endl;

추가로, C++에는 예외 사양(exception specification)이라는 것이 존재한다. 한정자의 일부인데, 이것이 지정되면 함수는 지정된 타입의 예외밖에 던지지 못한다. 하지만 이 예외 사양이 지정되어도 강제성이 거의 없다시피하여 실제로 예외 사양이 추가된 목적을 달성하기 어렵다.

따라서 이 예외 사양은 deprecated 되었으며, 실제로 대부분의 현대 C++ 컴파일러는 이 예외 사양에 경고 내지 메세지를 준다. 하지만 예외 사양의 필요성은 많은 C++ 프로그래머 사이에서 인정되었으며 그 요구에 따라 C++11부터는 noexcept 한정자/연산자가 추가되었다.

이름에서 알 수 있듯이 예외 사양을 던질수 있음/없음으로 이분한 형태이다. 이 한정자가 적용된 함수는 예외를 던지지 못하며 던질시 즉시 std::terminate가 호출된다. 기본 핸들러를 가진 예외 사양과 같은 동작인데, 다른 점은 noexcept는 stack unwind를 할 수도 있고 안할수도 있다는 것이다. 이는 컴파일러 최적화에 큰 도움을 준다. 이걸 사용하면 강한 예외 보장성을 쉽게 제공(이를 제공하지 않으면 C++11에 추가된 move semantics를 제대로 사용할 수 없다.) 할 수 있고, 최적화에도 도움이 된다.

아직 이 한정자는 타입 시스템의 일부가 아니며, C++17부터 함수 타입 시스템의 일부가 될 예정이다. noexcept 연산자는 피연산자가 noexcept specified인지 아닌지를 컴파일 타임에 확인하고, bool 형식으로 반환한다.

4.4. Python

#!syntax python
try:
    # 예외 발생시키기
    raise BaseException
except BaseException as e:
    # 예외가 발생했을 때만 수행할 작업
else:
    # 예외가 발생하지 않았을 때만 수행할 작업
finally:
    # 반드시 수행할 작업

여기 서술된 다른 많은 언어들과 다르게, 파이썬은 예외를 발생시킬 때 throw가 아닌 raise를 사용한다.

4.5. JavaScript

#!syntax javascript
try {
    /* 예외 던지기 */
    throw Error('내용');
} catch(e) {  /* e 말고 다른 이름도 가능하다. 다만 e를 안 쓴다고 catch()로 쓰면 오류가 나므로 주의. */
    /* 오류 시 수행 */
    console.log(e.stack); /* typescript일 경우 e: any로 지정해주지 않으면 unkown이 되어 내부 속성 호출이 불가하다. */
} finally {
    /* 반드시 수행할 작업 */
}

4.6. PHP

#!syntax php
try {
    // 예외 던지기
    throw new Exception();
}
catch (Exception $e) {
    // 수행할 작업
}
finally {
    // 반드시 수행할 작업
}

위의 C#, Java와 비슷하다. 처음에는 예외 처리를 지원하지 않았다가 5.0에서 try~catch 문이, 5.5에서 finally 문이 추가되어 오늘에 이른다.

4.7. Haskell

하스켈에서는 아래와 같이 함수 catch로 예외 처리를 한다.[5]
main = toTry `catch` handler

toTry = do
  (fileName:_) <- getArgs
  contents <- readFile fileName
  putStrLn ("The file has " ++ show (length (lines contents)) ++ " lines!")

handler e = putStrLn "Whoops, had some trouble!"
하스켈에서 catch는 별도 문법이 아니라 그냥 함수일 뿐이다.


파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 문서의 r28에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r28 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)


[1] 0으로 나누기가 대표적이다.[2] 정수형만 처리 가능한 값에 문자를 넣는 경우 등[3] 다만 정적 타입 언어에서는 컴파일 단계에서 전부 잡아낼 수 있다.[4] 이때 해당 catch 블록의 예외 객체는 Visual Studio 디버거에서는 $exception으로 접근할 수 있다.[5] https://learnyouahaskell.github.io/input-and-output.html