본문 바로가기

비개발자의 개발 일지

현장 팀장에서 수출 ERP까지 — AI와 함께 시스템을 만든 기록

시리즈 보기
php

PHP Prepared Statement 완전 정리 — bindValue, named placeholder, ? placeholder 완전 해설

by 왕진 2026. 7. 13.
반응형

 

 

Web Development · PHP + MySQL

PHP Prepared Statement 완전 정리

named placeholder(:name)와 ?로 SQL Injection을 막는 바인딩 방법 — prepare, bindValue, execute 완전 해설
시작 오늘 분석할 코드 — 두 가지 바인딩 방식

외부 입력값($_POST)을 SQL에 직접 넣으면 SQL Injection 공격에 취약하다. Prepared Statement는 SQL 템플릿을 먼저 만들고, 값을 나중에 "바인딩"하는 방식으로 이를 방어한다. 두 가지 placeholder 방식이 있다.

방법 1: 이름 있는 placeholder (:칼럼명)
// SQL 템플릿 — :last_name, :first_name, :age가 바인딩 변수 $sql = "INSERT INTO member (last_name, first_name, age) VALUES (:last_name, :first_name, :age)"; $stmh = $pdo->prepare($sql); // SQL 템플릿 준비 $stmh->bindValue(':last_name', $_POST['last_name']); // 값 바인딩 $stmh->bindValue(':first_name', $_POST['first_name']); $stmh->bindValue(':age', $_POST['age']); $stmh->execute(); // MySQL에 최종 실행
방법 2: ? placeholder (위치 기반)
$sql = "INSERT INTO member (last_name, first_name, age) VALUES (?, ?, ?)"; $stmh = $pdo->prepare($sql); $stmh->bindValue(1, $_POST['last_name']); // ?의 순서 번호(1부터) $stmh->bindValue(2, $_POST['first_name']); $stmh->bindValue(3, $_POST['age']); $stmh->execute(); // 이 코드는 해설용 — $pdo 객체 없이는 동작하지 않음
SQL Injection 방어 효과 시각적 비교
사용자가 last_name에 악성 입력 '; DROP TABLE member; --
문자열 직접 삽입 (위험)
INSERT INTO member ... VALUES (''; DROP TABLE member; --', ...)
→ 테이블 삭제됨!
Prepared Statement (안전)
:last_name에 문자열 그대로 바인딩
→ SQL로 해석 안 됨. 그냥 문자열로 저장
용어 핵심 용어 정리
prepared statementSQL 구조(템플릿)와 데이터(값)를 분리해 실행하는 방식. SQL Injection 완전 방어
placeholderSQL에서 실제 값이 들어갈 자리를 표시하는 기호. :name 또는 ?
바인딩 변수:칼럼명 형태의 이름 있는 placeholder. 외부 값이 "달라붙는(binding)" 자리
prepare()SQL 템플릿을 MySQL에 미리 전달해 분석·컴파일. PDOStatement 객체 반환
bindValue()placeholder에 실제 값을 바인딩. 이름(:name) 또는 위치(1,2,3)로 지정
execute()바인딩된 값을 포함해 prepared statement를 MySQL에 최종 실행
$stmhstatement handle의 약자. prepare()가 반환하는 PDOStatement 객체를 담는 변수
SQL Injection입력값에 SQL을 삽입해 DB를 조작하는 공격. Prepared Statement로 완전 방어 가능

1 왜 Prepared Statement인가 — SQL Injection 방어
값과 SQL 구조를 분리하면 주입 공격이 불가능해진다

사용자가 입력한 값을 SQL 문자열에 직접 연결하면 악의적인 SQL이 실행될 수 있다. Prepared Statement는 SQL의 구조(템플릿)를 먼저 MySQL에 전달하고, 데이터 값을 나중에 별도로 전달한다. MySQL은 이미 SQL 구조를 파싱한 상태이므로 나중에 전달된 값을 절대 SQL로 해석하지 않는다.

SQL 템플릿 (prepare)
MySQL이 파싱·컴파일
값 바인딩 (bindValue)
execute() 실행
※ 핵심: MySQL이 SQL 구조를 먼저 받고 나서, 값은 "데이터"로만 취급한다. 값 안에 SQL이 들어 있어도 실행되지 않는다.

2 prepare() — SQL 템플릿 준비
SQL을 미리 MySQL에 보내 컴파일한다

$pdo->prepare($sql)는 placeholder(:name 또는 ?)가 포함된 SQL 문자열을 MySQL에 전달해 파싱·컴파일하고, 실행 준비가 된 PDOStatement 객체를 반환한다. 이 객체를 $stmh에 저장해 이후 bindValue와 execute에 사용한다.

$sql = "INSERT INTO member (last_name, first_name, age) VALUES (:last_name, :first_name, :age)"; $stmh = $pdo->prepare($sql); // $stmh는 PDOStatement 객체 // MySQL이 SQL 구조를 이미 파싱·컴파일한 상태

3 bindValue() — 값을 placeholder에 바인딩
named placeholder와 ? placeholder의 차이

bindValue()는 첫 번째 인수로 placeholder 식별자를, 두 번째로 실제 값을 받는다. Named placeholder는 ':last_name'처럼 문자열로 지정하고, ? placeholder는 위치 번호(1부터 시작)로 지정한다.

Named placeholder (:name)
bindValue(':last_name', $v)

이름으로 지정 → 순서 무관
가독성 높음
컬럼이 많을 때 실수 줄어듦
? placeholder (위치 기반)
bindValue(1, $v) (1부터)

순서 기반 → 번호로 지정
타이핑 간결
순서 바뀌면 오류 발생 주의
// Named placeholder $stmh->bindValue(':last_name', $_POST['last_name']); $stmh->bindValue(':first_name', $_POST['first_name']); $stmh->bindValue(':age', $_POST['age']); // ? placeholder — 숫자는 1부터 시작 $stmh->bindValue(1, $_POST['last_name']); $stmh->bindValue(2, $_POST['first_name']); $stmh->bindValue(3, $_POST['age']);

 

반응형

 

4 bindValue vs bindParam — 두 바인딩 메서드의 차이
값을 고정으로 바인딩 vs 변수를 참조로 바인딩

bindValue()는 호출 시점의 값을 바로 바인딩하고, bindParam()은 변수를 참조로 연결해 execute() 실행 시점의 값을 사용한다. 루프에서 같은 statement를 반복 실행할 때는 bindParam이 편리하다.

bindValue() — 값 즉시 고정
bindValue(':name', $v)

호출 시점에 $v의 값을 복사
이후 $v가 바뀌어도 영향 없음
단순 1회 실행에 적합
bindParam() — 변수 참조
bindParam(':name', $v)

$v를 참조로 연결
execute() 시점의 $v 값 사용
루프 반복 실행에 편리
// bindParam 활용 — 루프에서 반복 실행 $stmh = $pdo->prepare("INSERT INTO member (last_name, first_name, age) VALUES (:ln, :fn, :age)"); $stmh->bindParam(':ln', $last); $stmh->bindParam(':fn', $first); $stmh->bindParam(':age', $age); foreach($data as $row) { $last = $row['last']; $first = $row['first']; $age = $row['age']; $stmh->execute(); // 이 시점의 $last/$first/$age 값으로 실행 }
5 execute() 배열 단축 — bindValue 생략 가능
bindValue 없이 execute()에 직접 배열로 값 전달

bindValue()를 일일이 호출하는 대신, execute()에 배열을 직접 넘기면 더 간결하게 쓸 수 있다. 이 방식은 내부적으로 bindValue와 동일하게 동작한다.

// named placeholder 방식 — execute에 연관배열 전달 $stmh = $pdo->prepare("INSERT INTO member (last_name, first_name, age) VALUES (:last_name, :first_name, :age)"); $stmh->execute([ ':last_name' => $_POST['last_name'], ':first_name' => $_POST['first_name'], ':age' => $_POST['age'], ]); // ? placeholder 방식 — execute에 인덱스 배열 전달 $stmh = $pdo->prepare("INSERT INTO member (last_name, first_name, age) VALUES (?, ?, ?)"); $stmh->execute([$_POST['last_name'], $_POST['first_name'], $_POST['age']]);
execute에 배열을 직접 넘기는 방식은 코드가 짧아지지만, 타입을 명시(PDO::PARAM_INT 등)할 수 없다. 정수형 age를 문자열로 저장하지 않으려면 bindValue(3, (int)$_POST['age'], PDO::PARAM_INT) 처럼 명시하는 것이 더 안전하다.

6 SELECT에서 Prepared Statement 사용하기
INSERT뿐 아니라 SELECT 조건에도 바인딩이 필요하다

외부 입력값을 SELECT WHERE 조건에 넣을 때도 반드시 prepared statement를 사용해야 한다. 예를 들어 사용자가 입력한 이름으로 DB를 검색할 때 직접 문자열 연결하면 SQL Injection에 취약하다.

// SELECT에서 외부 값 바인딩 $sql = "SELECT * FROM member WHERE last_name = :last_name"; $stmh = $pdo->prepare($sql); $stmh->bindValue(':last_name', $_POST['last_name']); $stmh->execute(); // 결과 행을 하나씩 꺼내기 while($row = $stmh->fetch(PDO::FETCH_ASSOC)) { echo $row['id'] . ' ' . $row['first_name'] . "<br>"; } // 전체를 한꺼번에 배열로 꺼내기 $rows = $stmh->fetchAll(PDO::FETCH_ASSOC); foreach($rows as $row) { ... }
⚠ SELECT WHERE last_name = '{$_POST['last_name']}' 처럼 직접 삽입하면 SQL Injection. 반드시 prepare + bindValue 사용.

7 execute() — 최종 실행
바인딩된 값과 함께 MySQL에 실행 요청

$stmh->execute()는 prepare로 컴파일된 SQL 템플릿에 bindValue로 바인딩된 값들을 결합해 MySQL에 실제로 실행시킨다. INSERT라면 이 시점에 데이터가 DB에 저장된다.

1
prepare($sql)SQL 템플릿을 MySQL에 전달 → PDOStatement 반환
2
bindValue() 3번 호출각 placeholder에 $_POST 값을 연결
3
execute() 호출템플릿 + 바인딩 값 → MySQL이 실제 INSERT 실행
4
DB에 저장됨member 테이블에 새 레코드 1개 추가 완료

5 자주 하는 실수
실수 증상 해결
? placeholder를 0부터 번호 지정 Invalid parameter number 오류 bindValue(1, ...) 로 1부터 시작
named placeholder에서 : 생략 bindValue('last_name', ...) — 바인딩 안 됨 bindValue(':last_name', ...) 콜론 필수
bindValue 순서와 SQL 순서 불일치 엉뚱한 컬럼에 값 저장 ? 방식은 SQL의 ? 순서와 bindValue 번호가 정확히 매핑되어야 함
execute() 누락 prepare/bindValue만 하고 실제 DB 저장 안 됨 bindValue 완료 후 반드시 execute() 호출
$_POST 미검증 후 직접 바인딩 빈 값·이상한 타입 저장 bindValue 전 empty() 체크 또는 타입 캐스팅 (예: (int)$_POST['age'])

핵심 한 줄 요약

prepare($sql)SQL 템플릿을 MySQL에 미리 전달·컴파일 — PDOStatement 반환
bindValue(':name', $value)placeholder에 실제 값 연결 — 값은 절대 SQL로 해석 안 됨
execute()템플릿 + 바인딩 값으로 MySQL 최종 실행 — 이 시점에 DB 변경
:name vs ?named=이름으로 지정(순서 무관), ?=위치 번호로 지정(1부터)
SQL Injection 방어값과 SQL 구조 분리 → 악성 입력도 데이터로만 처리
bindValue vs bindParambindValue=호출 시 값 복사(1회용), bindParam=참조 연결(루프 재사용)
execute(배열)bindValue 생략 단축 패턴 — 타입 지정 불가. 정수 컬럼은 명시 바인딩 권장

Tags

#PHP prepared statement #bindValue #PDO prepare #SQL Injection 방어 #placeholder #named placeholder #바인딩 변수 #execute #PHP PDO 바인딩 #PHP DB 보안 #웹개발 #티스토리
▼ 티스토리 태그 입력란 복사용
PHP prepared statement, bindValue, PDO prepare, SQL Injection 방어, placeholder, named placeholder, 바인딩 변수, execute, PHP PDO 바인딩, PHP DB 보안
반응형

댓글