PHP 5 Data Object (PDO) Abstraction Layer와 오라클
by Wez Furlong, 
wez@php.net

PHP Data Objects(PDO) data abstraction layer의 공동설계자 중 한 명인 Wez Furlong이, 오라클 사용자의 관점에서 바라본 PHP PDO의 개념을 설명해 드립니다.

요구버전-PHP: 5.0
기타 요구버전: Oracle 8 또는 상위 버전의 클라이언트 라이브러리 
PDO for Oracle (Windows) 다운로드: php_pdo.dllphp_pdo_oci.dll
PDO for Oracle (Unix) 다운로드: pdopdo_oci
샘플코드 다운로드


PDO의 소개

PHP는 기본적으로 “자원봉사자”들에 의해 주도되는 프로젝트입니다. 핵심 “contributor”들이 일부 있긴 하지만, 어느 누구도 PHP를 개발하는 대가로 생계를 유지하고 있지는 않습니다. PHP 프로젝트에 참여하는 이들이 전세계에 퍼져 있는 상황에서, 장기적인 목표를 세우고 개발자 간의 협력관계를 구축해 가는 것은 쉽지 않은 일입니다. 이러한 이유로, PHP 프로젝트는 개인적인 동기와 단기적인 요구사항에 의해 주도되는 경향을 보입니다. 어떤 이들은 단지 “실험”을 목적으로 또는 “내일의 데드라인을 맞추기 위해” PHP 개발에 참여합니다. 이러한 노력을 통해 PHP가 점차적으로 성장해 오긴 했지만, 장기적인 관점에서 볼 때 충분한 일관성을 얻지 못한 것이 사실입니다. 데이타베이스 extension이 좋은 예입니다.

PHP에서 사용되는 데이타베이스 extension(oci, mysql, postgresql, mssql 등) 간의 일관성은 심각하게 결여된 상태이며, 경우에 따라서는 같은 extension 안에서도 일관성이 부족한 경우가 발견되기도 합니다. 대부분의 PHP 데이타베이스 extension은 서로 다른 코드를 사용하고 있으며, 하부 데이타베이스 API에 대한 종속성이 강합니다. 이러한 문제는 코드의 유지보수를 어렵게 하며, 핵심 PHP 개발자 인력이 극히 제한된 현실에서 PHP의 미래에 어두운 그림자를 드리우고 있습니다.

2003년 독일에서 열린 LinuxTag 컨퍼런스 행사에서, PHP 데이타베이스 extension 관리 담당자들이 한자리에 모여 PHP의 미래에 대해 논의하는 자리가 만들어졌습니다. PHP의 무작위적인 진화 방식에 대한 문제를 논의하는 과정에서, 우리는 PHP의 데이타베이스 액세스에 관련한 몇 가지 목표를 설정할 수 있었습니다:

  • 명확하게 정의되고 사용이 편리한, “lightweight” API를 제공한다.
  • 여러 RDMBS 라이브러리들이 공통적으로 제공하는 기능들을 통합하되, 각 라이브러리가 제공하는 고급 기능을 제외시키지 않는다.
  • • 추상화/호환성에 관련한, 보다 무거운 기능들을 PHP 스크립트를 통해 옵션으로 제공한다.

우리는 이러한 개념을 PHP Data Objects (PDO)라 부르기로 했습니다. PDO라는 이름에는, API를 구현하는 과정에서 (PHP 5의 심장 역할을 담당하는) Zend Engine 2가 제공하는 고급 OO 기능을 활용할 수 있을 것이라는 우리의 기대가 담겨 있기도 합니다.

PHP에서 데이타 추상화 계층(data abstraction layer)의 개념은 전혀 새로운 것이 아닙니다. Google 검색 결과, “PHP database abstraction”을 주제로 한 게시물이 무려 83,200개나 발견되기도 했습니다. 하지만 실제적인 용도로 써드 파티 abstraction layer를 적용하려고 시도해 본 개발자라면, 모든 제품의 기능이 지나치게 무겁게 설계되어 있다는 사실을 확인하였을 것입니다. 어떤 제품은 배우기 어렵고, 어떤 제품은 인터페이스가 너무 느리거나, 어던 제품은 네이티브 데이타베이스 API에 매개변수가 전달되기까지 지나치게 많은 수의 스크립트 함수를 거쳐야 합니다. 또는 이러한 문제가 동시다발적으로 발견되는 경우도 있습니다.

그렇다면 이러한 문제의 근본적인 원인은 무엇일까요? 벤더들은 지나치게 많은 것을 바라며, 때로는 불가능한 것을 가능하게 만들려고 시도하기까지 합니다. 반면 우리는 공통적인 데이타베이스 API 기능을 우선적으로 구현하고 PDO 드라이버의 정규 extension 함수를 통해 각 제품의 기능을 활용할 수 있게 하는, 실용적인 접근방식을 취하고자 합니다.


PDO가 필요한 이유?

새로운 database abstraction extension에 대한 루머를 접한 대부분의 개발자들은 먼저, 추상화(abstraction)와 관련하여 어떤 새로운 기능이 추가될 것인지를 궁금해 합니다. 그들은 “SQL의 파싱을 통해 백-엔드 dialect로 변환하는 작업이 가능할 것인가?”, “X 기능과 Y 기능을 어떻게 추상화할 것인가?”와 같은 질문을 던집니다. 많은 사람들이 놀랄 만한 사실은, 우리가 실제로 이러한 부분에 대해 깊이 고민하지 않고 있다는 것입니다. 우리는 활용 가능한 공통 기능을 극단적으로 제한하지 않는 이상, PDO를 통해 완벽한 일관성(uniformity)을 얻는 것은 불가능하다는 결론을 내렸습니다.

PDO가 완전한 형태의 abstraction layer로 활용될 수 없다면, 도대체 그 활용 가치는 어디에 있는 것일까요?

  • 성능. 우리는 기존 database extension의 성공/실패 사례를 타산지석으로 활용하고자 합니다. PDO의 모든 코드는 새롭게 작성되며, PHP5 환경을 기반으로 성능 개선 효과를 극대화할 수 있도록 설계됩니다.
  • 기능. PDO는 공통 데이타베이스 기능을 기반 환경으로 제공하는 한편, 각 RDBMS 제품의 독특한 기능을 편리하게 접근할 수 있는 환경을 제공하도록 설계되었습니다.
  • 편의성. PDO는 데이타베이스 환경에서의 작업 편의성을 개선할 수 있도록 설계되었습니다. 개발자는 API에 구애 받지 않고 독립적인 코드를 작성하는 한편, 각 함수 호출의 역할을 명확하게 정의할 수 있습니다.
  • 런타임 확장 지원. PDO extension은 모듈러 형태로 구현되며, PHP 배포본을 다시 컴파일하거나 재설치하지 않고도 런타임 환경에서 데이타베이스 드라이버를 로드할 수 있습니다. 예를 들어, PDO_OCI extension은 PDO extension을 위한 오라클 데이타베이스 API를 구현하고 있습니다. 그 밖에도 MySQL, PostgreSQL, ODBC, Firebird 등을 위한 드라이버가 현재 개발 중에 있습니다.

여러분 모두 PDO가 PEAR DB 또는 ADODB. 와 같은 abstraction layer와 어떤 차이를 갖는지 궁금하게 생각하실 것입니다. PDO는 API 측면, 또는 성능 측면에서 이 두 가지 제품보다 가벼운 형태로 구현됩니다. 하지만 데이타베이스 백엔드 간의 일관성(uniformity) 면에서는 이 두 제품의 구현 수준을 따라가지 못합니다. (예를 들어, PEAR MDB 2의 경우 다양한 호환성(portability) 문제를 해결하기 위한 부가 기능을 지원하고 있습니다.)


PDO는 어디에서 구할 수 있는가?

PDO는 PECL(“페클”로 발음합니다) PHP extension repository에서 다운로드할 수 있습니다. Linux 환경의 사용자는 아래에 설명된 내용을 따라 셋업을 진행할 수 있습니다. (Windows 환경에서의 설치 방법에 대한 설명도 뒷부분에서 제공됩니다.)

PDO와 PDO 드라이버는 현재 “알파(alpha)” 버전의 상태로 제공되고 있음을 참고하시기 바랍니다. 기능상의 심각한 버그는 없을 것으로 판단하고 있지만, 패키지의 기능이 완전하지 않으며 앞으로도 많은 기능이 추가될 예정입니다. 따라서 현재 버전을 이용한 테스트는 적극 권장되지만, 운영 환경에의 적용은 권장되지 않습니다.


Unix/Linux 환경의 설치

아직 PHP5를 테스트해 보지 않은 사용자라면, 먼저 시간을 내어 최신 뉴스와 보도 자료를 확인해 보시기 바랍니다. UNIX 머신에서는 libxml2를 설치 또는 업그레이드해야 할 수 있습니다; libxml2가 설치되어 있지 않은 경우 “pear” package manager가 정상적으로 동작하지 않으며, 따라서 PDO의 설치에 어려움을 겪을 수 있습니다. 다음으로 PHP 5를 다운로드하여 컴파일 한 후 설치합니다. 설치 시에는 (PHP 4 설치 환경이 사용하는) “/usr/local/"을 제외한 다른 경로를 사용하도록 합니다:

% ./configure --prefix=/usr/local/php5 --with-zlib [other options here]
% make install

이제 “pear” 툴을 이용하여 PDO와 Oracle driver for PDO를 다운로드하고 설치할 차례입니다. PDO는 현재 알파 버전으로 제공되므로, 디폴트 설정에서는 pear 툴이 패키지를 다운로드하지 않습니다. 패키지 명 뒤에 “-alpha”를 추가하여, 알파 버전을 설치해도 무방하다는 사실을 pear 툴에 확인시킵니다:

% PATH="/technology/usr/local/php5/bin:$PATH"
% pear install PDO-alpha

또 PHP 5의 php.ini 파일을 이용해 PHP가 PDO 드라이버를 로드하도록 설정할 필요가 있습니다. 위에서 필자가 사용한 것과 동일한 명령을 사용했다면, php.ini파일은 “/usr/local/php5/lib/php.ini”에 위치하고 있을 것입니다. 파일에 아래 라인을 추가합니다:

extension=pdo.so

이제 데이타베이스 드라이버를 다운로드할 차례입니다. 오라클 드라이버의 이름은 PDO_OCI입니다. 다음과 같이 명령을 실행합니다:

% pear install PDO_OCI-alpha

또 php.ini 파일을 통해 드라이버를 로드하는 과정이 필요합니다. 위에 추가한 라인 아래에 아래 설정을 추가합니다:

extension=pdo_oci.so

이제 제대로 설정이 완료되었는지 확인합니다:

% php -m

모듈 리스트에 PDO와 PDO_OCI가 추가되었음을 확인하실 수 있을 것입니다.


방화벽 문제의 해결?

방화벽이 설치된 환경에서는, pear 툴을 사용하여 패키지를 설치하는 과정에서 문제가 발생할 수 있습니다. 이러한 경우, 아래 명령을 이용하여 수동으로 패키지를 다운로드하여 설치해야 합니다:

% wget http://pecl.php.net/get/PDO
% pear install PDO-0.1.1.tgz
[ add extension=pdo.so to php.ini ]

% wget http://pecl.php.net/get/PDO_OCI
% pear install PDO_OCI-0.1.tgz
[ add extension=pdo_oci.so to php.ini ]

위의 두 가지 경우 모두 패키지가 다운로드 된 후 “pear install” 명령을 실행하였음에 주의하시기 바랍니다. 위 명령에서 사용된 버전 넘버는 현재 시점을 기준으로 한 것이며, 향후 변경될 수 있습니다.


Windows 환경의 설치

Windows 환경을 사용하는 경우, 아래와 같은 절차를 거쳐 패키지를 설치합니다:

  1. http://www.php.net/downloads.php#v5에서 PHP 5를 다운로드하고 C:\php5 디렉토리에 압축을 풉니다.
  2. http://snaps.php.net/win32/PECL_5_0/php_pdo.dll와 http://snaps.php.net/win32/PECL_5_0/php_pdo_oci.dll에서 PDO와 PDO_OCI를 각각 다운로드한 후, 파일을 C:\php5\ext 경로에 저장합니다. 또는, PHP 5 다운로드 페이지에 있는 "Collection of PECL modules for PHP 5.0.0" zip 파일에서 모든 종류의 PDO 드라이버를 한꺼번에 다운로드 할 수도 있습니다.
  3. C:\php5\php.ini 파일에 아래 라인을 추가합니다:

    extension=php_pdo.dll
    extension=php_pdo_oci.dll

php.ini 파일을 편집할 때, 다른 PDO 드라이버가 로드되기 전에 PDO extension을 먼저 로드하는 것이 중요합니다. 그렇게 하지 않는 경우 초기화 작업이 성공적으로 완료되지 않으며 에러가 발생하게 됩니다.

Windows 디렉토리에 PHP 4의 php.ini 파일이 이미 존재하고 있는 경우에도 문제가 발생할 수 있습니다. 가장 좋은 방법은 php.ini을 PHP 4 SAPI 폴더(php4apache.dll 파일이 위치한 폴더)로 이동함으로써 PHP 4 설치 환경을 격리시키는 것입니다. 또 PHP 5 배포본에 포함된 문서가 아직 완전히 업데이트된 상태가 아니라는 사실을 참고하시기 바랍니다. 문서에서 설명하는 대로 DLL 파일을 windows 폴더 또는 system 폴더에 복사해서는 안됩니다. Apache 환경에서 DLL 로딩 과정에 관련한 에러가 발생하는 경우에는 C:\php5 경로를 PATH 변수에 추가해 줍니다. 또 PHP의 CGI 버전의 이름이 php-cgi.exe로 변경되었음을 참고하시기 바랍니다.


PDO로의 연결

제일 먼저, 데이타베이스 핸들(database handle)로 사용할 PDO 클래스의 인스턴스를 생성합니다. 사용자는 어떤 데이타베이스 드라이버를 사용하든 관계없이, 연결 시에 PDO 클래스 명을 사용하게 됩니다. constructor에 제공해야 하는 매개변수로는 Data Source Name (DSN), 사용자명, 패스워드가 있습니다. PDO에서는 “PDO 드라이버 명”, “:(콜론)”, “드라이버 관련 추가 정보”의 순으로 DSN을 설정합니다. 아래 예제에서는 OCI 드라이버를 로드하는 과정에서 추가적인 정보는 명시하지 않았으므로 디폴트 데이타베이스가 사용되게 됩니다. 다른 드라이버(ODBC 드라이버 등)를 사용하고자 하는 경우에는 첫 번째 콜론(:) 기호 이후에 ODBC DSN을 명시하도록 합니다. MYSQL 드라이버의 경우에도 DSN 설정 방법이 달라집니다.

드라이버가 정상적으로 로드되지 않은 경우, 또는 연결에 실패한 경우에는 PDOException 에러가 발생합니다.

<?php
    try {
        $dbh = new PDO("OCI:", "scott", "tiger");
    } catch (PDOException $e) {
        echo "Failed to obtain database handle " . $e->getMessage();
    }
?>

연결 구문에서 두 가지 매개변수를 추가로 설정할 수 있습니다. 그 첫 번째는 데이타베이스 명(database name)이며 두 번째는 문자 셋(character set)입니다. 이 두 가지 매개변수는 oci8 extension 함수 ociconnect() 또는 ociplogon()에서 사용된 3번째/4번째 매개변수에 대응됩니다. 특정 문자 셋을 사용하여 데이타베이스에 연결하고자 하는 경우에는 다음과 같이 실행합니다:

<?php
    try {
        $dbh = new PDO("OCI:dbname=accounts;charset=UTF-8", "scott", "tiger");
    } catch (PDOException $e) {
        echo "Failed to obtain database handle " . $e->getMessage();
    }
?>

위와 같이 try.catch 컨트롤 구문을 삽입하고 상위 애플리케이션에 exception 처리를 위한 코드를 추가하지 않은 경우, 스크립트는 데이타베이스 연결에 실패하는 경우 곧바로 종료하게 됩니다.


연결 관리

아직까지 PDO는 독자적인 연결 관리(connection management) 기능을 전혀 제공하고 있지 않습니다. 따라서 새로운 PDO가 호출될 때마다 데이타베이스에 새로운 연결이 생성되게 됩니다. 이렇게 생성된 연결은 $dbh 변수가 범위 밖으로 벗어나거나, 변수에 NULL 값이 할당되는 시점에 할당종료(release)됩니다.

<?php
    try {
        $dbh = new PDO("OCI:dbname=accounts;charset=UTF-8", "scott", "tiger");
    } catch (PDOException $e) {
        echo "Failed to obtain database handle " . $e->getMessage();
        exit;
    }
    // do something with the database here
    // ...
    
    // now we are done, release the connection
    $dbh = null;
?>

조만간 PDO에 연결 캐싱(connection caching) 기능도 추가될 예정입니다. 따라서 기존 서버에 설정된 연결을 재사용하고, 현재 사용중인 연결에서 유휴 상태인 로그인을 재활용하는 작업이 가능하게 될 것입니다. 캐시 연결 모드(cache connection mode)를 사용하는 경우, 할당 종료된 $dbh 변수는 다른 연결에 의해 재사용되게 됩니다.

ODBC 드라이버를 이용하여 오라클에 접근하는 경우에는, PDO_ODBC 드라이버가 ODBC 커넥션 풀링(connection pooling)을 기본적으로 지원한다는 사실을 참고하시기 바랍니다.


PDO in action

프로그래밍 API를 이해하는 가장 좋은 방법은 API의 실제 적용 결과를 눈으로 직접 확인하는 것입니다. 그럼 지금부터 첨부된 데모를 실행하여 배치 업데이트 작업을 수행해 보겠습니다 (샘플코드 참고)

오라클과의 연결이 성공적으로 완료되었다면, 다음으로는 데이타를 저장할 테이블을 생성합니다. 이번 예제에서는 PHP extension과 개발자의 목록을 데이타베이스에 저장하기로 합니다. 데이타베이스 핸들 오브젝트의 exec() 메소드를 이용하면 결과 셋을 반환하지 않고 단 한 번에 쿼리를 신속하게 완료할 수 있습니다. 이번 예제에서도 exec() 메소드를 사용하여 CREATE TABLE 쿼리를 실행합니다.

PHP extension과 개발자의 목록은 CSV 파일의 형태로 제공됩니다 ("credits.csv" 참고). 따라서 CSV 파일로부터 데이타를 배치 임포트(batch import) 하는 전형적인 시나리오를 테스트해 볼 수 있을 것입니다. 또 오라클의 prepared 구문과 bound 매개변수를 이용하여 임포트 작업의 효율성을 개선하였습니다. 본격적으로 예제를 실행하기 전에, PDO가 트랜잭션을 어떻게 관리하는지 설명하기로 하겠습니다.


PDO의 트랜잭션 관리

오라클에 연결된 사용자는 기본적으로 명시적 트랜잭션(explicit transaction) 환경에서 작업하며, 따라서 트랜잭션이 명시적으로 커밋(commit) 되기 전까지 변경 내용은 실제적인 효력을 발생시키지 않습니다. 트랜잭션이 제공하는 기본적인 혜택(ACID—Atomicity, Consistency, Isolation, Durability)을 고려하지 않더라도, 개별적인 변경 내역이 입력될 때마다 인덱스를 비롯한 내부 구조를 변경할 필요가 없다는 점에서 이것은 매우 합리적인 방식입니다. 구문의 실행속도도 한층 빨라집니다.

하지만 모든 데이타베이스 벤더가 명시적 트랜잭션 환경을 지원하는 것은 아닙니다. 이러한 이유 때문에, 데이타베이스간의 호환성을 고려하여 PDO는 디폴트 환경에서 “auto-commit” 모드로 동작하도록 설계되었습니다. “auto-commit” 모드가 활성화된 경우, 데이타베이스 드라이버는 업데이트가 성공할 때마다 자동으로 커밋을 수행합니다. 다만, “$dbh->beginTransaction()”을 호출한 경우에는, “$dbh->commit()” 또는 “$dbh->rollBack()” 이 호출되기 전까지 auto-commit 모드가 비활성화 됩니다.

PHP에 에러가 발생한 경우, 트랜잭션이 완료되지 않은 상태에서 스크립트가 종료된 경우, 또는 데이타베이스 핸들이 종료된 경우, PDO는 “$dbh->rollBack()”을 자동으로 호출하여 롤백 작업을 수행합니다. 이러한 동작 방식은 불완전하게 종료된 트랜잭션으로 인한 리스크를 최소화하기 위한 가장 효과적인 방법이며, 트랜잭션 처리를 위한 표준적인 방법으로 활용되고 있습니다.


Prepared statement와 저장 프로시저

PDO는 오라클과 같은 방식의 문법을 통해. SQL에 변수를 바인딩하기 위한 prepared statement를 지원합니다. (oci8 extension의 ocibindbyname()도 유사한 방식으로 구현되었습니다.) 또 PDO는 다른 데이타베이스(예: ODBC)를 위한 “named placeholder"의 에뮬레이션 기능을 제공하며, 심지어 이러한 기능을 내부적으로 구현하고 있지 않은 데이타베이스(예: MySQL)에서도 prepared statement과 bound 매개변수를 에뮬레이션 하는 것이 가능합니다. 이것은 매우 발전적인 기능이며, 개발자들은 데이타베이스 플랫폼에 구애 받지 않고 “엔터프라이즈 레벨”의 데이타베이스 애플리케이션을 작성할 수 있습니다.

PDO를 이용하여 prepared statement을 작성할 때에는, 데이타베이스 핸들의 prepare() 메소드를 호출하는 방법이 사용됩니다. prepare() 메소드가 반환하는 statement handle 오브젝트를 이용하여 매개변수를 바인딩하고 구문을 실행할 수 있습니다. 아래 예제에서는 ":extension"과 ":name"이라는 두 개의 named placeholder를 사용하였습니다. 여기서 “.extension”은 .CSV 파일의 PHP extension name에, “.name”은 .CSV 파일의 개발자 명에 대응됩니다.

$stmt = $dbh->prepare("INSERT INTO CREDITS (extension, name) VALUES (:extension, :name)");

prepared statement의 작성이 완료되었다면, bindParam() 메소드를 이용하여 정의된 두 개의 매개변수를 PHP 변수("$extension" ,"$name")에 대응시킬 수 있습니다. (oci8 extension의 ocibindbyname()와 유사한 방식으로 볼 수 있습니다). 또 데이타가 string 포맷을 가지며, 최대 64개의 문자로 구성됨을 오라클 데이타베이스에 통보합니다.

$stmt->bindParam(':extension', $extension, PDO_PARAM_STR, 64);
$stmt->bindParam(':name',      $name,      PDO_PARAM_STR, 64);

이제 데이타 입력작업을 시작할 준비가 완료되었습니다. CSV 파일을 열고 데이타를 읽어오기만 하면 됩니다. 이 작업은 fopen() 함수와 fgetcsv() 함수를 이용하여 수행됩니다. 다음에는 PHP list() construct를 이용하여 CSV 파일의 각 컬럼을 "$extension" 변수와 "$name" 변수에 할당합니다. 이미 이 변수들이 구문에 바인드 되어 있기 때문에, 여기에서는 statement 오브젝트의 execute() 메소드를 호출하여 INSERT 작업을 시작하기만 하면 됩니다. 이와 같은 방법은 편리하기도 하거니와 성능 면에서도 바람직합니다 코드는 트랜잭션 내부의 루프에서 2개의 라인을 반복 수행하는 구조를 가지며, 파일의 마지막 부분에 도달하면 데이타베이스 핸들의 commit() 메소드를 사용하여 변경 사항을 커밋 하게 됩니다.

입력 매개변수만을 전달하는 경우, 특히 사용되는 입력 매개변수의 수가 많은 경우에는 아래와 같은 코드를 사용하여 “$stmt->bindParam()” 호출 과정을 생략할 수도 있습니다.

$stmt = $dbh->prepare("INSERT INTO CREDITS (extension, name) VALUES (:extension, :name)");
$stmt->execute(array(':extension' => $extension, ':name' => $name));

또 bindParam을 이용하여 저장 프로시저(stored procedure)의 입력/출력 매개변수를 설정할 수도 있습니다. 이 경우 문법(syntax) 자체는 동일하며, 쿼리가 약간 수정되게 됩니다. 아래 코드는 "sp_add_item"이라는 이름의 저장 프로시저를 호출하는 과정을 예시하고 있습니다. $item_name은 입력 과정에서 설정되며, 결과가 반환된 후 $error_code가 업데이트됩니다.

$stmt = $dbh->prepare("begin sp_add_item(:item_name, :error_code); end");
$stmt->bindParam(':item_name', $item_name, PDO_PARAM_STR,  12);
$stmt->bindParam(':error_code', $error_code, PDO_PARAM_STR, 12);
$stmt->execute();

데이타 가져오기

PDO를 이용하여 데이타를 가져오는 과정은, 데이타를 INSERT 또는 UPDATE하는 과정과 유사합니다. 다만 쿼리를 수행한 후 반복적으로 fetch() 메소드를 호출하여 결과 셋의 다음 로우(row)를 가져와야 한다는 점이 다를 뿐입니다. 데이타를 조회하는 가장 간단한 방법이 아래 코드와 같습니다. 이 과정에서 WHERE 절을 변경하기 위해 bind 매개변수를 활용할 수 있다는 점을 참고하시기 바랍니다. 이때 사용하게 되는 문법은 앞에서 설명한 bindParam() 코드 적용 예와 동일합니다.

$stmt = $dbh->prepare("SELECT extension, name from CREDITS");
if ($stmt->execute()) {
    while ($row = stmt->fetch()) {
         print_r($row);
    }
}

PDO는 다양한 “data fetch” 모드를 제공하고 있습니다. 개발자는 편의성과 성능을 고려하여 적절한 방법을 선택하면 됩니다. 데이타 조회 방법을 선택하기 위해서는 fetch() 메소드의 매개변수로 아래 옵션을 명시해야 합니다. 
(반환되는 결과는 여러분이 작성한 스크립트에 맞게 수정할 있습니다.):

  • PDO_FETCH_NUM — 각각의 로우를 통해 “0-base” column position(첫 번째 컬럼이 0번째 엘리먼트)으로 인덱스된 어레이가 반환합니다.

    while ($row = $stmt->fetch(PDO_FETCH_NUM)) {
    	printf("Extension %s, by %s<br>", $row[0], $row[1]);
    }
    

  • PDO_FETCH_ASSOC — 각각의 로우를 통해 column name으로 인덱스된 어레이가 반환됩니다 .

    while ($row = $stmt->fetch(PDO_FETCH_ASSOC)) {
    	echo "Extension $row[EXTENSION] by $row[NAME]<br>";
    }
    

  • PDO_FETCH_BOTH — 각각의 로우를 통해 column position과 column name으로 인덱스된 어레이가 반환됩니다. 이 옵션은 위 두 가지 방식의 조합으로 볼 수 있습니다. fetch mode가 명시되지 않은 경우, 디폴트로 이 방법이 사용됩니다.

  • PDO_FETCH_OBJ — 각각의 로우를 통해 column name에 대응되는 property name을 가진 “anonymous” 오브젝트가 반환됩니다.

    while ($row = $stmt->fetch(PDO_FETCH_ASSOC)) {
    	echo "Extension {$row->EXTENSION} by {$row->NAME}<br>";
    }
    

  • PDO_FETCH_LAZY — 각각의 로우를 통해 statement 오브젝트를 참조하는 “overloaded” 오브젝트가 반환됩니다. 이 옵션은 PDO_FETCH_OBJ와 PDO_FETCH_BOTH 의 조합으로 볼 수 있지만, 스크립트에서 PHP 변수에 액세스하는 시점에 변수가 생성된다는 점에서 차이가 있습니다 .

  • PDO_FETCH_BOUND — 각각의 로우가 반환되면서 TRUE 값이 함께 반환됩니다. 이 옵션을 사용하면 불필요한 어레이 또는 오브젝트의 생성을 피할 수 있으므로 bound output column을 사용하는 경우에 유용합니다 (적용 예는 아래를 참고).

어떤 fetch mode를 사용하든 관계없이, 더 이상 가져올 로우(row)가 없는 경우 fetch() 메소드는 FALSE를 반환합니다. .

이제 스크립트의 성능을 최대한으로 쥐어짜내기 위한 몇 가지 팁을 소개 드리려 합니다. 하지만 그 전에 한 가지 경고를 드리겠습니다. 섣부른 최적화 작업을 삼가 하십시오. 그 보다 먼저 명확하고 관리가 용이한 솔루션을 구현하는 것이 우선입니다. 일반적인 웹 환경에서는, 처리해야 하는 데이타의 수가 아주 많지 않은 이상 fetch mode의 변경을 통해 성능 차이를 느끼기는 어렵습니다. 다시 한 번 강조해서 말씀 드립니다. 서로 다른 fetch mode 간의 성능 차이는 미미한 수준입니다. 그냥 여러분의 코드에 가장 적절하다고 판단되는 방법을 사용하시는 것이 무난합니다.

PDO_FETCH_NUM은 성능 면에서 가장 저렴한 방법으로 볼 수 있습니다. 컬럼 데이타에 접근하는 과정에서도 간단한 numeric 조회 작업만이 수행됩니다. PDO_FETCH_OBJ는 OO 문법을 사용하여 데이타 셋의 컬럼을 오브젝트 속성으로서 액세스하는 것을 가능하게 합니다. 하지만 각각의 속성에 액세스할 때 추가적인 hash-lookup이 발생하며, 따라서 그 성능 비용은 PDO_FETCH_ASSOC와 유사한 수준입니다. 이 두 가지 fetch mode는 전체 로우에 대한 복사본을 저장하며, 이로 인해 메모리 사용량이 다소 증가하게 됩니다.

대부분의 데이타베이스 드라이버는 pre-fetch 작업을 통해 일정한 수의 로우를 캐시에 저장하는 방법을 사용합니다. PHP가 캐시에 저장된 로우의 컬럼을 조회하는 경우, 별도의 메모리 영역에 복사본을 저장하는 작업이 수반됩니다. 쿼리를 통해 많은 수의 로우를 처리해야 하는 경우, 특히 복잡한 로직을 통해 특정 컬럼에만 액세스하는 경우라면, PDO_FETCH_LAZY를 이용하여 사용되는 메모리를 대폭적으로 절감할 수 있습니다 (PDO_FETCH_LAZY는 실제로 컬럼에 액세스할 때까지 데이타를 복사하는 작업을 수행하지 않습니다). 하지만 PDO_FETCH_LAZY에서는 각각의 로우에 대한 fetch() 작업을 위해 (오브젝트를 생성/제거하는 오버헤드를 줄이기 위해) 동일한 “lazy object”를 반복적으로 사용한다는 점에 주의해야 합니다. 따라서 오브젝트가 최근의 로우만을 참조하므로 저장된 오브젝트끼리 서로 비교하는 작업이 불가능하며, 이와 같은 작업을 수행하려면 수동으로 필요한 부분을 복사해 두어야만 합니다.

마지막으로 PDO_FETCH_BOUND는 모든 컬럼이 PHP 변수에 바인드 되었으며, 로우 셋(row-set)의 마지막 부분에 도달할 때까지 아무런 작업도 수행할 필요가 없음을 PDO에 알리는 역할을 수행합니다. bound output 컬럼은 개념적으로 볼 때 bound input 매개변수와 유사하지만, bound output column의 경우 모든 종류의 데이타베이스 드라이버를 지원한다는 점에서 차이를 갖습니다. PHP 변수를 특정 컬럼에 바인드하고, execute()를 호출함으로써 PDO가 업데이트 작업을 수행하도록 할 수 있습니다. 이러한 테크닉을 이용하면 컬럼 단위 또는 로우 단위로 수행되는 가상 머신의 op-code를 제거할 수 있습니다 (op-code는 native code에 비해서 성능이 떨어집니다). 하지만 이렇게 하는 경우 코드의 가독성이 떨어진다는 문제가 있으며, 변수 이름을 사용할 때 한층 주의를 기울여야 합니다. 아래 코드는 bound output 컬럼을 이용한 예를 보여주고 있습니다. 여기서 $stmt->bindColumn()를 이용하기 위해 굳이 PDO_FETCH_BOUND를 명시할 필요가 없다는 점을 참고하시기 바랍니다. PDO_FETCH_BOUND는 바인드된 값만을 활용하는 경우에 사용되는 최적화 방법의 하나일 뿐입니다.

$stmt = $dbh->prepare("SELECT extension, name from CREDITS");
if ($stmt->execute()) {
    $stmt->bindColumn('EXTENSION', $extension);
    $stmt->bindColumn('NAME',      $name);
    while ($stmt->fetch(PDO_FETCH_BOUND)) {
        echo "Extension: $extension, Author: $name\n";
    }
}

호환성 (Portability)

컬럼의 대소문자 표기

PDO는 호환성을 갖는 SQL 구문을 활용하여 “포터블 스크립트(portable script)”를 작성할 수 있는 환경을 제공하는 것을 목표로 하고 있습니다. 이 문서에서 예시된 모든 쿼리는 어떤 PDO 드라이버를 사용하더라도 정상적으로 동작합니다 (단 저장 프로시저의 호출에 관련한 예제 코드는 예외입니다). bound input 변수와 bound output 변수도 동일한 형태로 사용됩니다.

하지만 주의해야 할 점이 있습니다. PDO_FETCH_ASSOC을 이용하여 데이타를 가져오는 경우, 드라이버의 종류에 따라 컬럼 명이 반환되는 방식이 달라집니다. 어떤 드라이버는 강제적으로 대문자(또는 소문자)만을 사용하고, 어떤 드라이버는 쿼리의 설정을 그대로 사용하기도 합니다. PHP 스크립트에서는 어레이 키(array key)가 대소문자를 구분하기 때문에 이것이 문제가 될 수 있습니다. PDO는 스크립트에 반환되는 결과를 정규화하기 위한 속성을 별도로 제공합니다. 아래 코드는 위에서 제시된 PDO_FETCH_BOUND 예제 코드를 수정한 것입니다. 여기에서는 setAttribute() 메소드를 사용하여 PDO가 컬럼 명을 대문자로 반환할 것을 명시하고 있습니다:

$dbh = new PDO('OCI:', 'scott', 'tiger');
$dbh->setAttribute(PDO_ATTR_CASE, PDO_CASE_UPPER);
stmt = $dbh->prepare("SELECT extension, name from CREDITS");
if ($stmt->execute()) {
    $stmt->bindColumn('EXTENSION', $extension);
    $stmt->bindColumn('NAME',      $name);
    while ($stmt->fetch(PDO_FETCH_BOUND)) {
        echo "Extension: $extension, Author: $name\n";
    }
}

PDO_CASE_UPPER 옵션 이외에도 PDO_CASE_LOWER (컬럼 명을 소문자료 표기), 또는 PDO_CASE_NATURAL (디폴트 옵션: 데이타베이스 드라이버가 반환한 컬럼 명을 그대로 사용) 옵션을 적용할 수 있습니다.


에러와 에러 핸들링

“포터블 스크립트”를 구현하는데 장애가 되는 또 한 가지 문제로, 다양한 데이타베이스 핸들러가 반환하는 에러 메시지가 제각각이라는 점을 들 수 있습니다. 어떤 데이타베이스는 매우 다양한 에러 코드를 제공하는 반면, 어떤 데이타베이스는 매우 빈약한 에러 핸들링 기능만을 제공하기도 합니다. PDO는 개발자가 호환성과 관련한 문제로 인해 겪는 업무 부담을 최소화하기 위해, 가능한 한 일관성 있는 에러 코드를 제공하는 것을 목표로 하고 있습니다. 물론 PDO는 드라이버가 제공하는 네이티브 에러 코드와 에러 메시지를 함께 제공하고 있으므로, 에러 매핑이 100% 완벽하지 않은 경우에도 진단 작업을 효과적으로 수행할 수 있습니다.

PHP database extension과 관련한 또 한 가지 문제로 에러 핸들링(error handling)을 이야기할 수 있습니다. 일부 extension의 경우 에러 코드만을 반환하며, 에러 메시지를 확인하려면 별도의 코드를 실행해야 합니다. 또 어떤 extension은 단순히 PHP 경고 메시지만을 출력하기도 합니다. PDO는 기본적으로 세 가지 에러 핸들링 옵션을 제공합니다:

  • PDO_ERRMODE_SILENT
    이것은 디폴트 모드입니다. 사용자에게는 에러 코드만이 반환되며, statement 오브젝트 및 database handle 오브젝트의 errorCode() / errorInfo() 메소드를 이용하여 진단 작업을 수행할 수 있습니다.

    if (!$dbh->exec($sql)) {
    	echo $dbh->errorCode() . "<br>";
    	$info = $dbh->errorInfo();
    	// $info[0] == $dbh->errorCode() unified error code
    	// $info[1] is the driver specific error code
    	// $info[2] is the driver specific error string
    }
    

  • PDO_ERRMODE_WARNING
    에러 코드에 더하여 PDO가 PHP 경고 메시지를 출력합니다. PHP 에러 핸들러를 이용하여 이 메시지를 캡처하고, 애플리케이션에서 설정한 에러 핸들링/로깅 정책에 의해 에러를 중앙집중적으로 처리할 수 있습니다. 또는 브라우저에 에러가 표시되도록 할 수도 있습니다 (내부 테스팅 과정에서 유용한 방법입니다).

  • PDO_ERRMODE_EXCEPTION
    에러 코드에 더하여 PDO가 PDOException을 발생시킵니다. PDOException의 속성에는 에러 코드와 관련 정보가 포함됩니다. 그런 다음 상위 레벨의 코드를 통해 exception을 감지하고 글로벌 exception 핸들러를 통해 처리하거나 스크립트를 강제 종료하도록 할 수 있습니다 (스크립트가 강제 종료되는 경우 완료되지 않은 트랜잭션은 자동으로 롤백 됩니다).

    try {
    	$dbh->exec($sql);
    } catch (PDOException $e) {
    	// display warning message
    	print $e->getMessage();
    	$info = $e->errorInfo;
    	// $info[0] == $e->code; unified error code
    	// $info[1] is the driver specific error code
    	// $info[2] is the driver specific error string
    }
    

세 가지 옵션 중 PDO_ERRMODE_SILENT 모드가 가장 적은 리소스를 사용하지만, 코드 처리가 더 복잡해질 수 있다는 단점이 있습니다.

PDO의 통합 에러 코드에 포함되어 있는 상수가 다음과 같습니다: PDO_ERR_NONE, PDO_ERR_CANT_MAP, PDO_ERR_SYNTAX, PDO_ERR_CONSTRAINT, PDO_ERR_NOT_FOUND, PDO_ERR_ALREADY_EXISTS, PDO_ERR_NOT_IMPLEMENTED, PDO_ERR_MISMATCH, PDO_ERR_TRUNCATED, PDO_ERR_DISCONNECTED.

각 상수의 의미는 굳이 설명할 필요가 없을 것입니다. 단 PDO_ERR_CANT_MAP 코드는, 드라이버가 제공하는 코드를 통합 에러 코드에 매핑하는 작업이 실패했음을 알리는 PDO 전용 코드입니다. 이 에러가 발생한 경우에는 errorInfo() 메소드를 이용하여 드라이버 에러 코드를 확인할 필요가 있습니다.


데이타 타입

PDO는 데이타를 integer 또는 double 타입으로 변환하지 않고 string으로 표시한다는 점에서 “type-agnostic”한 환경을 제공합니다. 여기서 그 이유를 궁금해 하시는 분들이 있을 것입니다. string은 PHP에서 가장 넓은 범위를 지원하는, 가장 정확한 데이타 타입입니다. 데이타를 integer 또는 double로 섣불리 변경하는 경우 반올림 또는 버림이 발생할 수 있습니다. PDO는 데이타를 string으로 표현함으로써, 개발자가 필요한 시기에 데이타 타입 변환 작업을 수행할 수 있도록 배려하였습니다. (PHP에서 casting 명령을 수행하는 경우, 또는 산술 계산식을 수행하는 경우 자동으로 데이타 타입 변환이 수행됩니다).

NULL

결과 셋의 컬럼에 NULL 값이 포함된 경우, PDO는 이 값을 PHP null 값으로 매핑합니다. 오라클은 데이타를 PDO에 반환하는 과정에서 빈 문자열을 NULL로 변환하지만, 다른 데이타베이스의 경우는 그렇지 않습니다. 이로 인해 호환성 문제가 발생할 수 있습니다. PDO에서 제공되는 드라이버 레벨 속성인 PDO_ATTR_ORACLE_NULLS를 이용하면, 다른 데이타베이스 드라이버에서도 오라클 환경을 에뮬레이션 하도록 설정할 수 있습니다:

$dbh = new PDO('OCI:', 'scott', 'tiger');
$dbh->setAttribute(PDO_ATTR_ORACLE_NULLS, true);
// now empty strings will all be converted to NULL in any
// statements opened from this $dbh

PDO의 현재와 미래

PDO는 아직 미완의 기술이지만 매우 빠른 속도로 성숙하고 있습니다. 필자가 이 문서를 작성하고 있는 시점에도, PDO_OCI 드라이버를 통해 Oracle 8 및 상위 버전을 지원하기 위한 작업이 진행 중입니다 (현재 Oracle 8.0 및 9.2의 테스트가 완료되었습니다).

관련 자료
  1. PDO 패키지 관련 정보
  2. 새로운 버그 신고 - 버그의 해결에 도움이 될 수 있는 정보를 제공해 주시면 감사하겠습니다.
  3. PHP extension for Oracle JDeveloper - Oracle JDeveloper 10g 환경에서 PHP 스크립트를 생성, 편집, 실행하는 작업이 한층 용이해집니다.
  4. 히치하이커를 위한 PHP 가이드 (한글) - 처음으로 PHP 애플리케이션을 작성하는 방법에서 PHP의 미래에 이르기까지, PHP에 대한 모든 것을 알려 드립니다.
  5. 오픈 소스 센터 - 이 페이지를 바로가기에 등록하시고 최신 오픈 소스 툴과 언어에 관련한 기술 정보를 수시로 확인하시기 바랍니다.
  6. PHP 사용자 포럼

단기간 내에 구현될 PDO의 주요 기능이 다음과 같습니다:

  • PHP 스트림을 이용한 LOB 지원. LOB 쿼리에서 bound 매개변수를 이용하여 스트림 리소스(파일, 소켓, http 리소스, 압축/필터링된 스트림 등)를 입력/출력 매개변수로 전달할 수 있게 될 것입니다. 또 LOB 속성을 갖는 출력 매개변수는 PHP 스트림의 형태로 조회 및 활용이 가능하며, 따라서 fread(), fwrite(), fseek() 등의 스트림 함수를 이용하여 액세스할 수 있게 됩니다. 현재에는 PDO에 LOB 지원 기능이 전혀 제공되고 있지 않습니다.
  • Persistent connection 및 cached prepared statement. Persistent connection을 이용하면 개별 페이지 요청이 발생할 때마다 데이타베이스 연결을 열고 닫는 작업을 반복하지 않아도 됩니다. Cached prepared statement는 여기에서 한 걸음 더 나아가, 데이타베이스 핸들과 함께 prepared statement의 persistence를 구현할 수 있게 합니다.
  • 커서(cursor). 현재 PDO는 forward-read-only 커서만을 지원하며, 조만간 scrollable cursor, REF-CURSOR, 그리고 커서를 이용한 positioned update, update scrolling cursor 등을 지원할 예정입니다.

PHP 5.1에서는 PDO extension이 디폴트로 활성화된 형태로 지원될 예정입니다. 그 전까지 PHP 5.0 환경에서 PDO가 안정적으로 동작하도록 하는 것이 우리의 목표이지만, 참여자들의 개인적인 사정으로 인해 다소 일정이 지연되어 왔습니다. 최종 버전이 완성될 때까지 우리는 PECL을 통해 PDO를 배포하고, 새롭게 발견된 문제를 해결한 버전을 지속적으로 제공할 것입니다. 그러므로 PDO를 활용하기 위해 PHP 5.1이 발표될 때까지 기다리실 필요는 없을 것입니다.


여러분의 피드백이 필요합니다.

PDO를 테스트하는 과정에서 문제를 발견하셨다면, 별도로 제공되는 bug tracking 소프트웨어를 통해 문제를 알려주시기 바랍니다. 오라클 드라이버 사용자를 위한 페이지가 다음과 같습니다:

http://pecl.php.net/bugs/report.php?package=PDO_OCI

다른 드라이버를 사용하는 경우 URL의 " PDO_OCI" 부분을 해당 드라이버 명으로 대치하시면 됩니다.

PDO를 사용하면서 문제를 경험하셨거나, 특정 기능에 관련한 질문, 또는 기능에 대한 요청사항이 있는 경우, “pecl-dev@lists.php.net.”으로 메일을 보내주시기 바랍니다. 물론 필자에게 직접 메일(wez@php.net)을 보내주셔도 상관 없겠지만, 필자의 메일함에 쌓이는 PHP 관련 이메일이 워낙 많은 탓에 신속한 응답을 드리기가 어려운 점을 양해해 주시기 바랍니다..


출처 : http://blog.naver.com/s9712094?Redirect=Log&logNo=140015410359


+ Recent posts