PHP 오류 처리와 예외 관리 기법 완전정복! 🚀

2025년 3월 기준 최신 PHP 8.3 오류 처리 트렌드와 실전 테크닉
🔍 PHP 오류 처리, 왜 중요할까?
안녕! 오늘은 PHP 개발할 때 정말 중요한데 자칫 소홀해지기 쉬운 오류 처리와 예외 관리에 대해 함께 알아볼 거야. 2025년 현재 PHP 8.3 버전까지 나온 상황에서 오류 처리 방식도 많이 발전했거든! 😊
혹시 이런 경험 있어? 열심히 개발한 PHP 웹사이트가 갑자기 하얀 화면만 보여주거나, 사용자에게 민망한 오류 메시지를 그대로 노출시키는 상황? 아니면 로그 파일이 오류 메시지로 가득 차서 정작 중요한 문제를 놓치는 경우? 이런 문제들은 모두 효과적인 오류 처리와 예외 관리로 해결할 수 있어!
재능넷 같은 복잡한 재능 거래 플랫폼을 운영한다면 더더욱 중요해. 사용자가 재능을 등록하거나 결제하는 중요한 순간에 오류가 발생하면? 🤔 신뢰도가 떨어지고 사용자 이탈로 이어질 수 있거든. 그래서 오늘은 실전에서 바로 적용할 수 있는 PHP 오류 처리와 예외 관리 기법을 친절하게 알려줄게!
📊 PHP 오류의 종류 알아보기
PHP에서 발생하는 오류는 크게 여러 종류로 나눌 수 있어. 각 오류 유형에 따라 처리 방법도 달라지니 잘 알아두는 게 좋아! 👍
PHP 오류 유형 총정리
- Parse Error(구문 오류): PHP 코드 구문이 잘못되었을 때 발생해. 세미콜론 빼먹거나 괄호 짝이 안 맞을 때 주로 나타나지.
- Fatal Error(치명적 오류): 프로그램 실행을 중단시키는 심각한 오류야. 존재하지 않는 함수를 호출하거나 클래스를 찾을 수 없을 때 발생해.
- Warning(경고): 프로그램 실행은 계속되지만 문제가 있다고 알려주는 오류. 존재하지 않는 파일을 include 할 때 같은 상황에서 발생해.
- Notice(알림): 작은 문제지만 더 큰 오류의 원인이 될 수 있는 상황. 정의되지 않은 변수를 사용할 때 발생하곤 해.
- Deprecated(지원 중단): 앞으로 사라질 기능을 사용했을 때 발생하는 알림. PHP 8.3에서는 더 많은 레거시 기능들이 deprecated 되었어.
PHP 8부터는 Fatal Error 대신 Error 예외를 던지는 방식으로 많이 바뀌었어. 이렇게 하면 try-catch 블록으로 더 우아하게 오류를 처리할 수 있지! 😎
💡 알아두면 좋은 팁: PHP 8.3부터는 더 많은 치명적 오류가 예외로 변환되어 처리할 수 있게 되었어. 이전 버전과의 호환성을 위해 코드를 업데이트할 때 이 점을 꼭 고려해야 해!
🛠️ 기본적인 PHP 오류 처리 방법
PHP에서 오류를 처리하는 기본적인 방법부터 알아볼까? 이 부분은 모든 PHP 개발자가 꼭 알아야 하는 기초야! 🧱
1. 오류 보고 레벨 설정하기
PHP에서는 error_reporting()
함수로 어떤 종류의 오류를 보여줄지 설정할 수 있어. 개발 환경과 프로덕션 환경에서 다르게 설정하는 게 좋아!
// 개발 환경: 모든 오류 표시
error_reporting(E_ALL);
ini_set('display_errors', 1);
// 프로덕션 환경: 오류 화면에 표시하지 않고 로그에만 기록
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/path/to/error.log');
2. 커스텀 오류 핸들러 만들기
자신만의 오류 처리 함수를 만들어서 오류가 발생했을 때 어떻게 반응할지 직접 정의할 수 있어. 이건 정말 유용한 기능이지!
function myErrorHandler($errno, $errstr, $errfile, $errline) {
// 오류 타입에 따라 다르게 처리
switch ($errno) {
case E_USER_ERROR:
echo "<div style='color:red'>치명적 오류: $errstr</div>";
exit(1);
case E_USER_WARNING:
echo "<div style='color:orange'>경고: $errstr</div>";
break;
case E_USER_NOTICE:
echo "<div style='color:blue'>알림: $errstr</div>";
break;
default:
echo "<div style='color:gray'>알 수 없는 오류: $errstr</div>";
break;
}
// 개발 환경에서만 자세한 정보 표시
if (ENVIRONMENT === 'development') {
echo "<p>오류 위치: $errfile, 라인: $errline</p>";
}
// 로그에 기록
error_log("[$errno] $errstr in $errfile on line $errline");
// true를 반환하면 PHP 표준 오류 핸들러를 실행하지 않음
return true;
}
// 커스텀 오류 핸들러 등록
set_error_handler("myErrorHandler");
🔔 주의사항: set_error_handler()
는 모든 종류의 오류를 처리하지 않아. Parse Error나 일부 Fatal Error는 잡아내지 못하니 이 점 유의해야 해!
3. 종료 핸들러 활용하기
스크립트 실행이 끝날 때 (정상적으로 끝나든 오류로 끝나든) 특정 함수를 실행하도록 설정할 수 있어. 이걸 종료 핸들러(Shutdown Handler)라고 해.
function shutdownHandler() {
$error = error_get_last();
// 치명적인 오류가 발생했는지 확인
if ($error !== null && ($error['type'] === E_ERROR || $error['type'] === E_PARSE)) {
// 사용자에게 보여줄 오류 메시지
echo "<h1>죄송합니다, 오류가 발생했습니다.</h1>";
// 관리자에게 이메일 보내기
mail('admin@example.com', '사이트 오류 발생',
"타입: {$error['type']}\n메시지: {$error['message']}\n파일: {$error['file']}\n라인: {$error['line']}");
// 로그에 기록
error_log("치명적 오류: {$error['message']} in {$error['file']} on line {$error['line']}");
}
}
// 종료 핸들러 등록
register_shutdown_function('shutdownHandler');
이런 기본적인 오류 처리 방법을 활용하면 PHP 애플리케이션의 안정성을 크게 높일 수 있어. 하지만 현대적인 PHP 개발에서는 예외(Exception)를 활용한 방식이 더 많이 사용되고 있어. 다음으로 그 부분을 알아볼게! 👇
🎯 PHP 예외(Exception) 처리 마스터하기
PHP 5부터 도입된 예외 처리는 현대적인 오류 관리의 핵심이야. 특히 PHP 7 이후로는 더욱 강력해졌지! 예외를 사용하면 오류 흐름을 더 체계적으로 제어할 수 있어. 🧩
1. 기본 예외 처리 구문
예외 처리의 기본 구조는 try-catch
블록이야. 이렇게 사용해:
try {
// 예외가 발생할 수 있는 코드
$file = fopen('존재하지_않는_파일.txt', 'r');
if (!$file) {
throw new Exception('파일을 열 수 없습니다.');
}
// 파일 처리 코드...
} catch (Exception $e) {
// 예외 처리 코드
echo '오류 발생: ' . $e->getMessage();
// 로그에 기록
error_log('파일 오류: ' . $e->getMessage());
} finally {
// 예외 발생 여부와 관계없이 항상 실행되는 코드
if (isset($file) && $file) {
fclose($file);
}
echo '파일 처리 작업 완료';
}
finally 블록은 PHP 5.5부터 추가되었어. 예외 발생 여부와 상관없이 항상 실행되는 코드를 넣을 수 있지. 리소스를 정리하는 데 아주 유용해! 💧
2. 다중 예외 처리하기
여러 종류의 예외를 다르게 처리하고 싶을 때는 여러 개의 catch 블록을 사용할 수 있어:
try {
// 데이터베이스 연결
$db = new PDO('mysql:host=localhost;dbname=mydb', 'username', 'password');
// 쿼리 실행
$stmt = $db->query('SELECT * FROM 존재하지_않는_테이블');
// 파일 처리
$content = file_get_contents('config.json');
if ($content === false) {
throw new RuntimeException('설정 파일을 읽을 수 없습니다.');
}
} catch (PDOException $e) {
// 데이터베이스 관련 예외 처리
echo '데이터베이스 오류: ' . $e->getMessage();
logError('DB', $e);
} catch (RuntimeException $e) {
// 런타임 예외 처리
echo '실행 오류: ' . $e->getMessage();
logError('Runtime', $e);
} catch (Exception $e) {
// 기타 모든 예외 처리
echo '일반 오류: ' . $e->getMessage();
logError('General', $e);
}
💡 중요 팁: catch 블록의 순서가 중요해! 더 구체적인 예외 클래스(자식 클래스)를 먼저 캐치하고, 일반적인 예외 클래스(부모 클래스)를 나중에 캐치해야 해. 그렇지 않으면 구체적인 예외 처리 코드가 실행되지 않을 수 있어.
3. PHP 7+ 다중 예외 타입 캐치하기
PHP 7.1부터는 하나의 catch 블록에서 여러 예외 타입을 처리할 수 있게 되었어:
try {
// 예외가 발생할 수 있는 코드
processData();
} catch (InvalidArgumentException | LogicException $e) {
// 두 가지 예외 타입을 동일하게 처리
echo '입력 또는 로직 오류: ' . $e->getMessage();
} catch (Exception $e) {
// 그 외 모든 예외
echo '기타 오류: ' . $e->getMessage();
}
4. PHP 8의 새로운 예외 기능
PHP 8에서는 예외 처리에 몇 가지 멋진 개선 사항이 추가되었어. 특히 명명된 인수(Named Arguments)를 사용해 예외를 더 명확하게 생성할 수 있게 되었지!
// PHP 8 이전
throw new InvalidArgumentException('사용자 ID는 양수여야 합니다.', 400);
// PHP 8 이후 - 명명된 인수 사용
throw new InvalidArgumentException(
message: '사용자 ID는 양수여야 합니다.',
code: 400
);
또한 PHP 8에서는 Error와 Exception의 구분이 더 명확해졌어. 이제 많은 치명적 오류가 Error 예외로 변환되어 catch 블록에서 잡을 수 있게 되었지!
🔧 커스텀 예외 클래스 만들기
자신만의 예외 클래스를 만들면 애플리케이션에 특화된 오류를 더 효과적으로 처리할 수 있어. 특히 재능넷 같은 플랫폼에서는 결제 오류, 사용자 인증 오류 등 다양한 상황에 맞는 예외 처리가 필요하지! 🛒
1. 기본 커스텀 예외 클래스
간단한 커스텀 예외 클래스는 이렇게 만들 수 있어:
// 기본 예외 클래스 상속
class PaymentException extends Exception {
// 추가 속성
protected $transactionId;
// 생성자 오버라이드
public function __construct($message, $code = 0, $transactionId = null, Exception $previous = null) {
// 부모 생성자 호출
parent::__construct($message, $code, $previous);
// 추가 속성 설정
$this->transactionId = $transactionId;
}
// 추가 메서드
public function getTransactionId() {
return $this->transactionId;
}
}
2. 예외 계층 구조 만들기
대규모 애플리케이션에서는 체계적인 예외 계층 구조를 만드는 것이 좋아. 이렇게 하면 예외를 더 세분화해서 처리할 수 있지!
// 기본 애플리케이션 예외
class AppException extends Exception {}
// 도메인별 예외 클래스
class DatabaseException extends AppException {}
class ValidationException extends AppException {}
class AuthException extends AppException {}
// 더 구체적인 예외 클래스
class ConnectionException extends DatabaseException {}
class QueryException extends DatabaseException {}
class InputValidationException extends ValidationException {}
class BusinessRuleException extends ValidationException {}
class LoginFailedException extends AuthException {}
class UnauthorizedException extends AuthException {}
이런 계층 구조를 사용하면 예외 처리를 더 유연하게 할 수 있어:
try {
// 복잡한 비즈니스 로직 실행
processUserRegistration($userData);
} catch (DatabaseException $e) {
// 모든 데이터베이스 관련 예외 처리
logDatabaseError($e);
showUserFriendlyMessage('데이터베이스 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
} catch (ValidationException $e) {
// 모든 유효성 검사 관련 예외 처리
showValidationError($e->getMessage());
} catch (AuthException $e) {
// 모든 인증 관련 예외 처리
redirectToLogin();
} catch (AppException $e) {
// 기타 애플리케이션 예외 처리
logError($e);
showGenericErrorPage();
}
🌟 프로 팁: 예외 클래스에 HTTP 상태 코드를 포함시키면 API 응답을 생성할 때 매우 유용해! 각 예외 클래스가 적절한 HTTP 상태 코드를 반환하도록 설계하면 API 오류 처리가 훨씬 간결해져.
3. 실용적인 예외 처리 예제
실제 프로젝트에서 커스텀 예외를 어떻게 활용하는지 살펴볼까? 재능넷 같은 플랫폼에서 사용자 등록 과정을 예로 들어볼게:
// 사용자 등록 함수
function registerUser(array $userData) {
// 입력 데이터 검증
if (empty($userData['email'])) {
throw new InputValidationException('이메일은 필수 입력 항목입니다.');
}
if (!filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
throw new InputValidationException('유효한 이메일 형식이 아닙니다.');
}
if (strlen($userData['password']) < 8) {
throw new InputValidationException('비밀번호는 최소 8자 이상이어야 합니다.');
}
try {
// 데이터베이스 연결
$db = new PDO('mysql:host=localhost;dbname=talent_net', 'username', 'password');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 이메일 중복 확인
$stmt = $db->prepare('SELECT COUNT(*) FROM users WHERE email = :email');
$stmt->execute(['email' => $userData['email']]);
if ($stmt->fetchColumn() > 0) {
throw new BusinessRuleException('이미 등록된 이메일입니다.');
}
// 비밀번호 해싱
$hashedPassword = password_hash($userData['password'], PASSWORD_DEFAULT);
// 사용자 정보 저장
$stmt = $db->prepare('INSERT INTO users (name, email, password) VALUES (:name, :email, :password)');
$result = $stmt->execute([
'name' => $userData['name'],
'email' => $userData['email'],
'password' => $hashedPassword
]);
if (!$result) {
throw new QueryException('사용자 정보를 저장하는 중 오류가 발생했습니다.');
}
return $db->lastInsertId();
} catch (PDOException $e) {
// PDO 예외를 우리의 커스텀 예외로 변환
throw new ConnectionException('데이터베이스 연결 오류: ' . $e->getMessage(), $e->getCode(), $e);
}
}
이제 이 함수를 호출하는 코드에서는 다양한 예외를 처리할 수 있어:
try {
$userId = registerUser($_POST);
echo "회원가입이 완료되었습니다. 사용자 ID: " . $userId;
} catch (InputValidationException $e) {
// 입력 값 오류 - 사용자에게 직접 보여줄 수 있음
echo "입력 오류: " . $e->getMessage();
} catch (BusinessRuleException $e) {
// 비즈니스 규칙 오류 - 사용자에게 직접 보여줄 수 있음
echo "처리 오류: " . $e->getMessage();
} catch (DatabaseException $e) {
// 데이터베이스 오류 - 일반적인 메시지 표시 및 로깅
error_log("DB 오류: " . $e->getMessage());
echo "시스템 오류가 발생했습니다. 나중에 다시 시도해주세요.";
} catch (Exception $e) {
// 기타 모든 예외 - 일반적인 메시지 표시 및 로깅
error_log("예상치 못한 오류: " . $e->getMessage());
echo "처리 중 오류가 발생했습니다.";
}
이런 방식으로 예외를 체계적으로 관리하면 코드의 가독성과 유지보수성이 크게 향상돼! 😊
📝 오류 로깅과 모니터링
오류를 잘 처리하는 것도 중요하지만, 오류를 체계적으로 기록하고 모니터링하는 것도 매우 중요해. 이를 통해 문제를 사전에 발견하고 해결할 수 있거든! 🔍
1. 기본 오류 로깅
PHP의 기본 오류 로깅 기능을 활용하는 방법이야:
// 오류 로그 파일 위치 설정
ini_set('error_log', '/path/to/error.log');
// 오류 로깅 활성화
ini_set('log_errors', 1);
// 로그에 메시지 기록
error_log('중요한 오류가 발생했습니다: 데이터베이스 연결 실패');
2. 구조화된 로깅 시스템 구축
대규모 애플리케이션에서는 Monolog 같은 라이브러리를 사용해 더 체계적인 로깅 시스템을 구축하는 것이 좋아:
// Composer로 Monolog 설치
// composer require monolog/monolog
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SlackHandler;
use Monolog\Formatter\LineFormatter;
// 로거 인스턴스 생성
$logger = new Logger('app');
// 파일에 로그 기록
$fileHandler = new StreamHandler('/path/to/app.log', Logger::DEBUG);
$fileHandler->setFormatter(new LineFormatter(
"[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
"Y-m-d H:i:s"
));
$logger->pushHandler($fileHandler);
// 중요 오류는 Slack에도 알림
$slackHandler = new SlackHandler(
'slack-token',
'#errors',
'ErrorBot',
true,
null,
Logger::ERROR
);
$logger->pushHandler($slackHandler);
// 로그 기록 예시
$logger->info('사용자 로그인', ['user_id' => 123, 'ip' => '192.168.1.1']);
$logger->error('결제 처리 실패', [
'user_id' => 123,
'amount' => 5000,
'error_code' => 'CARD_DECLINED'
]);
⚠️ 주의사항: 로그에 개인정보나 민감한 정보(비밀번호, 신용카드 번호 등)를 기록하지 않도록 주의해야 해! GDPR 같은 개인정보 보호 규정을 준수하는 것이 중요해.
3. 컨텍스트 정보 포함하기
오류 로그에는 문제 해결에 도움이 되는 컨텍스트 정보를 최대한 포함시키는 것이 좋아:
try {
// 코드 실행
} catch (Exception $e) {
// 오류 발생 시 상세 정보 로깅
$logger->error('API 요청 처리 중 오류 발생', [
'exception' => get_class($e),
'message' => $e->getMessage(),
'code' => $e->getCode(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
'request_uri' => $_SERVER['REQUEST_URI'],
'request_method' => $_SERVER['REQUEST_METHOD'],
'request_params' => $_REQUEST,
'user_id' => $currentUserId ?? 'guest',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
]);
}
4. 오류 모니터링 도구 활용
프로덕션 환경에서는 Sentry, New Relic, Bugsnag 같은 오류 모니터링 도구를 활용하면 좋아. 이런 도구들은 오류를 실시간으로 수집하고 분석해줘서 문제를 빠르게 발견하고 해결하는 데 도움이 돼! 🚨
// Sentry 예시 (composer require sentry/sdk)
\Sentry\init([
'dsn' => 'https://examplePublicKey@o0.ingest.sentry.io/0',
'environment' => 'production',
'release' => '1.0.0',
]);
try {
processCheckout($order);
} catch (Exception $e) {
// Sentry에 오류 보고
\Sentry\captureException($e);
// 사용자에게 오류 메시지 표시
showErrorPage();
}
이런 도구들은 단순히 오류를 기록하는 것을 넘어 다음과 같은 기능을 제공해:
- 오류 그룹화 및 중복 제거
- 오류 발생 빈도 및 영향 분석
- 오류 발생 환경 정보 수집 (브라우저, OS, 사용자 정보 등)
- 알림 설정 (이메일, Slack 등)
- 성능 모니터링
재능넷 같은 플랫폼에서는 이런 도구를 활용해 사용자 경험을 지속적으로 모니터링하고 개선할 수 있어! 👍
🌐 REST API에서의 오류 처리
현대 웹 애플리케이션에서는 REST API를 통한 데이터 교환이 일반적이야. API에서의 오류 처리는 클라이언트에게 명확하고 일관된 방식으로 오류 정보를 전달하는 것이 중요해! 📡
1. HTTP 상태 코드 활용
API 응답에서는 적절한 HTTP 상태 코드를 사용해 오류의 성격을 나타내는 것이 좋아:
function handleApiRequest() {
try {
// API 요청 처리
$data = processRequest();
// 성공 응답
http_response_code(200); // OK
echo json_encode(['status' => 'success', 'data' => $data]);
} catch (ValidationException $e) {
// 유효성 검사 오류
http_response_code(400); // Bad Request
echo json_encode([
'status' => 'error',
'message' => $e->getMessage(),
'errors' => $e->getValidationErrors()
]);
} catch (AuthException $e) {
// 인증 오류
http_response_code(401); // Unauthorized
echo json_encode([
'status' => 'error',
'message' => '인증에 실패했습니다.'
]);
} catch (NotFoundException $e) {
// 리소스를 찾을 수 없음
http_response_code(404); // Not Found
echo json_encode([
'status' => 'error',
'message' => $e->getMessage()
]);
} catch (Exception $e) {
// 서버 내부 오류
http_response_code(500); // Internal Server Error
echo json_encode([
'status' => 'error',
'message' => '서버 오류가 발생했습니다.'
]);
// 상세 오류 정보는 로그에만 기록
error_log('API 오류: ' . $e->getMessage() . ' in ' . $e->getFile() . ' on line ' . $e->getLine());
}
}
🔑 핵심 포인트: API 오류 응답에서는 사용자에게 보여줄 메시지와 개발자를 위한 디버깅 정보를 구분하는 것이 좋아. 민감한 내부 오류 정보는 프로덕션 환경에서 클라이언트에게 노출하지 않도록 주의해!
2. 일관된 오류 응답 구조
API 오류 응답은 일관된 형식을 유지하는 것이 중요해. 이렇게 하면 클라이언트 측에서 오류 처리가 훨씬 쉬워져:
// 오류 응답 헬퍼 함수
function sendErrorResponse($statusCode, $message, $errors = null, $errorCode = null) {
http_response_code($statusCode);
$response = [
'status' => 'error',
'message' => $message
];
if ($errorCode) {
$response['code'] = $errorCode;
}
if ($errors) {
$response['errors'] = $errors;
}
echo json_encode($response);
exit;
}
// 사용 예시
try {
$user = getUserById($_GET['id']);
if (!$user) {
sendErrorResponse(404, '사용자를 찾을 수 없습니다.', null, 'USER_NOT_FOUND');
}
// 사용자 데이터 반환...
} catch (Exception $e) {
sendErrorResponse(500, '서버 오류가 발생했습니다.');
}
3. 유효성 검사 오류 처리
API에서 폼 데이터나 JSON 입력의 유효성 검사 오류는 특별히 신경 써서 처리해야 해. 클라이언트가 어떤 필드에 어떤 문제가 있는지 명확하게 알 수 있도록 해줘야 하거든:
function validateUserData($data) {
$errors = [];
if (empty($data['name'])) {
$errors['name'] = '이름은 필수 입력 항목입니다.';
}
if (empty($data['email'])) {
$errors['email'] = '이메일은 필수 입력 항목입니다.';
} elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = '유효한 이메일 형식이 아닙니다.';
}
if (empty($data['password'])) {
$errors['password'] = '비밀번호는 필수 입력 항목입니다.';
} elseif (strlen($data['password']) < 8) {
$errors['password'] = '비밀번호는 최소 8자 이상이어야 합니다.';
}
if (!empty($errors)) {
// 유효성 검사 오류가 있으면 예외 발생
throw new ValidationException('입력 데이터가 유효하지 않습니다.', $errors);
}
return $data;
}
// ValidationException 클래스
class ValidationException extends Exception {
protected $errors;
public function __construct($message, array $errors, $code = 0, Exception $previous = null) {
parent::__construct($message, $code, $previous);
$this->errors = $errors;
}
public function getValidationErrors() {
return $this->errors;
}
}
이런 방식으로 API 오류를 처리하면 프론트엔드 개발자가 훨씬 쉽게 오류를 처리하고 사용자에게 적절한 피드백을 제공할 수 있어! 🙌
🧪 오류 처리를 위한 테스트 전략
효과적인 오류 처리를 위해서는 체계적인 테스트가 필수야! 정상 케이스뿐만 아니라 다양한 오류 상황을 테스트해야 안정적인 애플리케이션을 만들 수 있어. 🧩
1. 단위 테스트에서 예외 테스트
PHPUnit을 사용해 예외가 올바르게 발생하는지 테스트하는 방법이야:
use PHPUnit\Framework\TestCase;
class UserServiceTest extends TestCase {
public function testRegisterUserWithInvalidEmail() {
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('유효한 이메일 형식이 아닙니다');
$userService = new UserService();
$userService->registerUser([
'name' => 'John Doe',
'email' => 'invalid-email', // 잘못된 이메일 형식
'password' => 'password123'
]);
}
public function testDuplicateEmailRegistration() {
// 먼저 사용자 등록
$userService = new UserService();
$userService->registerUser([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password123'
]);
// 같은 이메일로 다시 등록 시도
$this->expectException(BusinessRuleException::class);
$this->expectExceptionMessage('이미 등록된 이메일입니다');
$userService->registerUser([
'name' => 'Jane Doe',
'email' => 'john@example.com', // 중복된 이메일
'password' => 'password456'
]);
}
}
2. 통합 테스트에서 오류 시나리오 테스트
실제 데이터베이스나 외부 서비스와의 상호작용에서 발생할 수 있는 오류 상황을 테스트하는 것도 중요해:
class PaymentServiceIntegrationTest extends TestCase {
public function testPaymentProcessingWithServerError() {
// 결제 게이트웨이 모의 객체 설정
$mockGateway = $this->createMock(PaymentGateway::class);
$mockGateway->method('processPayment')
->willThrowException(new GatewayException('결제 서버 연결 오류'));
$paymentService = new PaymentService($mockGateway);
try {
$paymentService->processPayment([
'amount' => 10000,
'card_number' => '1234-5678-9012-3456',
'expiry' => '12/25',
'cvv' => '123'
]);
$this->fail('예외가 발생하지 않았습니다.');
} catch (PaymentException $e) {
$this->assertEquals('결제 처리 중 오류가 발생했습니다.', $e->getMessage());
$this->assertEquals('GATEWAY_ERROR', $e->getErrorCode());
}
}
}
3. API 테스트에서 오류 응답 검증
API 엔드포인트가 오류 상황에서 올바른 응답을 반환하는지 테스트하는 것도 중요해:
class ApiTest extends TestCase {
public function testInvalidLoginReturns401() {
$client = new Client();
$response = $client->post('https://api.example.com/login', [
'json' => [
'email' => 'user@example.com',
'password' => 'wrong-password'
],
'http_errors' => false // 클라이언트에서 예외를 던지지 않도록 설정
]);
$this->assertEquals(401, $response->getStatusCode());
$body = json_decode($response->getBody(), true);
$this->assertEquals('error', $body['status']);
$this->assertEquals('이메일 또는 비밀번호가 올바르지 않습니다.', $body['message']);
}
public function testValidationErrorReturns400() {
$client = new Client();
$response = $client->post('https://api.example.com/users', [
'json' => [
'name' => '', // 빈 이름
'email' => 'invalid-email', // 잘못된 이메일
'password' => '123' // 너무 짧은 비밀번호
],
'http_errors' => false
]);
$this->assertEquals(400, $response->getStatusCode());
$body = json_decode($response->getBody(), true);
$this->assertEquals('error', $body['status']);
$this->assertEquals('입력 데이터가 유효하지 않습니다.', $body['message']);
$this->assertArrayHasKey('errors', $body);
$this->assertArrayHasKey('name', $body['errors']);
$this->assertArrayHasKey('email', $body['errors']);
$this->assertArrayHasKey('password', $body['errors']);
}
}
🌱 테스트 팁: 오류 처리 테스트는 "행복한 경로(happy path)" 테스트만큼이나 중요해! 실제로 많은 버그는 예외적인 상황에서 발생하기 때문에, 다양한 오류 시나리오를 철저히 테스트하는 것이 안정적인 애플리케이션을 만드는 핵심이야.
🔄 PHP 8.x의 새로운 오류 처리 기능
PHP 8.0, 8.1, 8.2, 그리고 최신 8.3 버전에서는 오류 처리와 관련된 많은 개선 사항이 추가되었어. 2025년 현재 시점에서 활용할 수 있는 최신 오류 처리 기능들을 알아보자! 🚀
1. 명명된 인수(Named Arguments)
PHP 8.0부터 도입된 명명된 인수는 예외를 생성할 때 더 명확하게 매개변수를 지정할 수 있게 해줘:
// PHP 8.0 이전
throw new Exception('사용자를 찾을 수 없습니다.', 404);
// PHP 8.0 이후
throw new Exception(
message: '사용자를 찾을 수 없습니다.',
code: 404
);
2. 유니온 타입(Union Types)
PHP 8.0에서 도입된 유니온 타입을 사용하면 함수가 여러 종류의 예외를 던질 수 있음을 명시적으로 선언할 수 있어:
/**
* 사용자 정보를 조회합니다.
* @param int $userId 사용자 ID
* @return User 사용자 객체
* @throws DatabaseException|NotFoundException 데이터베이스 오류 또는 사용자를 찾을 수 없는 경우
*/
function getUser(int $userId): User {
// 구현...
}
3. match 표현식
PHP 8.0에서 도입된 match 표현식은 오류 코드에 따라 다른 예외를 던지는 등의 작업을 더 간결하게 처리할 수 있게 해줘:
function handleDatabaseError($errorCode) {
$exception = match ($errorCode) {
1045 => new DatabaseException('데이터베이스 접속 권한이 없습니다.'),
1049 => new DatabaseException('데이터베이스를 찾을 수 없습니다.'),
1146 => new TableNotFoundException('테이블을 찾을 수 없습니다.'),
default => new DatabaseException('알 수 없는 데이터베이스 오류: ' . $errorCode),
};
throw $exception;
}
4. 널 안전 연산자(Nullsafe Operator)
PHP 8.0에서 도입된 널 안전 연산자(?->)는 객체가 null일 때 발생할 수 있는 오류를 우아하게 처리할 수 있게 해줘:
// PHP 8.0 이전
$country = null;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->getCountry();
}
}
// PHP 8.0 이후
$country = $user?->getAddress()?->getCountry();
5. 열거형(Enumerations)
PHP 8.1에서 도입된 열거형은 오류 코드나 상태를 더 타입 안전하게 관리할 수 있게 해줘:
// PHP 8.1 이후
enum ErrorCode: string {
case VALIDATION_ERROR = 'VALIDATION_ERROR';
case NOT_FOUND = 'NOT_FOUND';
case PERMISSION_DENIED = 'PERMISSION_DENIED';
case SERVER_ERROR = 'SERVER_ERROR';
}
class ApiException extends Exception {
private ErrorCode $errorCode;
public function __construct(
string $message,
ErrorCode $errorCode,
?Throwable $previous = null
) {
parent::__construct($message, 0, $previous);
$this->errorCode = $errorCode;
}
public function getErrorCode(): ErrorCode {
return $this->errorCode;
}
}
// 사용 예시
throw new ApiException(
message: '요청한 리소스를 찾을 수 없습니다.',
errorCode: ErrorCode::NOT_FOUND
);
6. 읽기 전용 속성(Readonly Properties)
PHP 8.1에서 도입된 읽기 전용 속성은 예외 클래스의 불변성을 보장하는 데 유용해:
// PHP 8.1 이후
class ValidationException extends Exception {
public function __construct(
string $message,
public readonly array $errors,
?Throwable $previous = null
) {
parent::__construct($message, 0, $previous);
}
}
7. 파이널리 없는 try 구문
PHP 8.0부터는 catch 블록 없이 try-finally 구문을 사용할 수 있어. 이는 예외를 처리하지 않고 리소스 정리만 하고 싶을 때 유용해:
function processFile($filename) {
$file = fopen($filename, 'r');
try {
// 파일 처리 코드...
return processFileContents($file);
} finally {
// 예외 발생 여부와 관계없이 파일 핸들 닫기
fclose($file);
}
// 예외가 발생하면 호출자에게 전파됨
}
8. 새로운 내장 예외 클래스
PHP 8.x에서는 더 많은 내장 예외 클래스가 추가되었어. 이를 활용하면 더 구체적인 오류 처리가 가능해져:
// PHP 8.0 이후 추가된 예외들
try {
// 코드...
} catch (ValueError $e) {
// 함수에 잘못된 값이 전달되었을 때
echo "잘못된 값: " . $e->getMessage();
} catch (UnhandledMatchError $e) {
// match 표현식에서 일치하는 패턴이 없을 때
echo "일치하는 패턴 없음: " . $e->getMessage();
} catch (FiberError $e) {
// Fiber(경량 스레드) 관련 오류
echo "Fiber 오류: " . $e->getMessage();
}
💡 PHP 8.3 팁: PHP 8.3에서는 더 많은 내부 함수가 타입 선언과 엄격한 오류 처리를 갖게 되었어. 이로 인해 이전 버전에서 경고로 처리되던 많은 상황이 이제는 예외를 발생시켜. 코드를 PHP 8.3으로 마이그레이션할 때는 이 점을 고려해서 예외 처리를 업데이트해야 해!
🔐 보안과 오류 처리
오류 처리는 보안과도 밀접한 관련이 있어. 부적절한 오류 처리는 보안 취약점으로 이어질 수 있기 때문에 특별히 주의해야 해! 🔒
1. 민감한 정보 노출 방지
프로덕션 환경에서는 상세한 오류 메시지가 사용자에게 노출되지 않도록 해야 해:
// 개발 환경과 프로덕션 환경 구분
if (ENVIRONMENT === 'production') {
// 프로덕션 환경: 오류를 화면에 표시하지 않음
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
error_reporting(E_ALL);
} else {
// 개발 환경: 모든 오류 표시
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
}
// 커스텀 오류 핸들러에서도 환경에 따라 다르게 처리
function myErrorHandler($errno, $errstr, $errfile, $errline) {
if (ENVIRONMENT === 'production') {
// 프로덕션: 일반적인 오류 메시지만 표시
echo "죄송합니다, 오류가 발생했습니다.";
// 상세 정보는 로그에만 기록
error_log("[$errno] $errstr in $errfile on line $errline");
} else {
// 개발: 상세한 오류 정보 표시
echo "오류 발생
";
echo "타입: $errno
";
echo "메시지: $errstr
";
echo "파일: $errfile
";
echo "라인: $errline
";
}
return true;
}
2. SQL 인젝션 예방
데이터베이스 오류 처리에서는 SQL 인젝션 공격을 방지하는 것이 중요해:
// 잘못된 방법 (SQL 인젝션 취약점)
function getUserBadWay($username) {
$db = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'password');
try {
// 위험: 사용자 입력을 직접 쿼리에 삽입
$query = "SELECT * FROM users WHERE username = '$username'";
$result = $db->query($query);
return $result->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// 위험: 오류 메시지에 SQL 쿼리 포함
echo "데이터베이스 오류: " . $e->getMessage() . " 쿼리: $query";
}
}
// 올바른 방법 (SQL 인젝션 방지)
function getUserGoodWay($username) {
$db = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'password');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
// 안전: 준비된 문장과 매개변수 바인딩 사용
$stmt = $db->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);
return $stmt->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// 안전: 일반적인 오류 메시지만 사용자에게 표시
echo "사용자 정보를 가져오는 중 오류가 발생했습니다.";
// 상세 정보는 로그에만 기록 (쿼리 매개변수는 포함하지 않음)
error_log("데이터베이스 오류: " . $e->getMessage());
return null;
}
}
3. 오류를 통한 정보 노출 방지
오류 메시지를 통해 중요한 정보가 노출되지 않도록 주의해야 해:
- 지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
재능넷은 최신 AI 기술과 법률에 기반하여 자사의 지적 재산권을 적극적으로 보호하며,
무단 사용 및 침해 행위에 대해 법적 대응을 할 권리를 보유합니다.
© 2025 재능넷 | All rights reserved.
댓글 0개