메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

PHP Data Access 2. 라이브러리 작성

한빛미디어

|

2004-07-19

|

by HANBIT

12,452

저자: 한동훈(traxacun @ unitel.co.kr)

지난번 PHP Data Access 소개 기사에 이어 이번 시간에는 PHP에서 데이터 액세스를 담당할 함수를 작성해보자. 먼저 기본적인 데이터 액세스 라이브러리를 작성하고, 라이브러리 작성에서 연결을 암시적 연결을 이용한 방법과 명시적 연결을 이용하는 방법에 대해서 설명하겠습니다. 그 다음으로 데이터 액세스에 대한 캐싱을 처리하는 방법에 대해서 살펴보도록 하죠.

설정 파일

첫번째는 데이터베이스 연결에 필요한 설정파일을 작성입니다. 보통 설정 파일은 다른 파일에서 포함(include)시켜서 사용하기 때문에 확장자로 .inc를 사용하지만 PHP의 설정에 따라서 이 파일은 일반 텍스트 파일로 인식될 수도 있기 때문에 포함 파일에 대해서는 확장자로 .inc.php를 사용했습니다. 이렇게 하면 PHP 설정파일에 신경쓰지 않아도 데이터베이스 비밀번호가 노출될 염려가 없습니다.
작성하는 변수 이름은 데이터 액세스(Data Access)에 관여하기 때문에 접두어로 da를 사용하겠습니다. 이렇게 하면 다른 라이브러리와 있을 수 있는 이름충돌을 예방할 수 있거든요.

파일: settings.inc.php

/*******************************************************
   Author: traxacun at unitel.co.kr
   Created: 2004. 7. 16
   Last modified:

   Function: configuration for data access
*******************************************************/
$da_config["dbhost"] = "localhost";

// database user name
$da_config["dbuser"] = "username";

// database user password
$da_config["dbpassword"] = "password";

// database name
$da_config["dbname"] = "database";
?>

자, 다음으로 해야하는 건 데이터 액세스를 담당할 함수를 작성하는 겁니다. 데이터 액세스를 담당하는 함수를 작성한 다음에 기존의 PHP 함수를 썼을 때와 비교해보겠습니다.

라이브러리 작성

라이브러리는 전체 파일로 보여주는 것 대신에 순서대로 부분 부분 잘라서 설명하겠습니다. 일단, 모든 PHP 함수를 라이브러리로 작성했습니다. 2-3줄 밖에 안되는 함수도 작성하게 됩니다. 그러면 많은 분들은 ‘이게 모야? 이렇게 하면 더 느려지는 거 아냐?’라고 하실겁니다. 하지만 그렇게 느려지지 않습니다. 그러면서 새로운 API에 대한 유연성을 확보하게 되는 장점이 있습니다. 그 장점을 느껴보기 위해 마지막에는 향상된 MySQL API로 소개된 mysqli_로 시작하는 함수군들을 살펴보고, 라이브러리가 어떤 장점을 주는지 설명하겠습니다.

먼저 데이터베이스 연결을 생성하는 부분입니다.

$dc = da_openConnection();

// open db connection
function da_openConnection()
{
   global $da_config;
   $dc = @mysql_pconnect ($da_config["dbhost"], $da_config["dbuser"], $da_config["dbpassword"]);

   if (@mysql_select_db ($da_config["dbname"], $dc))
      return $dc;
}

먼저 데이터베이스 연결을 여는 da_openConnection() 함수를 작성합니다. 그리고 설정 파일도 포함(include)하여 사용할 것이기 때문에 $da_config의 값들을 함수안에서 이용할 수 있게 global로 선언했습니다. 나머지는 익히 알고 있는 것처럼 mysql_pconnect를 사용하여 데이터베이스 연결을 생성하고 mysql_select_db를 사용하여 데이터베이스를 선택합니다.
첫번째 문장은 $dc라는 이름으로 데이터 연결(data connection)을 갖습니다.
이렇게 연결을 만드는 부분을 가지게 한 이유는 나중에 자세히 얘기하겠지만, 일단 이 라이브러리를 포함시키는 것은 데이터 베이스에 액세스하기 위한 것이다. 액세스 하기 위해 연결을 열어야 한다. 모든 페이지마다 연결을 여는 코드 작성이 귀찮다. 따라서 포함파일 내에서 연결을 열어버리게 하자. 라는 생각입니다. 이런 종류의 연결을 ‘암시적 연결’이라고 합니다. 암시적 연결이 나쁜 것이라고 말하는 사람들도 있는데 꼭 그렇지는 않습니다. PHP의 기본 함수에서도 암시적 연결과 명시적 연결을 선택할 수 있습니다. 이 차이점은 뒤에 자세히 설명하겠습니다.
mysql_connect()와 mysql_pconnect()

mysql_connect()와 mysql_pconnect()의 차이점은 mysql_connect()는 연결을 생성하고 종료하는 방식이지만 mysql_pconnect()는 여러 개의 연결을 유지하고 있으면서 필요에 따라 연결을 제공하는 기능을 합니다. mysql_pconnect()는 데이터베이스 연결에 필요한 문자열들을 고유하게 인식합니다. 따라서 이 문자열 값이 다르면 전혀 다른 연결풀을 갖게 됩니다.

동시에 200명의 사용자가 사이트를 방문하고 있을 때 항상 데이터베이스에 액세스하는 것이 아닙니다. 실제로 데이터베이스 액세스는 짧은 시간이고 나머지는 페이지를 보여주기 위한 처리시간입니다. 또, 사용자는 화면의 글을 읽고 있는 시간도 있겠지요.
mysql_connect()를 사용하면 연결을 만들고 작업이 끝나면 연결을 닫습니다. 다른 페이지로 이동할 때도 이 과정이 반복됩니다. 반면에 mysql_pconnect()는 연결을 만들고 작업이 끝나면 연결을 반환합니다. 따라서 반복적인 연결 요청에 대해서 이미 생성된 것을 유지하다가 제공하기 때문에 그만큼 부하가 적고 보다 많은 사용자를 동시에 처리할 수 있습니다.
단점은 유지하는 연결수만큼 메모리 사용량이 증가한다는 것이지만 오늘날 이 정도 메모리 사용량은 신경쓰지 않아도 될만큼 작습니다.
연결 닫기

function da_closeConnection()
{
   global $dc;

   mysql_close( $dc );
}

연결을 닫는 함수도 매우 단순합니다. 특별히 할 것이 없습니다. 만약, 여러분이 연결을 직접 관리하지 않고 PHP에 관리를 위임하고 싶다면 da_closeConnection()은 다음과 같이 구현할 수 있습니다.

function da_closeConnection()
{
}

이와 같이 아무것도 없는 함수(dummy function)로 작성하면 됩니다.

데이터베이스 질의 함수

데이터베이스에 질의를 하는 함수를 작성하기 위해서는 먼저 질의의 종류를 구분해야 합니다. 무턱대고 함수를 작성하는 것은 곤란합니다.
이런 종류의 함수를 만들라고 하면 여러분은 꽤 어마어마한 함수를 작성하는 경향이 있습니다. 옙! 그렇게 작성한 함수 대부분이 쓸모없다는 공통점도 있습니다. 잘못작성되었기 때문입니다.

질의의 종류는 5가지가 있습니다.

1. 질의를 수행해서 한 필드의 값을 가져오는 경우
2. 질의에 해당하는 레코드가 몇 건이나 존재하는가 여부.
3. 질의를 수행해서 하나의 레코드를 반환하는 경우. ex. 각종 조회 화면들
4. 질의를 수행해서 여러 개의 레코드를 반환하는 경우.
5. 질의를 수행하고 성공 여부 만을 반환하는 경우. ex. 각종 UPDATE, INSERT, DELETE 질의

처음부터 이러한 분류를 할 수 있는 분들은 없습니다. 또한, 이보다 더 세분화하는 분들도 있을 것이고, 단순화하는 분들도 있으며 전혀 다른 방법으로 작성하는 분들도 있습니다. 어떤 경우이든 ‘이것이 모범이다’라고 할 수 있는 답은 없겠지요. ^^
이제, 각각의 경우에 대한 함수를 작성하면서 살펴보겠습니다.

1. 질의를 수행해서 한 필드의 값을 가져오는 경우

function da_queryValue($query)
{
   global $dc;
   $rows = @mysql_query($query, $dc);
   $row = @mysql_fetch_array($rows);

   return $row[0];
}

간단하게 사용자의 ID가 ‘trax’일 때 사용자 이름을 알아오려면 다음과 같은 코드를 작성하면 됩니다.

include "settings.inc.php";
include "database-mysql.inc.php";

$query = "SELECT userName FROM member WHERE id = ‘trax’";
echo da_queryValue( $query );

전체 회원수를 구하는 경우에는 다음과 같습니다.

$query = "SELECT COUNT(*) FROM member";
echo da_queryValue( $query );

2. 질의에 해당하는 레코드가 몇 건이나 존재하는가 여부.

보통 이런 종류의 질의는 회원 로그인 등에서 사용합니다. 그외에 사용자가 요청한 정보에 대한 레코드가 DB에 있는지 간단히 확인하기 위해 사용할 수 있습니다. 요청한 정보가 있다면 정보를 보여주기 위한 코드를 작성하면 되며, 요청한 정보가 없다면 적절한 에러 페이지를 보여주면 됩니다.
해당 레코드가 존재하는지 확인조차 하지 않으면서 사용자의 입력을 무조건 신뢰하여 수행되는 프로그램은 버그가 존재하는 것이며, 보안에 있어서도 위험한 상태라 할 수 있습니다. 따라서, 이 함수는 제법 수행되는 횟수가 많습니다. 초보 프로그래머는 이런 종류의 함수를 거의 사용하지 않지만요!!

function da_queryScalar($query)
{
   global $dc;
   $rows = @mysql_query($query, $dc);
   return @mysql_num_rows($rows);
}

함수 이름에 스칼라(Scalar)라고 하는 것이 있는데, 이것의 반대(?)되는 의미로 벡터(Vector)가 있습니다. 벡터란 크기와 방향이 있는 것을 나타내며 스칼라는 단순히 크기가 있는 것만을 나타냅니다. 즉, 해당 조건에 일치하는 레코드가 있다면 그에 해당하는 크기만을 나타내기 때문에 queryScalar라는 이름을 붙였습니다.
사용법은 다음과 같습니다.

$query = "SELECT * FROM contents WHERE keyword LIKE ‘%PHP%’";
echo mm_queryScalar( $query );
 
if( mm_queryScalar( $query ) ) // 레코드가 존재하는 경우
{
   // 레코드에 대한 처리
}
else
{
   // 해당 레코드가 존재하지 않습니다!!
}

이와 같이 작성하면 전보다 한 번 더 질의를 하지만 에러없는 프로그램을 작성할 수 있게 됩니다. 물론, 로그인시에 사용자 ID와 비밀번호에 해당하는 레코드가 있는지를 알아내기 위해서도 쓸 수 있습니다.

3. 질의를 수행해서 하나의 레코드를 반환하는 경우.

4. 질의를 수행해서 여러 개의 레코드를 반환하는 경우.

이제, 3번과 4번을 살펴볼 차례입니다. 4번은 주로 게시물 목록, 상품 목록과 같이 어떤 목록을 보여주는 경우이고 3번은 주로 게시물 조회, 상품 상세 정보와 같이 어떤 정보를 보는 페이지에 해당합니다.
즉, 3번이나 4번의 경우 세부적인 내용은 모두 동일하고 레코드 수가 1개거나 N개라는 차이점밖에 없기 때문에 하나의 함수로 작성했습니다.
데이터를 표의 형태로 보여준다는 것은 자료구조에서 가로축과 세로축이 있는 2차원 배열입니다. 따라서 이 함수는 2차원 배열을 반환합니다.

function da_getRecordsByFields($query)
{
   global $dc;
   $result = @mysql_query($query, $dc);

   if ( 0 == $result )
   {
      return false;
      exit;
   }

   for( $loopctr = 0; $loopctr < @mysql_num_rows($result); $loopctr++ )
   {
      $row_array = @mysql_fetch_row($result);

      for( $ctr = 0; $ctr < @mysql_num_fields($result); $ctr++ )
      {
         $field_name = @mysql_field_name($result, $ctr);
         $data_set[$loopctr][$field_name] = $row_array[$ctr];
      }
   }
   return $data_set;
}

이 함수의 사용법은 다음과 같습니다.(4번 항목)

   $rs = da_getRecordsByFields( "SELECT title, price FROM items" );

   for( $ctr = 0; $ctr < count( $rs ); $ctr++ )
   {
      echo $rs[ $ctr ][ title ];
      echo $rs[ $ctr ][ price ];
      echo "mmlt;brmmgt;";
   }

3번 항목의 경우에 사용법은 다음과 같습니다.

   $query = "SELECT title, price, company FROM items WHERE code = 12345";
   $rs = da_getRecordsByFields( $query );
   echo $rs[0][ title ];
   echo $rs[0][ price ];
   echo $rs[0][ company ];

앞에서 작성한 da_queryScalar()를 이용해서 에러처리까지 한다면 위 코드는 다음과 같이 다시 작성할 수 있습니다.

   $query = "SELECT title, price, company FROM items WHERE code = 12345";

   if( da_queryScalar( $query ) )
   {
      $rs = da_getRecordsByFields( $query );
      echo $rs[0][ title ];
      echo $rs[0][ price ];
      echo $rs[0][ company ];
   }

5. 질의를 수행하고 성공 여부 만을 반환하는 경우.

UPDATE, INSERT, DELETE 등의 질의는 적용된 행의 수를 반환합니다. 1개의 레코드를 삭제하면 1이 반환되고, 1개의 레코드를 등록하면 1이 반환됩니다. 마찬가지로 20개의 레코드를 삭제하면 20이 반환됩니다.

function da_queryNonResult($query)
{
   global $dc;
   @mysql_query($query, $dc);
   return @mysql_affected_rows($dc);
}

보통 이러한 질의는 결과를 반환하지 않기 때문에 함수이름은 queryNonResult라고 했습니다. mysql_affected_rows를 사용하면 쿼리를 수행한 결과가 적용된 행의 수를 반환합니다. 간단한 사용법은 다음과 같습니다.

$query = "DELETE FROM item WHERE code = 12345";
$affectedRows = da_queryNonResult( $query );
echo "$affectedRows 건이 삭제되었습니다.";

결과값을 이용해서 질의 결과가 성공했는지 실패했는지도 알 수 있습니다.

지금까지 기본적인 질의함수를 살펴봤습니다. 실제로 PHP의 MySQL API를 전부 포팅하기 때문에 꽤 방대한 작업이 됩니다. 하지만, 한 번 해두면 굉장히 편리해집니다. 물론, 여러분은 전체 소스(소스 다운로드)를 받아볼 수 있습니다.

나머지 mysql_free_result, mysql_affected_rows, mysql_fetch_row등의 함수에 대한 래퍼도 모두 작성해야하지만 이것은 함께 올리는 소스를 참고하시면 됩니다.

라이브러리 함수를 실행하는데 에러가 발생하면 안되기 때문에 @를 앞에 붙였습니다. 즉, 어떤 에러 메시지도 출력되지 않습니다. 그러면 어떻게 디버깅을 할까요??

디버깅 함수

디버깅 함수는 mysql_error()를 단순히 포장한 것에 지나지 않습니다. ^^ 그럼에도 불구하고 분명한 이점이 있으니 살펴보세요.

function da_error ()
{
   global $dc;
   return @mysql_error($dc);
}

즉, 위에 있는 함수들을 사용하고 오류가 있는지 확인하고 싶다면 다음과 같이 하면 됩니다.

$query = "SELECT price, FROM item WHERE code = 12345";
echo da_queryValue( $query );
echo da_error();

일부러 틀린 질의를 사용했기 때문에 화면에 에러 메시지가 출력되는 것을 볼 수 있습니다. 만약, 에러가 없다면 화면에 아무것도 출력되지 않습니다. ^^;

엑셀로 저장

질의 결과를 엑셀로 저장하려면 다음 함수를 사용하면 됩니다.

function da_getRecordsAsXls( $query )
{
   global $dc;

   $result = @mysql_query($query, $dc);
   if ( 0 == $result )
   {
      return false;
      exit;
   }

   for( $loopctr = 0; $loopctr < @mysql_num_rows($result); $loopctr++ )
   {
      $row_array = @mysql_fetch_row($result);

      for( $ctr = 0; $ctr < @mysql_num_fields($result); $ctr++ )
      {
         $field_name = @mysql_field_name($result, $ctr);
         $field_type = @mysql_field_type($result, $ctr);
         $str .= sprintf( "%s\t", $row_array[ $ctr ] );

      }

      $str .= "\n";
   }

   return $str;
}

이 함수의 사용법은 다음과 같습니다. 파일 전송이므로 화면에 출력되는 게 있어서는 안됩니다. ^^

   $data= da_getRecordsAsXls( "SELECT ib_title FROM i_books" );
         header("Content-Disposition: attachment; filename=excelfile.xls");
         header("Pragma: no-cache");
         header("Expires: 0");
         echo $header."\n".$data;

암시적 연결과 명시적 연결

암시적 연결과 명시적 연결이라는 말은 제가 마음대로 붙여버린겁니다. 다른 곳에서는 암시적 연결과 명시적 연결이라는 용어를 구분해서 사용하고 있습니다.
지금까지 작성한 라이브러리는 암시적 연결을 사용했습니다. 즉, 데이터베이스 연결과 닫기에 대해서 전혀 신경쓰지 않아도 되는 라이브러리였습니다. 이런 방식은 연결에 대한 코드 작성을 할 필요가 없기 때문에 편하지만 여러 개의 연결을 생성해야 하는 응용 프로그램에는 사용할 수 없습니다.(일반적으로 사용하는 경우가 없으므로 이렇게 작성했습니다.)
예를 들어, 한쪽에서는 MySQL에 연결하고, 다른쪽은 Oracle에 연결하는 경우 2개의 연결이 필요합니다.
이런 경우에는 명시적 연결 버전으로 라이브러리를 작성해야 합니다. 위 라이브러리에서 명시적 연결로 바꾸는 부분은 다음과 같습니다.

명시적 연결로 작성하기

먼저 $dc = da_openConnection() 부분을 삭제합니다. da_openConnection()은 변화가 없습니다. da_closeConnection()과 da_queryValue()만 명시적 연결로 바꾸겠습니다.

function da_closeConnection( $dc )
{
   mysql_close( $dc );
}

function da_queryValue($query, $dc )
{
   $rows = @mysql_query($query, $dc);
   $row = @mysql_fetch_array($rows);
   return $row[0];
}

위에서 알 수 있는 것처럼 각 함수에서 global $dc 부분을 삭제합니다. 함수의 인자에는 $dc라는 인자를 더 추가합니다. 라이브러리 사용법도 다음과 같이 약간 변경됩니다. ^^

$dc = da_openConnection();
$query = "SELECT name FROM member";
echo da_queryValue( $query, $dc );
da_closeConnection( $dc );

명시적 연결은 암시적 연결보다 좀 번거로워 보이지만 직접 작업을 처리하고자 하는 분들에게는 더 적합한 함수입니다.

PHP5의 향상된 MySQL API

PHP5에서는 MySQL 4.x를 지원하기 위해 새로운 API들이 제공됩니다. 그리고 이들 API는 모두 mysqli_connect(), mysqli_close(), mysqli_query()와 같이 mysqli로 시작합니다.
PHP5의 새로운 함수로 바꾸기로 했다면 기존 응용 프로그램에서 직접 mysql_로 작성한 함수들을 변경해야겠죠??
그러나 위에서 작성한 라이브러리만을 이용해서 응용 프로그램을 작성한 경우 라이브러리를 수정하는 것만으로 새로운 MySQL API를 사용할 수 있습니다.
라이브러리 대신 수작업으로 변경한다면 변경 중에 누락된 것이 발생할 수도 있습니다. 에러 발견, 수정, 에러 발견, 수정...이 일정기간 반복되겠지요.
즉, 이와 같이 라이브러리로 간단하게 함수들을 포장(wrapping)한 것으로 새로운 요구사항에 대한 변경을 최소화할 수 있으며, 코드 오염을 최소화할 수 있습니다.

라이브러리는 항상 한가지만 이용할 수 있을까요? 물론, 아닙니다. settings.inc.php에 다음과 같은 항목을 추가해봅시다.

$da_config[ ‘dbms’ ] = "php4mysql";

database.inc.php라는 새로운 파일을 작성합니다. 위에서 작성한 라이브러리는 PHP4와 PHP5 버전에 따라서 database-php4mysql.inc.php와 database-php5mysql.inc.php로 작성했다고 합시다.

파일: database.inc.php

   if( $da_config["dbms"] == "php5mysql" )
   {
      require( "database-php5mysql.inc.php");
   }
   else if( $da_config[‘dbms’] == ‘php4mysql’ )
   {
   require( "database-php4mysql.inc.php" );
   }
   else
   {
      require( mm_mod."/database-php4mysql.inc.php" );
   }

따라서 실제로 PHP 페이지에서는 다음과 같이 코드를 작성합니다.

require_once "/lib/settings.inc.php";
require_once "/lib/database.inc.php";

이와 같이 작성하면 필요에 따라 다양한 데이터베이스에 대해서 사용할 수 있습니다.

게시물 목록을 보여주는 프로그램을 작성할 때 전통적인 PHP4 API와 라이브러리를 작성한 코드를 비교하면 라이브러리를 이용하는 쪽이 코드 작성량이 훨씬 적다는 것을 알 수 있습니다. 이것은 프로그래머의 생산성 향상에도 기여한다는 것을 의미합니다.

라이브러리를 통한 이점은 다음과 같습니다.

1. API의 추상화
2. 코드 오염의 최소화
3. 다른 DBMS로의 변환 용이성, 즉 유연성
4. 재사용성
5. 코드 작성량의 감소, 생산성 향상

소스에 포함된 라이브러리는 PHP4 MySQL API에 대한 라이브러리만 제공합니다.

다음 시간에 캐싱에 대해서 다루겠습니다.
TAG :
댓글 입력
자료실