PHP에서 데이터베이스 작업을 하다 보면 갑자기 화면이 하얀색으로 변하면서 'Fatal error: Uncaught PDOException' 메시지가 뜬다. 에러 메시지를 읽어보면 Connection refused니 Access denied니 하는데, 정확히 어디가 문제인지 모르겠고, 같은 코드가 로컬에선 잘 돌아가니까 더 답답하다. 다만 대부분의 개발자들은 이 에러의 정확한 원인이 뭔지 이해하지 못한 채 데이터베이스 설정을 이것저것 만지다가 우연히 고친다. 이번에는 PDO 연결 실패의 주요 원인 5가지를 정확히 분석하고, 각각 어떻게 진단하고 해결하는지 완벽하게 정리해서 소개하겠다.
// ✗ 잘못된 코드: 에러 처리 없음
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'password');
위 코드에서 연결이 실패하면 그대로 크래시가 난다. 하지만 실제로는 원인을 파악해야 고칠 수 있으니까, 먼저 에러를 잡아서 메시지를 확인해야 한다.
// ✓ 올바른 코드: try-catch로 에러 처리
try {
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'password');
} catch (PDOException $e) {
echo "연결 실패: " . $e->getMessage();
exit;
}
이제 getMessage()로 구체적인 에러 메시지가 보인다. 이 메시지가 문제를 푸는 열쇠다.
가장 흔한 원인이다. 데이터베이스 서버가 그 주소에 없거나 포트 번호가 틀렸을 때 이런 에러가 난다.
// ✗ 호스트명 틀림
$pdo = new PDO('mysql:host=localhost;port=3306;dbname=mydb', 'root', 'password');
// 결과: "SQLSTATE[HY000] [2002] No such file or directory"
// ✓ 호스트명 확인 후 연결
// 터미널에서 먼저 확인
// mysql -h 127.0.0.1 -P 3306 -u root -p
$pdo = new PDO('mysql:host=127.0.0.1;port=3306;dbname=mydb', 'root', 'password');
로컬에서는 'localhost'가 유닉스 소켓을 사용하지만, 원격 서버는 'host:port' 형식이어야 한다. TCP 연결을 명확하게 하려면 'localhost' 대신 '127.0.0.1'을 쓰는 게 더 안전하다.
호스트는 맞는데 인증 정보가 틀렸을 때다. 특히 설정 파일에서 환경변수를 읽거나 네이밍을 실수했을 때 자주 난다.
// ✗ 잘못된 인증 정보
$pdo = new PDO(
'mysql:host=localhost;dbname=mydb',
'root',
'wrong_password' // 비밀번호 틀림
);
// 결과: "SQLSTATE[28000] [1045] Access denied for user 'root'@'localhost'"
// ✓ 환경변수에서 읽기
$host = getenv('DB_HOST') ?: 'localhost';
$user = getenv('DB_USER') ?: 'root';
$pass = getenv('DB_PASS') ?: '';
$pdo = new PDO(
"mysql:host=$host;dbname=mydb",
$user,
$pass
);
배포 환경에서는 반드시 환경변수나 .env 파일에서 읽어야 한다. 소스코드에 비밀번호를 하드코딩하면 버전 관리 시스템에 올라가는 보안 위험이 생긴다.
호스트, 포트, 인증 정보는 맞는데 데이터베이스 이름이 없을 때다.
// ✗ 존재하지 않는 데이터베이스
$pdo = new PDO(
'mysql:host=localhost;dbname=nonexistent_db',
'root',
'password'
);
// 결과: "SQLSTATE[42000] [1049] Unknown database 'nonexistent_db'"
// ✓ 데이터베이스 이름 확인
// 터미널에서
// mysql -u root -p -e "SHOW DATABASES;"
$pdo = new PDO(
'mysql:host=localhost;dbname=mydb',
'root',
'password'
);
연결 정보는 모두 맞는데 실제로 MySQL 서버 프로세스가 돌아가고 있지 않을 때다.
// 서버가 안 떠있으면
// "SQLSTATE[HY000] [2002] No such file or directory" 또는
// "SQLSTATE[HY000] [2003] Can't connect to MySQL server on 'localhost'"
// 터미널에서 확인
// systemctl status mysql (Linux)
// brew services list (Mac)
// or ps aux | grep mysql
// Mac에서 Homebrew로 설치했다면
// brew services start mysql
연결은 성공하지만 문자 인코딩이 제대로 설정되지 않으면 나중에 한글이 깨진다. 이건 PDOException을 안 던지고 조용히 데이터를 망친다.
// ✗ 인코딩 미설정
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'root', 'password');
// ✓ 연결 직후 인코딩 명시
$pdo = new PDO(
'mysql:host=localhost;dbname=mydb;charset=utf8mb4',
'root',
'password',
array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4')
);
매번 try-catch를 반복하는 건 비효율적이니, 재사용 가능한 데이터베이스 연결 클래스를 만들어두자.
class Database {
private static $instance = null;
private $pdo;
private function __construct() {
$host = getenv('DB_HOST') ?: 'localhost';
$dbname = getenv('DB_NAME') ?: 'mydb';
$user = getenv('DB_USER') ?: 'root';
$pass = getenv('DB_PASS') ?: '';
try {
$this->pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$user,
$pass,
array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4'
)
);
} catch (PDOException $e) {
error_log("DB 연결 실패: " . $e->getMessage());
die("데이터베이스 연결에 실패했습니다.");
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
return $this->pdo;
}
}
// 사용
$db = Database::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->query('SELECT * FROM users LIMIT 1');
싱글톤 패턴으로 만들면 애플리케이션 전체에서 데이터베이스 연결을 한 번만 생성하고 재사용할 수 있다. 또한 에러 로깅을 해두면 나중에 운영 단계에서 문제를 추적하기 쉬워진다.
연결에 자꾸 실패하면, 다음 순서로 진단해보자.
| 확인 순서 | 명령어 | 무엇을 확인하는가 |
|---|---|---|
| 1. 서버 실행 | systemctl status mysql | MySQL 데몬이 떠있는가 |
| 2. 연결 테스트 | mysql -h 127.0.0.1 -u root -p | 터미널에서 수동 연결 |
| 3. 포트 확인 | netstat -an | grep 3306 | 포트 3306 열려있는가 |
| 4. 사용자 권한 | mysql -u root -p -e "SELECT User, Host FROM mysql.user;" | 사용자가 올바른 호스트에서 인증되었는가 |
| 5. 데이터베이스 | mysql -u root -p -e "SHOW DATABASES;" | 데이터베이스가 정말 존재하는가 |
PDOException은 겉으로는 막연해 보이지만, 실제로는 호스트, 포트, 인증, 데이터베이스 이름 중 하나가 빠뜨렸을 가능성이 99%다. 에러 메시지를 꼼꼼히 읽고 try-catch로 감싸서 구체적인 메시지를 출력하는 습관이, 이 흔한 실수를 빠르게 잡아내는 가장 확실한 방법이다. 이 글의 디버깅 표를 참고해 체계적으로 점검하면, 언제 뭐가 빠졌는지 금방 알 수 있을 것이다.