유지보수 가능한 코드를 위한 PHP 오류 처리 모범 사례
새 기능을 배포했는데 갑자기 PHP 애플리케이션에서 치명적인 오류가 발생합니다. 사용자는 빈 페이지를 보고, 로그는 비어 있습니다. 익숙한 상황인가요? 잘못된 오류 처리는 PHP 코드가 유지보수 불가능해지는 가장 흔한 이유 중 하나입니다. 이 글에서는 코드를 더 견고하고, 디버깅하기 쉽고, 유지보수하기 간단하게 만들어 줄 실용적인 PHP 오류 처리 모범 사례를 살펴보겠습니다.
PHP 오류 처리가 중요한 이유
PHP는 기본적으로 관대합니다. 경고 후에도 실행을 계속하는 경우가 많고, 오류가 조용히 무시될 수 있습니다. 이러한 유연성은 양날의 검입니다. 적절한 처리 없이는 오류가 다음과 같은 문제를 일으킬 수 있습니다:
- 사용자에게 민감한 정보를 노출합니다 (예: 스택 추적에 데이터베이스 자격 증명).
- 데이터를 손상시키거나 애플리케이션을 일관성 없는 상태로 남깁니다.
- 오류가 흩어지거나 억제되어 디버깅을 악몽으로 만듭니다.
좋은 오류 처리는 문제가 발생했을 때 이를 인지하고, 신속하게 수정하며, 사용자가 원활한 경험을 할 수 있도록 보장합니다.
1. 적절한 오류 보고 수준 설정
첫 번째 단계는 환경에 맞게 PHP의 오류 보고를 올바르게 구성하는 것입니다. 개발 중에는 모든 오류를 보고 싶고, 프로덕션에서는 로그에 기록하되 표시하지 않아야 합니다.
// 개발
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// 프로덕션
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/path/to/php-error.log');
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
환경 변수나 구성 파일을 사용하여 이러한 설정을 자동으로 전환하세요. 프로덕션 서버에서 수동으로 php.ini를 편집하는 것에 의존하지 마세요.
2. 오류 코드 대신 예외 사용
오류 코드(예: false 또는 -1)를 반환하는 것은 코드를 복잡하게 만들고 실패를 무시하기 쉽게 만드는 레거시 패턴입니다. 예외는 오류를 명시적으로 처리하도록 강제하고 정상 경로를 깔끔하게 유지합니다.
// 나쁨: 오류 코드
function getUser($id) {
$user = db_find($id);
if (!$user) {
return false; // 호출자가 확인해야 함
}
return $user;
}
// 좋음: 예외
function getUser($id) {
$user = db_find($id);
if (!$user) {
throw new UserNotFoundException("User $id not found");
}
return $user;
}
다양한 오류 유형에 대해 사용자 정의 예외 클래스를 만드세요. 이렇게 하면 특정 오류를 쉽게 잡아 적절히 처리할 수 있습니다.
3. 올바른 수준에서 예외 잡기
흔한 실수는 예외를 너무 일찍 또는 너무 광범위하게 잡는 것입니다. 실제로 로그를 남기거나, 재시도하거나, 사용자 친화적인 메시지를 표시하는 등 의미 있는 조치를 취할 수 있을 때만 예외를 잡으세요.
try {
$user = getUser($id);
$order = createOrder($user, $items);
} catch (UserNotFoundException $e) {
// 사용자 없음 구체적으로 처리
return response('User not found', 404);
} catch (PaymentFailedException $e) {
// 결제 실패 처리
return response('Payment failed: ' . $e->getMessage(), 400);
} catch (Throwable $e) {
// 예상치 못한 오류에 대한 포괄적 처리
log_error($e);
return response('Something went wrong', 500);
}
Throwable(PHP 7+)을 사용하여 예외와 오류를 모두 잡으세요. 빈 catch 블록은 피하세요. 잡았다면 의미 있는 작업을 수행하세요.
4. 컨텍스트와 함께 오류 로깅
로깅은 프로덕션 문제를 디버깅할 때 최고의 친구입니다. 하지만 "Error occurred"와 같은 로그 메시지는 쓸모없습니다. 사용자 ID, 요청 매개변수, 스택 추적, 타임스탬프를 포함하세요.
try {
processPayment($order);
} catch (PaymentException $e) {
error_log(sprintf(
"Payment failed for order %d: %s in %s:%d\nStack trace: %s",
$order->id,
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
));
throw $e; // 로깅 후 다시 던지기
}
구조화된 로그를 위해 Monolog와 같은 로깅 라이브러리 사용을 고려하세요. 다양한 핸들러(파일, syslog, Slack)와 로그 수준(debug, info, warning, error)을 지원합니다.
5. 사용자 정의 오류 핸들러 만들기
PHP의 기본 오류 핸들러는 오류를 화면에 출력하므로 프로덕션에 적합하지 않습니다. 사용자 정의 오류 핸들러를 사용하면 오류를 예외로 변환하거나, 로그를 남기거나, 친화적인 오류 페이지를 표시할 수 있습니다.
set_error_handler(function ($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) {
return false; // error_reporting 설정 존중
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
set_exception_handler(function ($e) {
log_error($e);
http_response_code(500);
include 'views/error.php';
});
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
log_error(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']));
}
});
이 설정은 치명적인 오류를 포함한 모든 오류가 로깅되고 원활하게 처리되도록 보장합니다.
6. 입력 유효성 검사 및 조기 실패
많은 오류는 잘못된 입력에서 비롯됩니다. 애플리케이션의 경계(컨트롤러, API 엔드포인트)에서 데이터를 검증하고 유효성 검사에 실패하면 즉시 예외를 던지세요. 이렇게 하면 오류가 코드 깊숙이 전파되는 것을 방지할 수 있습니다.
function createUser(array $data) {
if (empty($data['email'])) {
throw new InvalidArgumentException('Email is required');
}
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
// ... 자신 있게 진행
}
PHP의 필터 함수나 유효성 검사 라이브러리(예: Respect\Validation)를 사용하여 유효성 검사를 일관되게 유지하세요.
7. @로 오류를 억제하지 마세요
@ 연산자는 오류를 침묵시켜 디버깅을 어렵게 만듭니다. 경고를 발생시킬 수 있는 함수(예: file_get_contents)를 호출할 때 사용하기 쉽지만 실제 문제를 숨깁니다. 대신 전제 조건을 확인하거나 예외와 함께 try-catch를 사용하세요.
// 나쁨
$content = @file_get_contents($url);
// 좋음
if (!is_readable($url)) {
throw new RuntimeException("Cannot read $url");
}
$content = file_get_contents($url);
억제해야 한다면 잘 이해된 경우에만 사용하고 이유를 문서화하세요.
8. 중앙 집중식 오류 처리 미들웨어 사용
Laravel이나 Symfony와 같은 프레임워크에서는 오류 처리가 종종 미들웨어나 예외 핸들러에 중앙 집중화되어 있습니다. 직접 구축하는 경우 모든 예외를 잡아 HTTP 응답으로 변환하는 단일 진입점을 만드세요.
// 프론트 컨트롤러(index.php)에서
try {
$response = $router->dispatch($request);
} catch (HttpException $e) {
$response = new Response($e->getMessage(), $e->getStatusCode());
} catch (Throwable $e) {
log_error($e);
$response = new Response('Internal Server Error', 500);
}
$response->send();
이렇게 하면 오류 처리 로직이 한 곳에 유지되고 일관성이 보장됩니다.
비교: 오류 처리 접근 방식
| 접근 방식 | 장점 | 단점 |
|---|---|---|
| 오류 코드 | 간단하고 예외 없음 | 무시하기 쉽고 코드를 복잡하게 만듦 |
| 예외 | 처리를 강제하고 깔끔한 분리 | 과도하게 사용될 수 있고 성능 오버헤드 |
| 사용자 정의 오류 핸들러 | 중앙 집중식, 모든 오류 포착 | 설정 필요, 잘못 구성하면 오류를 숨길 수 있음 |
| 로깅만 | 비침투적, 모니터링에 좋음 | 오류를 처리하지 않고 기록만 함 |
FAQ
PHP에서 오류와 예외의 차이점은 무엇인가요?
오류는 구문 오류나 유형 오류와 같은 저수준 문제이고, 예외는 예외적인 조건을 나타내는 던져진 객체입니다. PHP 7+에서는 둘 다 Throwable 인터페이스를 구현하므로 단일 catch 블록으로 둘 다 잡을 수 있습니다.
모든 함수 호출에 try-catch를 사용해야 하나요?
아니요, 그렇게 하면 지나치게 방어적인 코드가 됩니다. 의미 있게 처리할 수 있을 때만 예외를 잡으세요. 로그, 재시도, 사용자 친화적인 메시지 표시 등입니다. 예상치 못한 경우에는 예외가 중앙 핸들러로 버블링되도록 하세요.
민감한 데이터를 노출하지 않고 오류를 로깅하려면 어떻게 해야 하나요?
비밀번호, 토큰, 개인 데이터를 제거하여 로그 메시지를 정리하세요. 컨텍스트 필드와 함께 구조화된 로깅을 사용하고, 로깅 라이브러리가 민감한 키를 삭제하도록 구성하세요. 또한 로그 파일이 안전하게 저장되고 접근이 제한되도록 하세요.
PHP 오류 처리를 간소화할 준비가 되셨나요? 현재 오류 보고 설정을 감사하고 사용자 정의 오류 핸들러를 구현하는 것부터 시작하세요. JSON 페이로드나 로그를 빠르게 디버깅하려면 JSON Formatter를 사용하여 데이터를 검증하고 예쁘게 출력해보세요.