데이터베이스 연결 작업을 하다 보면 갑자기 PDOException이 튀어나와서 당황한 경험이 있을까? 많은 개발자들이 에러 메시지를 읽고도 정확히 뭐가 문제인지 모른 채 구글링으로 찾은 코드를 그냥 복사해 붙인다. 다만 같은 해결책이 항상 먹히는 건 아니고, 상황에 따라 원인이 완전히 다를 수 있다는 게 함정이다. 이번에는 PDOException의 종류별 원인이 정확히 뭔지, 각각 어떻게 해결하는지 실무에서 마주치는 모든 케이스를 정리해서 소개하겠다.
PDO와 PDOException 기초 이해하기
PDO(PHP Data Objects)는 PHP에서 다양한 데이터베이스에 접근하는 추상화 계층이다. MySQL뿐 아니라 PostgreSQL, SQLite, Oracle 등 여러 데이터베이스를 같은 인터페이스로 다룰 수 있다는 게 장점이다. 다만 데이터베이스 연결이 실패하거나 쿼리 실행 중 오류가 나면 PDOException이라는 예외를 던진다.PDOException은 단순히 "데이터베이스 에러 났어"라고만 알려주는 게 아니다. 에러 메시지, 에러 코드, SQL 상태 코드 등 여러 정보를 담고 있다. 문제는 대부분의 개발자가 이 정보를 제대로 읽지 않고 넘어간다는 것이다.
자주 만나는 PDOException 5가지 원인과 해결법
1. SQLSTATE[HY000]: General error - MySQL 서버 접속 실패
PDOException 중 가장 흔한 케이스는 데이터베이스 서버 자체에 접속할 수 없는 경우다. 호스트 주소, 포트, 계정 정보, 데이터베이스 이름 중 뭔가 잘못되었다는 뜻이다.✗ 잘못된 코드:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'root', 'password');
?>
위 코드는 아무 에러 처리 없이 연결을 시도한다. 만약 데이터베이스가 없거나 MySQL 서버가 꺼져 있으면, 사용자는 화면에 "Fatal error" 메시지를 보게 된다. 이건 보안상 위험할 뿐 아니라 디버깅도 어렵다.✓ 올바른 코드:
<?php
try {
$dsn = 'mysql:host=localhost;port=3306;charset=utf8mb4;dbname=mydb';
$pdo = new PDO($dsn, 'root', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
PDO::ATTR_PERSISTENT => false
]);
echo "연결 성공";
} catch (PDOException $e) {
error_log("DB 연결 실패: " . $e->getMessage());
die("데이터베이스 연결 오류가 발생했습니다.");
}
?>
여기서 주목할 점은 세 가지다. 첫째, try-catch로 예외를 잡아서 서버 에러 로그에만 기록하고 사용자에게는 일반적인 메시지만 보여준다. 둘째, PDO::ATTR_TIMEOUT으로 5초 이상 응답 없으면 연결을 포기하도록 설정한다(무한 대기 방지). 셋째, charset=utf8mb4를 명시해서 한글 인코딩 문제를 미리 방지한다.실제 원인 진단 방법:
- MySQL 서버가 실행 중인지 확인: `systemctl status mysql` (Linux) 또는 작업 관리자 (Windows)
- 호스트 주소가 맞는지 확인: 로컬은 localhost 또는 127.0.0.1, 원격은 실제 IP/도메인 - 포트 번호가 맞는지 확인: 기본값은 3306, 변경했다면 host=localhost:포트번호로 명시 - 계정과 암호가 맞는지 확인: `mysql -u root -p` 로 수동 접속 시도
2. SQLSTATE[HY000]: General error - 데이터베이스(스키마) 없음
연결 정보는 다 맞는데도 "SQLSTATE[HY000]: General error"가 나는 경우가 있다. 보통 dbname에 지정한 데이터베이스가 존재하지 않을 때다.✗ 잘못된 코드:
<?php
try {
$pdo = new PDO('mysql:host=localhost;dbname=typo_dbname', 'root', 'password');
} catch (PDOException $e) {
echo "에러: " . $e->getMessage();
}
?>
에러 메시지: "SQLSTATE[HY000]: General error: 1049 Unknown database 'typo_dbname'"✓ 올바른 코드:
<?php
// 1단계: MySQL에 접속 (데이터베이스 선택 없이)
$pdo = new PDO('mysql:host=localhost;charset=utf8mb4', 'root', 'password');
// 2단계: 데이터베이스 존재 여부 확인
$stmt = $pdo->query("SHOW DATABASES LIKE 'mydb'");
if ($stmt->rowCount() === 0) {
// 3단계: 데이터베이스 자동 생성
$pdo->exec("CREATE DATABASE IF NOT EXISTS mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
error_log("데이터베이스 'mydb' 생성됨");
}
// 4단계: 데이터베이스 선택
$pdo->exec("USE mydb");
echo "데이터베이스 준비 완료";
?>
이 방식은 배포 환경에서 데이터베이스가 없어도 자동으로 생성하므로 설정 오류를 최소화할 수 있다.
3. SQLSTATE[28000]: Access denied for user
MySQL 계정이나 암호가 잘못된 경우다. 특히 운영 서버에서는 계정별로 접근 가능한 데이터베이스가 제한되어 있을 수 있다.✗ 잘못된 코드:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'admin', 'wrong_password');
?>
에러 메시지: "SQLSTATE[28000]: Access denied for user 'admin'@'localhost' (using password: YES)"✓ 올바른 코드:
<?php
try {
$db_config = [
'host' => getenv('DB_HOST') ?: 'localhost',
'user' => getenv('DB_USER') ?: 'root',
'pass' => getenv('DB_PASS') ?: '',
'name' => getenv('DB_NAME') ?: 'mydb'
];
$dsn = "mysql:host={$db_config['host']};dbname={$db_config['name']};charset=utf8mb4";
$pdo = new PDO($dsn, $db_config['user'], $db_config['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
} catch (PDOException $e) {
// 에러 로그에만 기록, 상세 정보는 출력하지 않음
error_log("DB 인증 실패 - 계정 또는 암호 확인 필요");
die("데이터베이스 접근 권한이 없습니다.");
}
?>
환경 변수를 사용하면 코드에 민감한 정보(계정, 암호)를 하드코딩하지 않아도 된다. 배포 환경별로 .env 파일을 따로 관리하자.
4. SQLSTATE[HY000]: SQLSTATE error - 쿼리 문법 오류 또는 테이블 없음
PDO 연결 자체는 성공했지만, SQL 쿼리 실행 단계에서 에러가 나는 경우다. 테이블이 없거나 컬럼명이 잘못되었거나, SQL 문법이 틀렸을 때다.✗ 잘못된 코드:
<?php
try {
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'root', 'password');
// 테이블이 없거나 컬럼명이 틀린 경우
$stmt = $pdo->query("SELECT * FROM users WHERE id = 1");
$result = $stmt->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo "에러: " . $e->getMessage();
}
?>
만약 users 테이블이 없으면 에러: "SQLSTATE[42S02]: Table 'mydb.users' doesn't exist"✓ 올바른 코드:
<?php
try {
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'root', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
// 1단계: 테이블 존재 여부 확인
$stmt = $pdo->query("SHOW TABLES LIKE 'users'");
if ($stmt->rowCount() === 0) {
// 테이블 없으면 생성
$pdo->exec("CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
error_log("테이블 'users' 생성됨");
}
// 2단계: Prepared Statement로 안전하게 쿼리 실행
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => 1]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
echo "조회 성공: " . json_encode($result);
} catch (PDOException $e) {
error_log("쿼리 실행 실패: " . $e->getMessage() . " (Code: " . $e->getCode() . ")");
die("데이터 조회 오류가 발생했습니다.");
}
?>
Prepared Statement를 사용하면 SQL 인젝션 방지는 물론, 바인딩 변수의 타입을 명시해서 오류를 줄일 수 있다.
5. PDOException 발생 후 커넥션 풀 고갈 문제
예외가 발생한 후에도 연결이 제대로 닫히지 않으면, 계속 새로운 연결을 열게 되어 결국 "Too many connections" 에러에 도달한다.✗ 잘못된 코드:
<?php
// 루프 안에서 매번 새로운 PDO 인스턴스 생성
for ($i = 0; $i < 100; $i++) {
try {
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'root', 'password');
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
} catch (PDOException $e) {
echo "에러 발생";
}
// $pdo가 명시적으로 닫히지 않음
}
?>
이 코드는 100번 반복할 때마다 100개의 연결을 열고 가비지 컬렉션을 기다린다. 동시성이 높은 환경에서는 곧 연결 풀이 고갈된다.✓ 올바른 코드:
<?php
class DatabaseManager {
private static $pdo = null;
public static function getInstance() {
if (self::$pdo === null) {
try {
$dsn = 'mysql:host=localhost;dbname=mydb;charset=utf8mb4';
self::$pdo = new PDO($dsn, 'root', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false,
PDO::ATTR_TIMEOUT => 5
]);
} catch (PDOException $e) {
error_log("초기 DB 연결 실패: " . $e->getMessage());
throw new Exception("데이터베이스 초기화 실패");
}
}
return self::$pdo;
}
public static function closeConnection() {
self::$pdo = null; // PDO 객체 참조 해제 → 자동 연결 해제
}
}
// 사용 예제
try {
for ($i = 0; $i < 100; $i++) {
$pdo = DatabaseManager::getInstance();
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
$count = $stmt->fetchColumn();
}
} catch (Exception $e) {
echo "작업 중 에러: " . $e->getMessage();
} finally {
DatabaseManager::closeConnection();
}
?>
Singleton 패턴으로 전체 애플리케이션에서 단 하나의 PDO 인스턴스만 사용하면, 불필요한 연결 생성을 막을 수 있다.
주의사항: PDOException 에러 메시지 노출 금지
PDOException 메시지에는 데이터베이스 구조, 서버 경로, 계정 정보 등 민감한 정보가 포함될 수 있다. 절대 사용자 화면에 그대로 출력하면 안 된다.✗ 위험한 코드:
<?php
try {
$pdo = new PDO(...);
} catch (PDOException $e) {
// 절대 금지: 사용자에게 에러 메시지 노출
echo "<h1>" . $e->getMessage() . "</h1>";
echo "<p>File: " . $e->getFile() . "</p>";
}
?>
이렇게 하면 공격자가 데이터베이스 구조나 서버 경로를 파악할 수 있다.✓ 안전한 코드:
<?php
try {
$pdo = new PDO(...);
} catch (PDOException $e) {
// 1단계: 상세 정보는 로그에만 기록
error_log([
"message" => $e->getMessage(),
"code" => $e->getCode(),
"file" => $e->getFile(),
"line" => $e->getLine(),
"trace" => $e->getTraceAsString()
]);
// 2단계: 사용자에게는 일반적인 메시지만 보여줌
die("시스템 오류가 발생했습니다. 관리자에게 문의해주세요.");
}
?>
setup 단계에서 display_errors를 끄고 error_log를 활성화하면, 개발 환경에서는 로그로 디버깅할 수 있고 운영 환경에서는 사용자에게 정보가 노출되지 않는다.
실무에서 바로 쓸 수 있는 완벽한 DB 연결 함수
<?php
function createDatabaseConnection($config = []) {
$default_config = [
'host' => getenv('DB_HOST') ?: 'localhost',
'port' => getenv('DB_PORT') ?: 3306,
'user' => getenv('DB_USER') ?: 'root',
'pass' => getenv('DB_PASS') ?: '',
'name' => getenv('DB_NAME') ?: 'mydb'
];
$config = array_merge($default_config, $config);
try {
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
$config['host'],
$config['port'],
$config['name']
);
$pdo = new PDO($dsn, $config['user'], $config['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_TIMEOUT => 5,
PDO::ATTR_PERSISTENT => false
]);
// MySQL 세션 변수 설정 (추가 안정성)
$pdo->exec("SET SESSION sql_mode='STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'");
return $pdo;
} catch (PDOException $e) {
error_log("[DB Connection Error] " . $e->getMessage());
error_log("[DB Config] Host: " . $config['host'] . ", User: " . $config['user']);
// 개발 환경과 운영 환경 구분
if (getenv('ENVIRONMENT') === 'development') {
throw new Exception("데이터베이스 연결 실패: " . $e->getMessage());
} else {
throw new Exception("데이터베이스 연결 오류가 발생했습니다.");
}
}
}
// 사용 예제
try {
$pdo = createDatabaseConnection();
echo "✓ 데이터베이스 연결 성공";
} catch (Exception $e) {
echo "✗ " . $e->getMessage();
}
?>
핵심 정리: PDOException 대처법
PDOException은 "데이터베이스가 문제다"라는 신호다. 하지만 원인은 6가지 이상 있을 수 있다(연결 실패, 데이터베이스 없음, 계정 오류, 쿼리 오류, 연결 풀 고갈, 타임아웃). 각 원인마다 다른 해결책이 필요하고, 에러 메시지를 정확히 읽어야 디버깅이 빠르다. 특히 환경 변수로 설정값을 관리하고, 예외를 try-catch로 안전하게 처리하고, 에러 로그를 남기는 습관이 모여서 "언제 터질지 모르는 서비스"를 "안정적인 서비스"로 만든다는 점을 잊지 말자. 이 글의 "완벽한 DB 연결 함수" 부분을 그대로 복사해서 당신의 프로젝트에 적용하면, 앞으로 만날 대부분의 PDO 에러를 미리 막을 수 있을 것이다.