PHP에서 데이터베이스 작업을 하다 보면 갑자기 화면이 하얀색으로 변하면서 'Fatal error: Uncaught PDOException' 메시지가 뜬다. 에러 메시지를 읽어보면 Connection refused니 Access denied니 하는데, 정확히 어디가 문제인지 모르겠고, 같은 코드가 로컬에선 잘 돌아가니까 더 답답하다. 다만 대부분의 개발자들은 이 에러의 정확한 원인이 뭔지 이해하지 못한 채 데이터베이스 설정을 이것저것 만지다가 우연히 고친다. 이번에는 PDO 연결 실패의 주요 원인 5가지를 정확히 분석하고, 각각 어떻게 진단하고 해결하는지 완벽하게 정리해서 소개하겠다.

 

PDOException이란 뭔가
PDO는 PHP Data Objects의 약자로, 데이터베이스와 통신하는 표준 인터페이스다. MySQL, PostgreSQL, SQLite 등 다양한 데이터베이스를 같은 방식으로 다룰 수 있다는 게 장점인데, 문제가 생기면 이 PDOException이 날아온다. 보통 'new PDO()' 부분에서 발생한다.
// ✗ 잘못된 코드: 에러 처리 없음
$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 연결 실패의 5가지 원인과 진단
1. 호스트명/포트 오류 (SQLSTATE HY000)

가장 흔한 원인이다. 데이터베이스 서버가 그 주소에 없거나 포트 번호가 틀렸을 때 이런 에러가 난다.

// ✗ 호스트명 틀림
$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'을 쓰는 게 더 안전하다.

2. 사용자명/비밀번호 오류 (SQLSTATE 28000)

호스트는 맞는데 인증 정보가 틀렸을 때다. 특히 설정 파일에서 환경변수를 읽거나 네이밍을 실수했을 때 자주 난다.

// ✗ 잘못된 인증 정보
$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 파일에서 읽어야 한다. 소스코드에 비밀번호를 하드코딩하면 버전 관리 시스템에 올라가는 보안 위험이 생긴다.

3. 데이터베이스 미존재 (SQLSTATE 42000)

호스트, 포트, 인증 정보는 맞는데 데이터베이스 이름이 없을 때다.

// ✗ 존재하지 않는 데이터베이스
$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'
);
4. MySQL 서버 미실행 (SQLSTATE HY000)

연결 정보는 모두 맞는데 실제로 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
5. 인코딩 미설정 (데이터 손상)

연결은 성공하지만 문자 인코딩이 제대로 설정되지 않으면 나중에 한글이 깨진다. 이건 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 mysqlMySQL 데몬이 떠있는가
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로 감싸서 구체적인 메시지를 출력하는 습관이, 이 흔한 실수를 빠르게 잡아내는 가장 확실한 방법이다. 이 글의 디버깅 표를 참고해 체계적으로 점검하면, 언제 뭐가 빠졌는지 금방 알 수 있을 것이다.