예외

예외 (Exceptions)

PHP에도 다른 프로그래밍 언어와 비슷한 예외 모델이 있어요. PHP 안에서 예외를 throw로 던질 수도 있고 catch로 잡을 수도 있죠. 잠재적인 예외를 잡기 쉽도록 코드를 try 블록으로 감쌀 수 있어요. try 하나에는 반드시 catchfinally 블록이 하나 이상 대응해야 해요.

참고: Exception 클래스와 함께 보세요.

예외가 던져졌는데 현재 함수 스코프에 catch 블록이 없으면, 그 예외는 호출 스택을 타고 거슬러 올라가며(bubble up) 일치하는 catch 블록을 만날 때까지 호출한 함수로 전파돼요. 그 과정에서 만나는 finally 블록은 모두 실행되고요. 호출 스택이 전역 스코프까지 풀렸는데도 일치하는 catch 블록을 못 만나면, 전역 예외 처리기가 설정돼 있지 않은 한 프로그램은 치명적 오류(fatal error)로 종료돼요.

던져지는 객체는 반드시 Throwableinstanceof여야 해요. 그렇지 않은 객체를 던지려 하면 PHP 치명적 오류가 발생해요.

PHP 8.0.0부터 throw 키워드는 표현식(expression)이라서 어떤 표현식 자리에서든 쓸 수 있어요. 이전 버전에서는 문(statement)이라서 단독 줄에 써야 했죠.

catch

catch 블록은 던져진 예외에 어떻게 응답할지를 정의해요. catch 블록은 처리할 수 있는 예외나 오류의 타입을 하나 이상 지정하고, 선택적으로 예외를 할당할 변수를 지정할 수 있어요. (이 변수는 PHP 8.0.0 이전에는 필수였어요.) 던져진 예외나 오류가 만나는 catch 블록 중에서, 그 객체의 타입과 일치하는 첫 번째 블록이 그 객체를 처리해요.

catch 블록을 여러 개 쓰면 서로 다른 클래스의 예외를 각각 잡을 수 있어요. 정상 실행(try 블록 안에서 예외가 발생하지 않은 경우)은 순서대로 정의된 마지막 catch 블록 다음부터 이어져요. catch 블록 안에서도 예외를 던지거나 다시 던질(re-thrown) 수 있는데, 그러지 않으면 실행은 발동된 catch 블록 다음부터 계속돼요.

예외가 던져지면 그 문장 다음의 코드는 실행되지 않고, PHP는 첫 번째로 일치하는 catch 블록을 찾아요. 예외가 잡히지 않으면 set_exception_handler()로 처리기를 정의하지 않은 한 "Uncaught Exception ..." 메시지와 함께 PHP 치명적 오류가 발생해요.

PHP 7.1.0부터 catch 블록은 파이프(|) 문자를 사용해 예외를 여러 개 지정할 수 있어요. 서로 다른 클래스 계층의 예외를 같은 방식으로 처리할 때 유용하죠.

PHP 8.0.0부터 잡은 예외의 변수 이름은 선택 사항이에요. 지정하지 않으면 catch 블록은 여전히 실행되지만 던져진 객체에는 접근할 수 없어요.

finally

finally 블록은 catch 블록 뒤나 그 대신에 지정할 수 있어요. finally 블록 안의 코드는 예외가 던져졌는지와 무관하게 trycatch 블록이 끝난 뒤, 정상 실행이 재개되기 전에 항상 실행돼요.

주목할 상호작용이 하나 있는데, 바로 finally 블록과 return 문 사이예요. trycatch 블록 안에서 return 문을 만나도 finally 블록은 여전히 실행돼요. 게다가 return 문은 만난 시점에 평가되지만, 그 결과는 finally 블록이 실행된 뒤에 반환돼요. 그리고 finally 블록에도 return 문이 있으면, finally 블록의 값이 반환돼요.

또 하나 주목할 상호작용은 try 블록 안에서 던져진 예외와 finally 블록 안에서 던져진 예외 사이의 관계예요. 두 블록이 모두 예외를 던지면 finally 블록에서 던져진 예외가 전파되고, try 블록에서 던져진 예외는 그 이전 예외(previous exception)로 사용돼요.

전역 예외 처리기 (Global exception handler)

예외가 전역 스코프까지 거슬러 올라가면, 설정되어 있다면 전역 예외 처리기가 잡을 수 있어요. set_exception_handler() 함수로 다른 블록이 호출되지 않을 때 catch 블록 대신 호출될 함수를 지정할 수 있어요. 그 효과는 본질적으로 프로그램 전체를 try-catch 블록으로 감싸고 그 함수를 catch로 둔 것과 같아요.

참고 (Notes)

참고: 내장 PHP 함수는 주로 오류 보고를 사용하고, 최신 객체 지향 확장만 예외를 사용해요. 하지만 오류는 ErrorException으로 쉽게 예외로 바꿀 수 있어요. 다만 이 기법은 치명적이지 않은 오류에서만 동작해요.

예제 #1 오류 보고를 예외로 변환하기

<?php
function exceptions_error_handler($severity, $message, $filename, $lineno) {
    throw new ErrorException($message, 0, $severity, $filename, $lineno);
}

set_error_handler('exceptions_error_handler');

표준 PHP 라이브러리(SPL)는 꽤 많은 내장 예외를 제공해요.

예제 (Examples)

예제 #2 예외 던지기

<?php
function inverse($x) {
    if (!$x) {
        throw new Exception('Division by zero.');
    }
    return 1/$x;
}

try {
    echo inverse(5) . "\n";
    echo inverse(0) . "\n";
} catch (Exception $e) {
    echo 'Caught exception: ',  $e->getMessage(), "\n";
}

// Continue execution
echo "Hello World\n";

위 예제는 다음과 같이 출력해요:

0.2
Caught exception: Division by zero.
Hello World

예제 #3 finally 블록을 사용한 예외 처리

<?php
function inverse($x) {
    if (!$x) {
        throw new Exception('Division by zero.');
    }
    return 1/$x;
}

try {
    echo inverse(5) . "\n";
} catch (Exception $e) {
    echo 'Caught exception: ',  $e->getMessage(), "\n";
} finally {
    echo "First finally.\n";
}

try {
    echo inverse(0) . "\n";
} catch (Exception $e) {
    echo 'Caught exception: ',  $e->getMessage(), "\n";
} finally {
    echo "Second finally.\n";
}

// Continue execution
echo "Hello World\n";

위 예제는 다음과 같이 출력해요:

0.2
First finally.
Caught exception: Division by zero.
Second finally.
Hello World

예제 #4 finally 블록과 return 사이의 상호작용

<?php

function test() {
    try {
        throw new Exception('foo');
    } catch (Exception $e) {
        return 'catch';
    } finally {
        return 'finally';
    }
}

echo test();

위 예제는 다음과 같이 출력해요:

finally

예제 #5 중첩 예외 (Nested Exception)

<?php

class MyException extends Exception { }

class Test {
    public function testing() {
        try {
            try {
                throw new MyException('foo!');
            } catch (MyException $e) {
                // rethrow it
                throw $e;
            }
        } catch (Exception $e) {
            var_dump($e->getMessage());
        }
    }
}

$foo = new Test;
$foo->testing();

위 예제는 다음과 같이 출력해요:

string(4) "foo!"

예제 #6 여러 타입을 잡는 catch (Multi catch)

<?php

class MyException extends Exception { }

class MyOtherException extends Exception { }

class Test {
    public function testing() {
        try {
            throw new MyException();
        } catch (MyException | MyOtherException $e) {
            var_dump(get_class($e));
        }
    }
}

$foo = new Test;
$foo->testing();

위 예제는 다음과 같이 출력해요:

string(11) "MyException"

예제 #7 잡은 변수 생략하기

PHP 8.0.0 이상에서만 허용돼요.

<?php

class SpecificException extends Exception {}

function test() {
    throw new SpecificException('Oopsie');
}

try {
    test();
} catch (SpecificException) {
    print "A SpecificException was thrown, but we don't care about the details.";
}

위 예제는 다음과 같이 출력해요:

A SpecificException was thrown, but we don't care about the details.

예제 #8 표현식으로서의 throw

PHP 8.0.0 이상에서만 허용돼요.

<?php

function test() {
    do_something_risky() or throw new Exception('It did not work');
}

function do_something_risky() {
    return false; // Simulate failure
}

try {
    test();
} catch (Exception $e) {
    print $e->getMessage();
}

위 예제는 다음과 같이 출력해요:

It did not work

예제 #9 try와 finally에서 모두 예외 발생

<?php

try {
    try {
        throw new Exception(message: 'Third', previous: new Exception('Fourth'));
    } finally {
        throw new Exception(message: 'First', previous: new Exception('Second'));
    }
} catch (Exception $e) {
    var_dump(
        $e->getMessage(),
        $e->getPrevious()->getMessage(),
        $e->getPrevious()->getPrevious()->getMessage(),
        $e->getPrevious()->getPrevious()->getPrevious()->getMessage(),
    );
}

위 예제는 다음과 같이 출력해요:

string(5) "First"
string(6) "Second"
string(5) "Third"
string(6) "Fourth"