본문 바로가기

프로그래밍 공부/PHP

PHP - PHP에서 데이터베이스 조작 中

SQL문

웹 페이지에서 데이터베이스를 조작하는 방법은 일반적으로 웹 페이지에서 PHP가 동작할 때 폼 컨트롤 버튼을 눌러 PHP 파일로 데이터를 송신하는 방법(POST)과 링크에 파라미터를 붙여 PHP 파일로 데이터를 송신하는 방법(GET)이 있다.

송신된 데이터는 PHP의 슈퍼 글로벌 변수 $_POST, $_GET으로 받을 수 있다. 데이터베이스 조작에 필요한 데이터를 받거나, 데이터를 가지고 SQL문의 결과를 받는다.

폼에서 송신 또는 링크에서 송신
데이터의 수신
SQL문의 구성
SQL문의 발행
결과 수신
결과표시

데이터베이스를 조작하는 SQL문을 작성하려면 필요한 데이터를 넘겨 받아야 한다.

조작 필요한 데이터
입력 데이터와 칼럼 위치
검색 검색 조건에 사용할 데이터
수정 수정 후의 데이터와 수정 조건
삭제 삭제 조건에 사용할 데이터

데이터베이스 조작에는 입력, 검색, 수정, 삭제 이외에 집계(카운트, 합계, 평균, 최대, 최소)를 위한 조작이 존재한다. SQL에는 이를 처리할 수 있도록 group by와 같은 문으로 구성된 집계 함수를 제공하기도 한다.

히어 도큐먼트(Here Document) 구문

1
2
3
4
$sql = <<<EOS 
  INSERT INTO member(last_name, first_name, age) 
   VALUES('고''길동'21
EOS;
cs

SQL문은 변수에 할당한다. SQL문이 짧을 때는 간단하게 변수에 할당할 수 있지만, 처리에 따라서는 SQL문이 꽤 길 때도 있다. 이 경우에 히어 도큐먼트 구문을 사용해 긴 SQL문을 틀리지않고 보기 쉽게 기술할 수 있다. 히어 도큐먼트는 시작점에 <<<EOS를 기술하고 개행하여 SQL문을 기술하고 마지막으로 EOS;로 마친다.

EOS에는 임의의 문자열을 붙일 수 있지만 영문자나 _만으로 작성된 문자는 오류가 발생한다. <<<와 EOS 사이와 EOS; 앞뒤에 탭이나 공백과 같은 문자열이 있어도 오류가 발생한다.

마지막으로 PHP에서 발행하는 SQL문은 반드시 ;으로 마칠 필요는 없다.

변수가 있는 SQL의 구성

1
2
3
4
5
6
7
8
9
// 적절한 기술 방법, 값이 직접 기술되어 있는 SQL문
$sql = "INSERT INTO
member(last_name, first_name, age)
VALUES('고', '길동', 21)";
 
// 위험한 기술 방법, 값이 글로벌 변수에 기술되어 이ㅆ는 SQL문
$sql = "INSERT INTO
member(last_name, first_name, age)
VALUES('".$_POST['last_name']."', '".$_POST['first_name']."', ".$_POST['age'].")";
cs

위의 코드는 데이터를 등록할 때 사용하는 INSERT문이다. 1번의 '값이 직접 기술되어 있는 SQL문'은 터미널이나 명령 프롬프트에서 입력할 때처럼 기술하면 된다. 또한 회원 등록 폼에서 사용자가 데이터를 송신하려면 외부에서 전달받은 데이터를 SQL에 포함해야 한다.

프리페어드 스테이트먼트

1
2
3
4
5
6
7
8
9
// [:이름]을 사용하는 경우
$sql = "INSERT INTO
member(last_name, first_name, age)
VALUES(:last_name, :first_name, :age)";
 
// [?]를 사용하는 경우
$sql = "INSERT INTO
member(last_name, first_name, age)
VALUES(?, ?, ?)";
cs

플레이스 홀더(placeholder)를 이용한 구성, SQL을 안전하게 실행하려면 프리페어드 스테이트먼트라는 기능을 사용해야한다. 프리페어드 스테이트먼트는 SQL로 구성되는 값만 설정할 수 있어 안전하게 처리할 수 있다.

같은 SQL문을 반복할 때는 성능 최적화도 기대할 수 있다. 프리페어드 스테이트먼트는 SQL문의 템플릿을 먼저 준비하고 값과 SQL 부분을 명확하게 분리하는 것으로 안전을 보장한다. SQL문과 외부의 값을 구별하기 위해 값 부분에 플레이스 홀드라는 식별자를 :이름 또는 ?라는 두 종류의 형식을 선택하여 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
    // ':이름' placeholder
    $sql = "INSERT INTO member(last_name, first_name, age) VALUES(:last_name, :first_name, :age)";
    $stmh = $pdo->prepare($sql);
    
    $stmh->bindValue(':last_name'$_POST['last_name']);
    $stmh->bindValue(':first_name'$_POST['first_name']);
    $stmh->bindValue(':age'$_POST['age']);
    $stmh->execute();
 
    // '?' placeholder
    $sql = "INSERT INTO member(last_name, first_name, age) VALUES(?, ?, ?)";
    $stmh = $pdo->prepare($sql);
    
    $stmh->bindValue(1$_POST['last_name']);    
    $stmh->bindValue(2$_POST['first_name']);
    $stmh->bindValue(3$_POST['age']);
    $stmh->execute();
?>
cs

해설을 위한 코드이기 때문에 작동은 하지 않는다. perpare 메서드로 작성한 SQL에 인수를 설정하여 실행하면 SQL을 해석해 캐싱한다. 같은 SQL을 두 번째로 실행할 때부터 이 캐시를 사용하기 때문에 속도가 개선이 된다.

SQL의 각 플레이스 홀더와 값은 바인드를 해야 하는데 이때 이용할 수 있는 메서드로 bindValue와 bindParam이 있다. $pdo에는 PDO 클래스의 객체가 할당되어 있고 perpare 메서드로 $sql에 할당된 SQL문을 해석한다.

스테이트먼트 핸들러를 반환 값으로 돌려받아 $stmh에 할당된다. PDOStatement 클래스의 bindValue 메서드를 이용해 첫 번째 인수에는 플레이스 홀더에 사용한 이름을 두 번째 인수에는 외부에서 전달받은 값을 지정한다. bindValue 메서드 안의 식별자는 '로 묶여있다.

마지막으로 execute();로 SQL문을 실행하면 된다. ? 플레이스 홀더의 경우는 대응하는 순서대로 숫자를 적으면 된다.

데이터 입력

송신 폼은 대상이 되는 테이블 구조에 맞춰야 하기 때문에 먼저 테이블의 구조를 확인해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- form.html -->
<!DOCTYPE html>
<html>
    <head>
        <title>PHP 테스트</title>
        <meta http-equiv="Content-type" content="text/html; charset=UTF-8">
    </head>
    <body bgcolor="#FFFFFF" text="#000000">
        <font size="4">PHP 테스트</font>
        <form name="form1" method="POST" action="view.php">
            성:<br>
            <input type="text" name="last_name"><br>
            이름:<br>
            <input type="text" name="first_name"><br>
            연령:<br>
            <input type="text" name="age"><br>
            <input type="submit" value="송신">
        </form>
    </body>
</html>
cs

테이블을 확인을 하고 위와 같은 입력받을 form.html 파일을 생성한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!-- view.php -->
<html>
<head>
    <title>PHP 테스트</title>
</head>
<body>
    <?php
        $db_user = "root";          // 사용자명
        $db_pass = "password";      // 패스워드
        $db_host = "localhost";     // 호스트명
        $db_name = "phpdb";         // 데이터베이스명
        $db_type = "mysql";         // 데이터베이스 종류
        $dsn = "$db_type:host=$db_host; dbname=$db_name charset=UTF8";
 
        try{
            $pdo = new PDO($dsn$db_user$db_pass);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
            
            print "접속하였습니다.";
        }catch(PDOExecption $Exception){
            die('오류:'.$Exception->getMessage());
        }
 
        try{
            $pdo->beginTransaction();
            $sql = "INSERT INTO member(last_name, first_name, age) VALUES(:last_name, :first_name, :age)";
            
            $stmh = $pdo->prepare($sql);
            $stmh->bindValue(':last_name'$_POST['last_name'], PDO::PARAM_STR);
            $stmh->bindValue(':first_name'$_POST['first_name'], PDO::PARAM_STR);
            $stmh->bindValue(':age'$_POST['age'], PDO::PARAM_INT);
            $stmh->execute();
 
            $pdo->commit();
 
            print "데이터를 ".$stmh->rowCount()."건 입력하였습니다.<br>";
        }catch(PDOExecption $Exception){
            $pdo->rollBack();
 
            print "오류:".$Exception->getMesssage();
        }
    ?>
</body>
</html>
cs

접속은 PDO를 사용한다. 이전 접속방법과 비슷하지만 DSN(데이터 소스명: 접속 정보 문자열)을 변수에 넣어 구성한다. 변수는 "로 묶여 있어 실행시 문자열이 된다. try-catch를 이용할 수 있도록 오류 모드를 설정한다. 또한 프리페어드 스테이트먼트를 사용할 수 있도록 에뮬레이터 기능을 FALSE로 한다.

MySQL 서버에 접속 후, try 블록 안에서 데이터를 입력하는 처리를 실행한다. 트랙잭션 처리와 bindValue에 세 번째 인수를 설정하고 있는 부분이다. 트랜잭션은 여러 처리를 하나의 처리로 묶은 것을 의미한다. 위의 코드에서 두 번째 try-catch문 부분이 트랜잭션이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
트랜잭션 시작
입력 처리1
입력 처리2
수정 처리
삭제 처리
트랜잭션 종료
 
<?php
try{
   $pdo->beginTransaction();
    $sql = "INSERT INTO member(last_name, first_name, age) VALUES(:last_name, :first_name, :age)";
          
    $stmh = $pdo->prepare($sql);
    $stmh->bindValue(':last_name'$_POST['last_name'], PDO::PARAM_STR);
    $stmh->bindValue(':first_name'$_POST['first_name'], PDO::PARAM_STR);
    $stmh->bindValue(':age'$_POST['age'], PDO::PARAM_INT);
    $stmh->execute();
    
    $pdo->commit();
    
    print "데이터를 ".$stmh->rowCount()."건 입력하였습니다.<br>";
}catch(PDOExecption $Exception){
    $pdo->rollBack();
 
    print "오류:".$Exception->getMesssage();
}
?>
cs

위의 코드가 앞서 설명한 트랜잭션 부분이다. beginTransaction(); 메서드를 사용해 트랜잭션을 시작하고 SQL을 실행한 후에 commit(); 메서드로 처리를 확정시킨다. 오류가 발생했다면 catch 블록 안에 rollback(); 메서드에 따라 원상태로 돌리고 종료를 시킨다.

bindValue 메서드의 세 번째 인수 PDO::PARAM_STR은 데이터형이라는 것이다. 바인드 하는 값이 문자열인지 수치인지를 명시하여 SQL 인젝션 공격에 대한 안정성을 높히고 있다. 

정의된 상수
PDO::PARAM_BOOL 불린(boolean) 값을 나타낸다.
PDO::PARAM_NULL SQL의 NULL 값을 나타낸다.
PDO::PARAM_INT SQL의 INTEGER(정수)형을 나타낸다.
PDO::PARAM_STR SQL의 CHAR형, VARCHAR형 등의 문자열을 나타낸다.

PDOStatement 클래스의 rowCount 메서드는 SQL을 실행한 결과에 대한 건수를 반환한다. 여기에서는 등록 건수를 반환한다.