Skip to content

PHP 치트시트

웹 개발을 위한 인기 있는 범용 스크립팅 언어.

01

기본

변수, 타입 & 상수

PHP 변수는 $로 시작하고 동적으로 타입이 지정됩니다. PHP 7.4+는 타입이 지정된 속성을 지원합니다. 런타임 상수에는 define()을, 컴파일 타임 상수에는 const를 사용하세요(더 빠름). PHP 8.1은 enum을 도입했습니다 — 클래스 상수보다 우수한 타입화된 열거 시스템입니다. 가능한 곳에 항상 타입을 선언(PHP 7+)하여 조기 에러 감지와 더 나은 IDE 지원을 받으세요.

php
<?php
$name = "Alice";          // string
$age = 30;                // integer
$price = 19.99;           // float (double)
$active = true;           // boolean
$items = [1, 2, 3];       // array
$null = null;             // null

// constants
define("MAX_USERS", 100); // runtime constant
const PI = 3.14159;       // compile-time constant
echo MAX_USERS;            // no $ prefix

// PHP 8.1+ enums
enum Status: string {
    case Active = 'active';
    case Inactive = 'inactive';
}
$s = Status::Active;

Echo, Print & 디버깅

echo는 언어 구조(함수 아님)입니다 — 출력에 가장 빠릅니다. print는 1을 반환하므로 표현식에 사용할 수 있습니다. printf/sprintf는 C 스타일 형식 지정자(%s 문자열, %d 정수, %f 실수, %x 16진수)를 사용합니다. var_dump()는 주요 디버깅 도구입니다 — 타입과 값을 표시합니다. error_log()는 PHP 에러 로그나 syslog에 기록합니다. 프로덕션에서는 디버그 출력을 사용자에게 노출하지 마세요.

php
<?php
// echo: no return, multiple args (fastest)
echo "Hello", " ", "World";

// print: returns 1, single arg
print "Hello World";

// printf: formatted output
printf("Name: %s, Age: %d, Price: %.2f", "Alice", 30, 19.99);

// debugging output
print_r($array);           // human-readable
var_dump($variable);       // type + value (detailed)
var_export($array, true);  // valid PHP code (for caching)

// sprintf: return formatted string (don't print)
$log = sprintf("[%s] %s", date('H:i:s'), "Started");
error_log($log);           // log to error log

연산자 & 비교

항상 ===(엄격한 비교)를 사용하여 타입 강제 변환 버그를 피하세요. ==는 비교 전에 타입을 변환하여 예상치 못한 결과를 초래합니다(0 == 'abc'는 PHP 7에서 true였습니다). 우주선 연산자(<=>)는 -1/0/1을 반환합니다 — usort에 유용합니다. 널 병합 연산자(??)는 기본값을 제공하는 관용적인 방법입니다. 널 안전 연산자(?->)(PHP 8+)는 null에서 메서드 체인을 단락시켜 장황한 isset() 검사를 대체합니다.

php
<?php
// arithmetic
$sum = 10 + 3;       // 13
$mod = 10 % 3;       // 1
$pow = 2 ** 3;       // 8 (PHP 5.6+)

// comparison: == vs ===
echo (0 == "abc");   // true (loose, PHP 7-); false (PHP 8+)
echo (0 === "abc");  // false (strict — type + value)
echo ("1" == 1);     // true (loose)
echo ("1" === 1);    // false (strict)

// spaceship operator (PHP 7+)
echo 1 <=> 2;        // -1 (less than)
echo 2 <=> 2;        // 0 (equal)
echo 3 <=> 2;        // 1 (greater than)

// null coalescing
$name = $input ?? "default";    // if $input is null
$deep = $data['user']['name'] ?? "Anonymous";

// null safe operator (PHP 8+)
$country = $user?->getAddress()?->country; // null if any step is null

슈퍼글로벌 & 웹

슈퍼글로벌은 모든 범위에서 사용 가능한 내장 연관 배열입니다. $_GET과 $_POST는 사용자 입력을 포함합니다 — 사용 전에 항상 살균/검증하세요. filter_input()이 직접 접근보다 안전합니다. 클라이언트가 위조할 수 있는 $_SERVER 값(예: HTTP_USER_AGENT)은 절대 신뢰하지 마세요. header('Location:') 후 항상 exit를 호출하세요 — 그렇지 않으면 PHP가 계속 실행됩니다. 세션은 출력 전에 session_start()로 시작하세요.

php
<?php
// superglobals available everywhere
$_GET['name'];           // query string params
$_POST['email'];         // form POST data
$_REQUEST['x'];          // GET + POST + COOKIE
$_SERVER['HTTP_HOST'];   // server/env info
$_SERVER['REQUEST_METHOD']; // GET, POST, etc.
$_COOKIE['session'];     // cookies
$_FILES['upload'];       // file uploads
$_SESSION['user_id'];    // session data (after session_start())

// get client IP
$ip = $_SERVER['REMOTE_ADDR'];

// check request method
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
}

// redirect
header("Location: /dashboard");
exit; // always exit after redirect

Include & Require

include/require는 지정된 파일을 실행합니다. require는 파일이 없으면 치명적 에러를 발생시킵니다(중요 의존성에 사용); include는 경고만 합니다(선택적 템플릿에 사용). _once 변형은 이중 포함을 방지하기 위해 포함된 파일을 추적합니다 — 함수/클래스 정의에 필수적입니다. Composer의 오토로더(require_once 'vendor/autoload.php')는 수동 include 관리를 제거합니다. 파일은 값을 반환할 수 있어 설정에 유용합니다.

php
<?php
// include: warning on failure, continues
include 'header.php';
include_once 'config.php'; // only once

// require: fatal error on failure, stops
require 'database.php';
require_once 'vendor/autoload.php'; // Composer autoload

// _once variants prevent re-declaration errors
// use require for critical files (DB config, autoloaders)
// use include for optional files (templates)

// return values from included files
$config = include 'config.php';
// config.php: return ['db' => 'mysql://...', 'debug' => true];
02

문자열

문자열 함수

PHP는 100개 이상의 문자열 함수가 있습니다. strpos()는 찾지 못하면 false를 반환합니다 — 확인에는 === false를 사용하세요(0은 유효한 위치입니다). str_replace()는 검색/대체에 배열을 사용할 수 있습니다. substr()은 음수 오프셋(끝에서부터)을 지원합니다. 멀티바이트 문자열(UTF-8)의 경우 mb_* 동등 함수(mb_strlen, mb_substr)를 사용하세요 — strlen은 문자가 아닌 바이트를 셉니다. 항상 default_charset='UTF-8'로 설정하고 비 ASCII 텍스트에는 mb_* 함수를 사용하세요.

php
<?php
$s = "Hello, World";

// length and case
echo strlen($s);              // 12
echo str_word_count($s);      // 2
echo strtoupper($s);          // HELLO, WORLD
echo strtolower($s);          // hello, world
echo ucfirst("hello");        // Hello
echo ucwords("hello world");  // Hello World

// search and replace
echo strpos($s, "World");     // 7 (false if not found)
echo str_replace("o", "0", $s); // Hell0, W0rld
echo substr($s, 0, 5);        // Hello
echo substr($s, -5);          // World
echo strrev($s);              // dlroW ,olleH

// trim
echo trim("  hi  ");          // "hi"
echo ltrim("  hi");           // "hi"
echo rtrim("hi  ");           // "hi"

문자열 보간 & Heredoc

큰따옴표 문자열은 변수를 보간합니다; 작은따옴표 문자열은 보간하지 않습니다(리터럴 텍스트에는 작은따옴표 사용 — 약간 더 빠름). 복잡한 표현식(객체 속성, 배열 접근, 메서드 호출)에는 {$var}를 사용하세요. Heredoc(<<<ID)은 SQL이나 HTML 같은 다중 줄 문자열에 이상적입니다 — 변수를 보간합니다. Nowdoc(<<<'ID')은 해석하지 않는 버전으로, 백슬래시가 있는 정규식 패턴에 유용합니다.

php
<?php
$name = "Alice";
$age = 30;

// double quotes: variable interpolation
echo "Hello, $name!";          // Hello, Alice!
echo "Hello, {$name}!";        // Hello, Alice! (braces for clarity)
echo "Age: {$age}";            // Age: 30

// single quotes: no interpolation (faster)
echo 'Hello, $name';           // Hello, $name (literal)

// complex expressions need braces
echo "Result: {$obj->method()}";
echo "Item: {$array['key']}";

// heredoc: multi-line, interpolates
$sql = <<<SQL
    SELECT * FROM users
    WHERE name = '$name'
    AND age > $age
SQL;

// nowdoc: multi-line, NO interpolation (like single quotes)
$regex = <<<'REGEX'
    \d{3}-\d{4}
REGEX;

sprintf & 형식화

sprintf()는 안전하게 형식화된 문자열을 구축하는 데 필수적입니다 — 문자열 보간과 달리 타입 변환과 패딩을 처리합니다. 정수에는 %s가 아닌 %d를 사용하여 숫자 형식을 보장하세요. number_format()은 천 단위 구분자로 숫자를 형식화합니다 — 통화 표시에 중요합니다. 준비된 문장의 SQL 조각에는 항상 sprintf를 사용하세요(사용자 입력에는 여전히 준비된 문장이 필요합니다).

php
<?php
// sprintf: format string (returns, doesn't print)
$formatted = sprintf(
    "%-10s | %5d | %8.2f",
    "Alice", 42, 19.99
);
echo $formatted;  // "Alice      |    42 |    19.99"

// format specifiers:
// %s string, %d int, %f float, %x hex, %b binary, %c char
// %5d  right-padded to width 5
// %-5d left-padded
// %05d zero-padded
// %.2f 2 decimal places
// %8.2f width 8, 2 decimals

// numbered placeholders
echo sprintf("Hi %1$s, bye %1$s", "Alice"); // Hi Alice, bye Alice

// number_format
echo number_format(1234567.891, 2);  // 1,234,567.89
echo number_format(1234567.89, 2, ',', '.'); // European format

정규식 (PCRE)

PHP는 /pattern/ 구분자와 함께 PCRE(Perl 호환 정규식)를 사용합니다. preg_match는 일치하면 1, 아니면 0을 반환합니다(0은 falsy이므로 ==가 아닌 ===를 사용). 항상 정규식으로 사용자 입력을 검증하지만 그것만 의존하지 마세요 — 이메일, URL에는 filter_var()를 사용하세요. preg_replace는 강력하지만 큰 문자열에서 느릴 수 있습니다. 블랙리스트 대신 문자를 화이트리스트하려면 [^...]를 사용하세요. i 플래그는 대소문자 구분 없이 일치시킵니다.

php
<?php
// preg_match: test pattern (returns 0 or 1)
if (preg_match('/^[a-z]+$/i', "Hello")) {
    echo "alphabetic";
}

// capture groups
preg_match('/(d{4})-(d{2})-(d{2})/', "2024-01-15", $matches);
// $matches[0] = "2024-01-15" (full match)
// $matches[1] = "2024" (group 1)
// $matches[2] = "01"
// $matches[3] = "15"

// preg_match_all: all matches
preg_match_all('/d+/', "a1 b22 c333", $matches);
// $matches[0] = ["1", "22", "333"]

// preg_replace: substitute
$clean = preg_replace('/[^a-z0-9]/i', '', "Hello, World!"); // HelloWorld

// preg_split: split by pattern
$parts = preg_split('/[s,]+/', "one, two,three  four");
// ["one", "two", "three", "four"]

멀티바이트 & 인코딩

PHP의 기본 문자열 함수는 문자 지향이 아닌 바이트 지향입니다 — 멀티바이트 문자(UTF-8, 중국어, 이모지)에서 깨집니다. 비 ASCII 텍스트에는 항상 mb_* 함수(mb_strlen, mb_substr, mb_strpos, mb_strtoupper)를 사용하세요. 애플리케이션 시작 시 mb_internal_encoding('UTF-8')을 설정하세요. JSON 출력에서 중국어/이모지를 읽을 수 있게 유지하려면 JSON_UNESCAPED_UNICODE를 사용하세요. 이는 국제 애플리케이션에서 버그의 일반적인 원인입니다.

php
<?php
// UTF-8 strings: use mb_* functions
$text = "Café";  // 4 chars, 5 bytes (é = 2 bytes)

echo strlen($text);      // 5 (bytes!)
echo mb_strlen($text);   // 4 (characters!)

echo substr($text, 0, 3);    // "Caf" (might break UTF-8!)
echo mb_substr($text, 0, 3); // "Caf" (safe)

// case conversion
echo strtoupper("straße");    // "STRAßE" (wrong!)
echo mb_strtoupper("straße"); // "STRAßE" (correct, locale-aware)

// encoding detection
$encoding = mb_detect_encoding($text);
$utf8 = mb_convert_encoding($text, 'UTF-8', 'auto');

// set internal encoding
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');

// JSON with UTF-8
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
03

배열

인덱스 & 연관 배열

PHP 배열은 실제로 정렬된 해시 맵입니다 — 리스트와 딕셔너리 모두로 작동합니다. 인덱스 배열은 숫자 키를 자동 할당; 연관 배열은 문자열 키를 사용합니다. isset()은 null 값에 대해 false를 반환; array_key_exists()는 null에도 true를 반환합니다. unset()은 요소를 제거하지만 재인덱스하지 않습니다. 진정한 리스트(간격 없음)를 위해 삭제 후 array_values()로 재인덱스하세요. PHP 8.1+에는 readonly 배열 타입이 있습니다.

php
<?php
// indexed array (numeric keys)
$nums = [10, 20, 30];
$nums[] = 40;              // append
echo $nums[0];             // 10
echo count($nums);         // 4

// associative array (string keys)
$user = [
    "name" => "Alice",
    "age" => 30,
    "email" => "[email protected]",
];
echo $user["name"];        // Alice
$user["phone"] = "555-1234"; // add key

// mixed keys
$mixed = [0 => "a", "name" => "b", 5 => "c"];

// check key existence
if (isset($user["email"])) { /* ... */ }
if (array_key_exists("name", $user)) { /* ... */ }

// remove element
unset($user["phone"]);

다차원 & 반복

다차원 배열은 배열의 배열입니다. foreach는 반복하는 관용적인 방법입니다 — for 루프보다 빠르고 읽기 쉽습니다. 요소를 제자리에서 수정하려면 &$value를 사용하세요(버그를 피하기 위해 루프 후 항상 참조 해제). array_column()은 2D 배열에서 단일 열을 추출합니다 — 데이터베이스 결과 집합을 변환하는 데 매우 유용합니다. PHP 배열은 삽입 순서를 유지합니다.

php
<?php
$users = [
    ["name" => "Alice", "age" => 30],
    ["name" => "Bob", "age" => 25],
    ["name" => "Carol", "age" => 35],
];

// iterate with key + value
foreach ($users as $index => $user) {
    echo "$index: {$user['name']} ({$user['age']})\n";
}

// modify by reference
foreach ($users as &$user) {
    $user['age'] += 1; // increment each age
}
unset($user); // break reference!

// nested iteration
$matrix = [[1, 2], [3, 4], [5, 6]];
foreach ($matrix as $row) {
    foreach ($row as $cell) {
        echo $cell . " ";
    }
    echo "\n";
}

// extract column
$names = array_column($users, 'name'); // ["Alice", "Bob", "Carol"]

배열 함수: map, filter, reduce

array_map, array_filter, array_reduce는 배열의 함수형 프로그래밍 3인방입니다. 화살표 함수(fn() =>)는 이들을 간결하게 만듭니다. array_filter는 키를 보존합니다 — 필요하면 array_values()로 재인덱스하세요. array_merge는 숫자 키를 재인덱스하지만 문자열 키는 보존합니다(나중 값이 덮어씀). array_column, array_chunk, array_slice는 데이터 조작에 필수적입니다. 이 함수들은 PHP에서 데이터 처리의 중추입니다.

php
<?php
$nums = [1, 2, 3, 4, 5];

// map: transform each element
$doubled = array_map(fn($n) => $n * 2, $nums);  // [2, 4, 6, 8, 10]

// filter: keep elements matching condition
$evens = array_filter($nums, fn($n) => $n % 2 === 0);  // [2, 4]

// reduce: accumulate to single value
$sum = array_reduce($nums, fn($carry, $n) => $carry + $n, 0);  // 15

// walk: like map but modifies in place (by reference)
array_walk($nums, fn(&$n) => $n *= 2);

// combining arrays
$merged = array_merge([1, 2], [3, 4]);        // [1, 2, 3, 4]
$combined = array_combine(['a', 'b'], [1, 2]); // ['a' => 1, 'b' => 2]
$sliced = array_slice($nums, 1, 2);            // [2, 3]
$chunked = array_chunk($nums, 2);              // [[1,2], [3,4], [5]]

배열 정렬

PHP 정렬 함수는 배열을 제자리에서 수정합니다(참조로 전달). sort/rsort는 재인덱스; asort/arsort는 키를 보존합니다. usort와 비교 함수(<=> 사용)는 사용자 정의 논리로 정렬합니다. natsort()는 자연 정렬을 수행합니다(img10보다 img2가 먼저) — 파일명에 필수적입니다. 다차원 배열의 경우 원하는 필드를 비교하는 클로저와 함께 usort를 사용하세요. 우주선 연산자(<=>)는 비교 함수를 단순화합니다.

php
<?php
$nums = [3, 1, 4, 1, 5, 9, 2, 6];

// sort by value (reindexes)
sort($nums);                          // [1, 1, 2, 3, 4, 5, 6, 9]
rsort($nums);                         // descending

// sort preserving keys
asort($nums);   // ascending, preserve keys
arsort($nums);  // descending, preserve keys

// sort by key
ksort($nums);   // by key ascending
krsort($nums);  // by key descending

// custom sort with callback
$users = [["name" => "Bob", "age" => 25], ["name" => "Alice", "age" => 30]];
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// sorted by age ascending

// natural sort (for strings with numbers)
$files = ["img10.jpg", "img2.jpg", "img1.jpg"];
natsort($files); // ["img1.jpg", "img2.jpg", "img10.jpg"]
sort($files);    // ["img1.jpg", "img10.jpg", "img2.jpg"] (wrong!)

배열 검사 & 조작

in_array에 strict=true(세 번째 매개변수)를 사용하면 타입 강제 변환 버그를 방지합니다. array_search는 키를 반환합니다(=== false로 확인). array_push/pop은 LIFO(스택)를 구현; array_shift/unshift는 FIFO(큐)를 구현 — 하지만 shift는 O(n)입니다. 큰 큐의 경우 SplQueue나 SplDoublyLinkedList를 사용하세요. array_unique는 키를 보존합니다. array_diff/intersect는 값을 비교; 키 기반 비교에는 array_diff_key/intersect_key를 사용하세요.

php
<?php
$arr = [1, 2, 3, 4, 5];

// inspection
echo count($arr);                  // 5
echo in_array(3, $arr);            // true
echo in_array("3", $arr, true);    // false (strict)
echo array_search(3, $arr);        // 2 (key, false if not found)
print_r(array_keys($arr));         // [0, 1, 2, 3, 4]
print_r(array_values($arr));       // [1, 2, 3, 4, 5]

// stack/queue operations
array_push($arr, 6);    // push to end
$last = array_pop($arr); // pop from end
$first = array_shift($arr); // remove from front
array_unshift($arr, 0); // add to front

// set operations
$unique = array_unique([1, 2, 2, 3]); // [1, 2, 3]
$diff = array_diff([1, 2, 3], [2, 3, 4]); // [1] (in first, not second)
$intersect = array_intersect([1, 2, 3], [2, 3, 4]); // [2, 3]

// flip and reverse
$flipped = array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b']
$reversed = array_reverse([1, 2, 3]); // [3, 2, 1]
04

제어 흐름

If / Else / Elseif

PHP는 elseif(한 단어)를 사용합니다 — 공백이 있는 'else if'가 아닙니다(물론 그것도 작동합니다). 대체 구문(if: ... endif;)은 중괄호 매칭 혼란을 피하기 위해 HTML 템플릿에 유용합니다. 삼항 연산자는 우결합성입니다 — 중첩을 피하세요. 널 병합 할당 연산자(??=)는 현재 null인 경우에만 값을 설정합니다 — 설정 기본값의 지연 초기화에 완벽합니다.

php
<?php
$score = 85;

if ($score >= 90) {
    $grade = "A";
} elseif ($score >= 80) {
    $grade = "B";
} elseif ($score >= 70) {
    $grade = "C";
} else {
    $grade = "F";
}

// alternative syntax (for templates)
if ($score >= 90):
    echo "Excellent";
elseif ($score >= 80):
    echo "Good";
else:
    echo "Try harder";
endif;

// ternary
$status = $age >= 18 ? "adult" : "minor";

// null coalescing assignment (PHP 7.4+)
$config['timeout'] ??= 30; // set if not set

Switch & Match

switch는 느슨한 비교(==)를 사용하고 fall-through를 방지하기 위해 break가 필요합니다 — 일반적인 버그 원인입니다. match(PHP 8+)는 엄격한 비교(===)를 사용하고, 값을 직접 반환하며, 일치하는 arm이 없으면 예외를 던집니다(조용한 실패 없음). 값이 필요할 때 match는 switch의 현대적 대체입니다. 복잡한 다중 명령문 케이스에는 switch를; 단순 값 선택에는 match를 사용하세요. 항상 default 케이스를 포함하세요.

php
<?php
// switch: loose comparison (==)
$day = "Mon";
switch ($day) {
    case "Mon":
    case "Tue":
    case "Wed":
        echo "Weekday";
        break;
    case "Sat":
    case "Sun":
        echo "Weekend";
        break;
    default:
        echo "Unknown";
}

// match (PHP 8+): strict comparison (===), returns value
$status = 404;
$message = match($status) {
    200, 201 => "Success",
    301, 302 => "Redirect",
    404 => "Not Found",
    500 => "Server Error",
    default => "Unknown",
};
// match throws UnhandledMatchError if no match and no default

루프: for, while, foreach, do-while

foreach는 배열의 관용적 루프입니다 — count()와 for보다 빠르고 안전합니다. continue로 반복을 건너뛰고 break로 종료하세요. PHP에는 레이블이 있는 break/continue가 없습니다(Java/Rust와 달리). 연관 배열의 경우 foreach ($arr as $key => $value)가 표준 패턴입니다. do-while은 최소 한 번 실행 — 입력 검증에 유용합니다. foreach 중에 배열을 수정하지 마세요(결과를 별도 배열에 저장).

php
<?php
// for loop
for ($i = 0; $i < 5; $i++) {
    echo $i;  // 01234
}

// foreach (most common in PHP)
$fruits = ["apple", "banana", "cherry"];
foreach ($fruits as $fruit) {
    echo $fruit;
}

// foreach with key
foreach ($fruits as $index => $fruit) {
    echo "$index: $fruit";
}

// while
$count = 0;
while ($count < 3) {
    echo $count++;
}

// do-while (runs at least once)
do {
    $line = readline("> ");
} while ($line !== "quit");

// break and continue
for ($i = 0; $i < 10; $i++) {
    if ($i === 3) continue; // skip 3
    if ($i === 7) break;    // stop at 7
    echo $i;
}

템플릿에서의 제어 흐름

PHP의 대체 제어 구문(if:/elseif:/else:/endif;, foreach:/endforeach;)은 HTML 템플릿용으로 설계되었습니다. <?= $var ?>는 <?php echo $var; ?>의 약어입니다 — 템플릿에서 가독성을 위해 항상 사용하세요. XSS를 방지하기 위해 항상 htmlspecialchars()로 출력을 이스케이프하세요. PHP 로직과 HTML 프레젠테이션의 분리는 Twig와 Blade 같은 템플릿 시스템의 기초이며, 더 깔끔한 구문과 자동 이스케이핑을 제공합니다.

php
<?php // template file ?>
<?php if ($user->isAdmin()): ?>
    <div class="admin-panel">Admin Tools</div>
<?php elseif ($user->isEditor()): ?>
    <div class="editor-tools">Edit Tools</div>
<?php else: ?>
    <div class="user-view">Read Only</div>
<?php endif; ?>

<?php foreach ($products as $p): ?>
    <div class="product">
        <?= htmlspecialchars($p['name']) ?>
        - $<?= number_format($p['price'], 2) ?>
    </div>
<?php endforeach; ?>

<?php // shorthand echo ?>
<h1><?= $title ?></h1>

<?php // ternary in templates ?>
<span class="<?= $active ? 'on' : 'off' ?>"><?= $active ? 'Active' : 'Inactive' ?></span>

예외 & 에러 처리

PHP 7+는 대부분의 에러에 예외를 사용합니다. 항상 특정 예외 타입(단순 Exception이 아닌)을 catch하여 다른 실패를 적절히 처리하세요. finally는 항상 실행됩니다 — 정리(파일 닫기, 연결)에 사용하세요. 사용자 정의 예외는 Exception을 확장하고 도메인 컨텍스트를 추가합니다. PHP 8+는 |로 여러 예외 타입을 catch할 수 있습니다. 일관된 에러 처리를 위해 PDO를 예외 모드로 설정하세요. 로깅 없이 예외를 catch하지 마세요 — 조용한 실패는 버그를 숨깁니다.

php
<?php
// try / catch / finally
try {
    $pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    error_log("DB connection failed: " . $e->getMessage());
    die("Service unavailable");
} finally {
    // always runs, even after return/throw
    echo "Cleanup";
}

// custom exception
class ValidationException extends Exception {
    public function __construct(string $field, string $message = "") {
        parent::__construct("$field: $message");
    }
}

// throw
if (empty($email)) {
    throw new ValidationException("email", "is required");
}

// catch multiple types (PHP 8+)
try {
    riskyOperation();
} catch (PDOException | RuntimeException $e) {
    // catches either type
    log_error($e);
}
05

함수

함수 정의

PHP 7+는 타입이 지정된 매개변수와 반환 타입(int, string, array, ?Type은 nullable)을 지원합니다. PHP 8+는 명명된 인수(기본값 건너뛰기, 매개변수 순서 변경), 유니온 타입(int|string), mixed 타입을 추가합니다. 가변 매개변수(...$nums)는 추가 인수를 배열로 수집합니다. 전개 연산자(...$arr)는 배열을 인수로 언팩합니다. 참조로 전달(&)은 원본을 수정합니다 — 코드를 추론하기 어렵게 만들므로 드물게 사용하세요.

php
<?php
// basic function with return type
function add(int $a, int $b): int {
    return $a + $b;
}

// default parameters
function greet(string $name, string $greeting = "Hello"): string {
    return "$greeting, $name!";
}

// named arguments (PHP 8+)
echo greet(name: "Alice", greeting: "Hi");

// variadic functions
function sum(int ...$nums): int {
    return array_sum($nums);
}
echo sum(1, 2, 3, 4);  // 10

// spread operator
$nums = [1, 2, 3];
echo sum(...$nums);  // 6

// pass by reference
function increment(int &$n): void {
    $n++;
}
$x = 5;
increment($x);
echo $x;  // 6

화살표 함수 & 클로저

화살표 함수(fn() =>)는 간결하고, 단일 표현식 클로저로 외부 변수를 값으로 자동 캡처합니다. 전통적인 클로저(function() use ($var))는 다중 줄 본문이나 참조로 캡처(&$var)할 때 필요합니다. 클로저는 array_map, array_filter, usort, 이벤트 핸들러에 필수적입니다. 화살표 함수는 명령문을 가질 수 없습니다(if, for 없음) — 복잡한 로직에는 전통적인 클로저를 사용하세요. 클로저는 일급 객체입니다(Closure 클래스).

php
<?php
// arrow function (PHP 7.4+): single expression, auto-capture
$square = fn($x) => $x * $x;
echo $square(5);  // 25

// auto-captures outer variables by value
$multiplier = 3;
$multiply = fn($x) => $x * $multiplier;
echo $multiply(5);  // 15

// traditional closure (multi-line, explicit capture)
$factor = 10;
$scale = function ($x) use ($factor) {
    return $x * $factor + 1;
};

// capture by reference
$count = 0;
$increment = function () use (&$count) {
    $count++;
};
$increment();
echo $count;  // 1

// closures as callbacks
$nums = [1, 2, 3, 4];
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
$doubled = array_map(fn($n) => $n * 2, $nums);

변수 범위 & 전역

PHP는 함수 수준 범위를 가집니다 — 함수 외부에 정의된 변수는 'global'이나 $GLOBALS 없이는 함수 내부에서 접근할 수 없습니다. 'global'을 피하세요 — 숨겨진 의존성을 만들고 테스트를 어렵게 합니다. 대신 의존성 주입을 사용하세요. static 변수는 함수 호출 간에 유지되지만 함수에 범위가 지정됩니다 — 캐싱/메모이제이션에 유용하지만 장기 실행 프로세스에서 문제를 일으킬 수 있습니다. 클로저는 'use'로 변수를 명시적으로 캡처해야 합니다.

php
<?php
$global = "I'm global";

function testScope(): void {
    // echo $global; // ERROR: not in scope
    global $global;  // import global
    echo $global;    // OK

    // $GLOBALS superglobal (alternative)
    echo $GLOBALS['global'];
}

// static variables: persist across calls
function counter(): int {
    static $count = 0;
    return ++$count;
}
echo counter();  // 1
echo counter();  // 2
echo counter();  // 3

// closures don't see outer scope by default
$outer = "hello";
$closure = function () {
    // echo $outer; // ERROR
};
$closure2 = function () use ($outer) {
    echo $outer;  // OK, captured
};

타입 선언 & 엄격한 타입

declare(strict_types=1)은 첫 번째 명령문이어야 합니다 — 전체 파일에 대해 엄격한 타입 검사(강제 변환 없음)를 적용합니다. 없으면 PHP가 타입을 강제 변환합니다(문자열 매개변수에 전달된 int 5는 '5'가 됩니다). 새 코드에는 항상 엄격한 타입을 사용하세요. PHP 8+는 유니온 타입, mixed, never(함수가 반환하지 않음), static(클래스 반환)을 추가합니다. 일급 호출 가능 구문(func(...))은 모든 callable에서 클로저를 생성합니다 — 함수 참조보다 깔끔합니다.

php
<?php
// declare strict types (FIRST line of file)
declare(strict_types=1);

function divide(float $a, float $b): float {
    if ($b === 0.0) {
        throw new DivisionByZeroError();
    }
    return $a / $b;
}

// nullable types (?Type or Type|null)
function findUser(?int $id): ?string {
    if ($id === null) return null;
    return "User $id";
}

// union types (PHP 8+)
function process(int|string $input): int|string {
    return is_int($input) ? $input * 2 : strtoupper($input);
}

// return types: void, never, mixed, static
function log(string $msg): void { /* no return */ }
function redirect(): never { header("Location: /"); exit; }

// first-class callable syntax (PHP 8.1+)
$func = strlen(...);  // creates Closure from function
echo $func("hello");  // 5

제너레이터 & Yield

제너레이터(yield가 있는 함수)는 값을 지연해서 생성합니다 — 모든 값을 미리 계산하지 않아 메모리를 절약합니다. 이는 큰 파일이나 데이터셋을 처리하는 데 필수적입니다. yield는 함수를 일시 정지하고 값을 반환; 다음 값이 요청되면 함수가 재개됩니다. 제너레이터는 Iterator를 구현하여 foreach와 작동합니다. 파일 처리, 데이터베이스 행 반복, 무한 시퀀스, 파이프라인에 제너레이터를 사용하세요. key=>value 쌍을 yield할 수도 있고 send()로 값을 받을 수도 있습니다.

php
<?php
// generator: memory-efficient iteration
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (($line = fgets($handle)) !== false) {
        yield trim($line);
    }
    fclose($handle);
}

foreach (readLines("large.txt") as $line) {
    echo $line;  // one line at a time, low memory
}

// infinite generator
function fibonacci(): Generator {
    [$a, $b] = [0, 1];
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

// take first 10
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
    echo $fib->current() . " ";
    $fib->next();
}

// yield with key
function pairs(): Generator {
    yield 'a' => 1;
    yield 'b' => 2;
    yield 'c' => 3;
}
06

OOP & 클래스

클래스, 속성 & 생성자

PHP 8 생성자 프로모션은 보일러플레이트를 제거합니다 — 속성을 생성자 매개변수로 선언합니다. 속성 가시성: public(어디서나), protected(클래스 + 서브클래스), private(클래스만). readonly(PHP 8.1)는 초기화 후 수정을 방지합니다. self는 현재 클래스를 참조; static은 호출 클래스를 참조(지연 정적 바인딩). 상속 계층에서 적절한 다형성을 위해 self:: 대신 static::을 사용하세요.

php
<?php
class Person {
    // typed properties (PHP 7.4+)
    public string $name;
    protected int $age;
    private string $email;
    public readonly string $id;  // PHP 8.1+

    // constructor promotion (PHP 8+)
    public function __construct(
        string $name,
        int $age,
        string $email = "",
        string $id = ""
    ) {
        $this->name = $name;
        $this->age = $age;
        $this->email = $email;
        $this->id = $id;
    }

    // methods
    public function greet(): string {
        return "Hi, I'm {$this->name}";
    }

    // static method
    public static function create(string $name): self {
        return new self($name, 0);
    }
}

$p = new Person("Alice", 30);
echo $p->greet();
echo Person::create("Bob")->name;

상속 & 추상 클래스

abstract 클래스는 인스턴스화할 수 없습니다 — 서브클래스를 위한 템플릿을 정의합니다. 추상 메서드는 구체적인 서브클래스가 구현해야 합니다. PHP는 단일 상속만 지원합니다(하나의 extends). 구현이 변경되지 않아야 할 때 final로 상속/오버라이딩을 방지하세요. protected 멤버는 서브클래스에서 접근 가능 — 내부 API에 사용하세요. 부모에 생성자가 있으면 항상 parent::__construct()를 호출하세요. instanceof로 타입 확인: if ($dog instanceof Animal).

php
<?php
abstract class Animal {
    protected string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // abstract method: must be implemented by subclasses
    abstract public function speak(): string;

    // concrete method: inherited as-is
    public function describe(): string {
        return "{$this->name} says {$this->speak()}";
    }
}

class Dog extends Animal {
    public function speak(): string {
        return "Woof";
    }
}

class Cat extends Animal {
    public function speak(): string {
        return "Meow";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof

// final class/method: cannot be extended/overridden
final class Singleton { /* ... */ }

인터페이스 & 트레이트

인터페이스는 계약을 정의 — 클래스는 여러 인터페이스를 구현할 수 있습니다(단일 상속과 달리). 모든 인터페이스 메서드는 public이어야 합니다. 트레이트는 상속 없는 코드 재사용을 제공 — 언어 수준의 '복사-붙여넣기'입니다. 트레이트는 속성, 메서드, 추상 메서드를 가질 수 있습니다. 횡단 관심사(타임스탬프, 로깅, 소프트 삭제)에 트레이트를 사용하세요. 충돌 해결: 트레이트에 같은 메서드 이름이 있을 때 TraitA::method insteadof TraitB를 사용하세요.

php
<?php
// interface: contract (no implementation)
interface Comparable {
    public function compareTo(object $other): int;
}

interface JsonSerializable {
    public function jsonSerialize(): mixed;
}

// a class can implement multiple interfaces
class Product implements Comparable, JsonSerializable {
    public function __construct(public float $price) {}

    public function compareTo(object $other): int {
        return $this->price <=> $other->price;
    }

    public function jsonSerialize(): mixed {
        return ['price' => $this->price];
    }
}

// trait: reusable code (horizontal reuse)
trait Timestampable {
    public DateTime $createdAt;

    public function setCreatedAt(): void {
        $this->createdAt = new DateTime();
    }

    public function age(): DateInterval {
        return $this->createdAt->diff(new DateTime());
    }
}

class Article {
    use Timestampable; // use trait
}

매직 메서드

매직 메서드는 객체 작업을 가로채는 특수 메서드입니다. __get/__set은 속성 오버로딩(동적 속성)을 구현합니다. __toString은 문자열 캐스팅을 가능하게 합니다. __invoke는 객체를 호출 가능하게 만듭니다. __clone은 복제 시 실행됩니다(clone $obj). 추적하기 어려운 '매직'을 추가하므로 드물게 사용하세요. __get/__set은 데이터 전송 객체나 지연 로딩에 유용합니다. 매직 동작을 항상 명확히 문서화하세요. PHP 8.2는 동적 속성을 사용 중단 — __get/__set이나 #[AllowDynamicProperties]를 사용하세요.

php
<?php
class Magic {
    private array $data = [];

    // called when accessing undefined property
    public function __get(string $name): mixed {
        return $this->data[$name] ?? null;
    }

    // called when setting undefined property
    public function __set(string $name, mixed $value): void {
        $this->data[$name] = $value;
    }

    // called when isset() or empty() on undefined property
    public function __isset(string $name): bool {
        return isset($this->data[$name]);
    }

    // called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // called when object is called as function
    public function __invoke(string $arg): string {
        return "Called with: $arg";
    }

    // called on clone
    public function __clone(): void {
        $this->data = []; // reset on clone
    }
}

$m = new Magic();
$m->foo = "bar";       // __set
echo $m->foo;          // __get -> "bar"
echo $m;               // __toString -> {"foo":"bar"}
echo $m("test");       // __invoke

네임스페이스 & 오토로딩

네임스페이스는 클래스 이름 충돌을 방지합니다 — Java의 패키지와 같습니다. 네임스페이스는 첫 번째 명령문이어야 합니다. use는 클래스를 임포트합니다(선택적 별칭: use Foo\Bar as B). PSR-4 오토로딩은 네임스페이스를 파일 경로에 매핑: App\Models\User → src/Models/User.php. Composer의 오토로더(require 'vendor/autoload.php')가 이를 자동으로 처리합니다. 현대 PHP에는 항상 네임스페이스를 사용하세요. 문자열의 \\는 이스케이프된 백슬래시(네임스페이스 구분자)입니다.

php
<?php
// file: src/Models/User.php
namespace App\Models;

use App\Database\Connection;
use App\Exceptions\UserNotFoundException;

class User {
    private Connection $db;

    public function __construct(Connection $db) {
        $this->db = $db;
    }

    public function find(int $id): ?self {
        // ...
        throw new UserNotFoundException("User $id not found");
    }
}

// composer.json (PSR-4 autoloading)
// {
//   "autoload": {
//     "psr-4": { "App\\": "src/" }
//   }
// }

// usage
use App\Models\User;
$user = new User($db);
07

웹, 폼 & 파일 I/O

폼 처리 & 검증

항상 서버 측에서 검증 — 클라이언트 측 검증은 보안이 아닌 UX용입니다. filter_input/filter_var와 FILTER_VALIDATE_*는 잘못된 입력에 대해 false를 반환합니다. 검증 전에 문자열을 trim하세요. 데이터베이스 삽입에는 준비된 문장을 사용하세요. CSRF 토큰은 크로스 사이트 요청 위조를 방지 — 세션당 생성하고 POST에서 검증합니다. bin2hex(random_bytes(32))는 암호학적으로 안전한 토큰을 생성합니다. 사용자 입력을 절대 신뢰하지 마세요 — 검증, 살균, 이스케이프하세요.

php
<?php
// process POST form
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
    $age = filter_var($_POST['age'] ?? 0, FILTER_VALIDATE_INT);

    $errors = [];

    if (empty($name)) {
        $errors[] = "Name is required";
    } elseif (strlen($name) > 100) {
        $errors[] = "Name too long";
    }

    if ($email === false) {
        $errors[] = "Valid email required";
    }

    if ($age === false || $age < 18) {
        $errors[] = "Must be 18+";
    }

    if (empty($errors)) {
        // process valid data
        // save to database, redirect, etc.
        header("Location: /success");
        exit;
    }
}

// CSRF token
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['csrf'] = $token;
// in form: <input type="hidden" name="csrf" value="<?= $token ?>">

세션 & 쿠키

세션은 서버 측에 데이터를 저장합니다(세션 ID 쿠키로 식별). session_start()는 출력 전에 호출해야 합니다(또는 ob_start() 사용). 세션에 최소한의 데이터를 저장 — 서버 메모리를 소비합니다. 쿠키의 경우 항상 secure(HTTPS만), httponly(XSS 접근 방지), samesite(CSRF 보호)를 설정하세요. 세션을 적절히 파괴: 변수 해제, 세션 파괴, 쿠키 삭제. 확장 가능한 앱의 경우 파일 대신 Redis/데이터베이스 기반 세션 핸들러를 사용하세요.

php
<?php
// start session (must be before any output)
session_start();

// set session data
$_SESSION['user_id'] = 42;
$_SESSION['username'] = "Alice";
$_SESSION['login_time'] = time();

// read session data
$userId = $_SESSION['user_id'] ?? null;

// check if logged in
function isLoggedIn(): bool {
    return isset($_SESSION['user_id']);
}

// destroy session
session_unset();     // clear variables
session_destroy();   // destroy session
setcookie(session_name(), '', time() - 3600, '/'); // clear cookie

// cookies
setcookie("theme", "dark", [
    'expires' => time() + 86400 * 30, // 30 days
    'path' => '/',
    'secure' => true,     // HTTPS only
    'httponly' => true,   // not accessible via JS
    'samesite' => 'Strict', // CSRF protection
]);

// read cookie
$theme = $_COOKIE['theme'] ?? 'light';

파일 I/O

file_get_contents/file_put_contents는 작은 파일에 편리합니다. 큰 파일의 경우 fopen/fread/fwrite와 스트림을 사용하세요. fgetcsv/fputcsv는 CSV 형식을 처리합니다(따옴표/이스케이핑 포함). json_decode에 true를 전달하면 연관 배열을 반환합니다(기본은 객체). 항상 file_exists를 확인하고 에러를 처리하세요(권한, 디스크 가득 참). 파일 업로드의 경우 보안을 위해 move_uploaded_file()을 사용하세요. 동시에 쓸 때 flock()으로 파일을 잠그세요.

php
<?php
// read entire file
$content = file_get_contents("data.txt");
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// write file
file_put_contents("output.txt", "Hello World");
file_put_contents("log.txt", "entry\n", FILE_APPEND); // append

// CSV
$csv = fopen("data.csv", "r");
while (($row = fgetcsv($csv)) !== false) {
    print_r($row); // array of column values
}
fclose($csv);

// write CSV
$out = fopen("export.csv", "w");
fputcsv($out, ["Name", "Email", "Age"]);
fputcsv($out, ["Alice", "[email protected]", 30]);
fclose($out);

// JSON
$data = json_decode(file_get_contents("config.json"), true); // assoc array
file_put_contents("config.json", json_encode($data, JSON_PRETTY_PRINT));

// file info
file_exists("data.txt");  // bool
filesize("data.txt");     // bytes
filemtime("data.txt");    // modification timestamp
is_dir("folder");         // bool

파일 업로드

파일 업로드는 $_POST가 아닌 $_FILES를 통해 옵니다. 항상 검증: 에러 코드 확인, finfo로 MIME 타입 확인($_FILES['type']은 클라이언트 제공이며 위조 가능하므로 사용하지 마세요), 크기 제한, 안전한 파일명 생성(원본 이름을 절대 신뢰하지 마세요). move_uploaded_file()은 보안 함수 — HTTP POST를 통해 업로드되었는지 확인합니다. 웹 루트 외부에 저장하거나 직접 접근을 방지하기 위해 PHP를 통해 서브하세요. 업로드를 맬웨어 스캔하는 것을 고려하세요.

php
<?php
// HTML: <form method="POST" enctype="multipart/form-data">
//   <input type="file" name="document">
//   <input type="submit">
// </form>

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $file = $_FILES['document'];

    // $file contains:
    // ['name'] => original filename
    // ['type'] => MIME type (unreliable!)
    // ['tmp_name'] => temporary path
    // ['error'] => UPLOAD_ERR_OK (0) on success
    // ['size'] => file size in bytes

    if ($file['error'] !== UPLOAD_ERR_OK) {
        die("Upload error: " . $file['error']);
    }

    // validate
    $allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($file['tmp_name']);

    if (!in_array($mimeType, $allowedTypes)) {
        die("Invalid file type");
    }

    if ($file['size'] > 5 * 1024 * 1024) { // 5MB
        die("File too large");
    }

    // move to permanent location
    $dest = "uploads/" . uniqid() . "_" . $file['name'];
    move_uploaded_file($file['tmp_name'], $dest);
    echo "Uploaded to: $dest";
}

cURL & HTTP 요청

cURL은 PHP의 표준 HTTP 클라이언트 — HTTPS, 리다이렉트, 쿠키, 인증을 처리합니다. 항상 CURLOPT_RETURNTRANSFER을 설정하여 응답을 문자열로 받으세요(그렇지 않으면 출력됨). 멈추지 않도록 타임아웃을 설정하세요. 단순한 요청의 경우 file_get_contents와 stream_context가 작동하지만 기능이 부족합니다. 프로덕션의 경우 Guzzle(composer require guzzlehttp/guzzle)이나 Symfony HTTP Client를 사용하세요 — 더 나은 API, 재시도 로직, PSR-18 준수를 제공합니다.

php
<?php
// GET request
$ch = curl_init("https://api.example.com/users");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 200) {
    $data = json_decode($response, true);
    print_r($data);
}

// POST request with JSON
$ch = curl_init("https://api.example.com/users");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode(['name' => 'Alice']),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

// simpler: file_get_contents with context
$context = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json\r\n",
        'content' => json_encode(['name' => 'Alice']),
    ],
]);
$result = file_get_contents("https://api.example.com/users", false, $context);
08

데이터베이스 (PDO)

PDO 연결 & 기본

PDO(PHP Data Objects)는 표준 데이터베이스 추상화 계층 — MySQL, PostgreSQL, SQLite 등을 지원합니다. 적절한 에러 처리를 위해 ERRMODE_EXCEPTION을, 실제 준비된 문장을 위해 ATTR_EMULATE_PREPARES=false를 항상 설정하세요. FETCH_ASSOC은 연관 배열을 반환(FETCH_OBJ는 객체, FETCH_CLASS는 클래스에 매핑). 완전한 Unicode 지원(이모지 포함)을 위해 항상 utf8mb4 문자셋을 사용하세요. 싱글톤이나 DI 컨테이너에 연결을 저장하세요.

php
<?php
// connect (always use exception mode)
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
$pdo = new PDO($dsn, "username", "password", [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
]);

// simple query
$stmt = $pdo->query("SELECT * FROM users LIMIT 5");
$users = $stmt->fetchAll(); // array of associative arrays

// fetch one row
$user = $pdo->query("SELECT * FROM users WHERE id = 1")->fetch();

// fetch column
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

// execute (no params)
$pdo->exec("DELETE FROM logs WHERE created_at < '2023-01-01'");
$deleted = $pdo->rowCount(); // affected rows

준비된 문장 (SQL 인젝션 방지)

준비된 문장은 사용자 입력이 있는 모든 쿼리에 필수 — SQL 구조와 데이터를 분리하여 인젝션을 불가능하게 합니다. 위치는 ?로, 명명된 매개변수는 :name으로 사용하세요. IN 절의 경우 자리 표시자 문자열을 동적으로 빌드해야 합니다(하지만 값은 여전히 매개변수화됨). lastInsertId()는 마지막 자동 증가 값을 반환합니다. 이스케이핑 함수로도 사용자 입력을 SQL에 연결하지 마세요. 이는 PHP의 #1 보안 규칙입니다.

php
<?php
// prepared statements: THE way to prevent SQL injection
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->execute([$email, 'active']);
$user = $stmt->fetch();

// named parameters (more readable)
$stmt = $pdo->prepare(
    "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
);
$stmt->execute([
    ':name' => 'Alice',
    ':email' => '[email protected]',
    ':age' => 30,
]);
$id = $pdo->lastInsertId(); // get auto-increment ID

// IN clause with prepared statements
$ids = [1, 2, 3, 4];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
$users = $stmt->fetchAll();

// NEVER do this (SQL injection!):
// $pdo->query("SELECT * FROM users WHERE name = '$_GET[name]'");

트랜잭션 & 에러 처리

트랜잭션은 원자성을 보장 — 모든 작업이 성공하거나 모두 실패합니다. beginTransaction/commit/rollBack으로 작업 단위를 감쌉니다. 항상 트랜잭션을 try/catch로 감싸고 예외 발생 시 롤백하세요. PDO는 에러 시 PDOException을 던집니다(ERRMODE_EXCEPTION 사용). 잠금 경합을 줄이기 위해 트랜잭션을 짧게 유지하세요. 중첩 트랜잭션의 경우 저장점이나 트랜잭션 매니저를 사용하세요. 트랜잭션을 열어두지 마세요 — 항상 커밋하거나 롤백하세요.

php
<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
        ->execute([$amount, $fromId]);

    $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
        ->execute([$amount, $toId]);

    // log transaction
    $pdo->prepare("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)")
        ->execute([$fromId, $toId, $amount]);

    $pdo->commit();
    echo "Transfer complete";
} catch (PDOException $e) {
    $pdo->rollBack(); // undo all changes
    error_log("Transfer failed: " . $e->getMessage());
    throw new RuntimeException("Transfer failed", 0, $e);
}

// check if in transaction
if ($pdo->inTransaction()) {
    $pdo->commit();
}

데이터 가져오기 패턴

사용 사례에 맞는 가져오기 모드를 선택하세요. FETCH_ASSOC가 가장 일반적(열 이름이 있는 배열). FETCH_CLASS는 행을 객체로 매핑 — 도메인 모델에 적합. FETCH_KEY_PAIR는 id=>value 맵 생성(드롭다운용). FETCH_GROUP은 첫 번째 열로 행을 그룹화 — 일대다 관계에 유용. 큰 결과 집합의 경우 메모리를 절약하기 위해 fetchAll() 대신 루프에서 fetch()를 사용하세요. 완료 후 항상 $stmt->closeCursor()로 커서를 닫으세요.

php
<?php
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE active = 1");
$stmt->execute();

// fetch modes
$row = $stmt->fetch(PDO::FETCH_ASSOC);  // ['id' => 1, 'name' => 'Alice']
$row = $stmt->fetch(PDO::FETCH_NUM);    // [1, 'Alice', '[email protected]']
$row = $stmt->fetch(PDO::FETCH_BOTH);   // both (default)
$obj = $stmt->fetch(PDO::FETCH_OBJ);    // stdClass with properties

// fetch all
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);

// fetch into class
class User {
    public int $id;
    public string $name;
}
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
$users = $stmt->fetchAll(); // array of User objects

// fetch key-value pairs
$pairs = $pdo->query("SELECT id, name FROM users")
    ->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'Alice', 2 => 'Bob']

// fetch grouped
$grouped = $pdo->query("SELECT dept, name FROM employees")
    ->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
// ['IT' => [['name' => 'Alice']], 'HR' => [['name' => 'Bob']]]

데이터베이스 모범 사례

리포지토리 패턴은 데이터 접근을 비즈니스 로직에서 분리 — 코드를 테스트 가능(PDO 모킹)하고 유지보수 가능하게 만듭니다. 의존성 주입으로 PDO 연결을 전달하세요. 쿼리당 새 PDO 연결을 만들지 마세요 — 단일 연결(또는 풀)을 재사용하세요. 고트래픽 앱의 경우 연결 풀러(MySQL은 ProxySQL, PostgreSQL은 PgBouncer)를 고려하세요. 항상 EXPLAIN으로 느린 쿼리를 프로파일링하고 적절한 인덱스를 추가하세요. 복잡한 도메인의 경우 ORM(Doctrine, Eloquent)을 고려하세요.

php
<?php
// 1. Connection singleton (or use DI container)
class Database {
    private static ?PDO $instance = null;

    public static function conn(): PDO {
        if (self::$instance === null) {
            self::$instance = new PDO(
                "mysql:host=localhost;dbname=app;charset=utf8mb4",
                "user", "pass",
                [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
            );
        }
        return self::$instance;
    }
}

// 2. Repository pattern
class UserRepository {
    public function __construct(private PDO $db) {}

    public function findById(int $id): ?array {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        $user = $stmt->fetch();
        return $user ?: null;
    }
}

// 3. Always close statements (or let them go out of scope)
// 4. Use LIMIT for queries that might return huge results
// 5. Index columns used in WHERE, JOIN, ORDER BY
// 6. Use EXPLAIN to analyze slow queries
09

날짜/시간 & 보안

날짜 & 시간

PHP의 날짜 함수는 기본적으로 서버의 시간대를 사용 — 항상 date_default_timezone_set('Asia/Shanghai')를 설정하거나 DateTimeZone을 명시적으로 사용하세요. DateTime 클래스는 객체 지향적이며 시간대, 간격, 형식화를 절차적 함수보다 더 잘 처리합니다. strtotime()은 영어 날짜 설명('next Monday', '+1 month')을 파싱 — 편리하지만 월 경계에서 놀랄 수 있습니다. 날짜 계산에는 DateTime::diff()와 DateInterval을 사용하세요. 데이터베이스에는 항상 UTC로 날짜를 저장하세요.

php
<?php
// current time
$now = date('Y-m-d H:i:s');           // 2024-06-15 14:30:00
$timestamp = time();                   // Unix timestamp
$dt = new DateTime();                  // DateTime object

// formatting
echo date('Y-m-d');                    // 2024-06-15
echo date('d/m/Y H:i:s');              // 15/06/2024 14:30:00
echo date('l, F j, Y');                // Saturday, June 15, 2024

// create from string
$dt = new DateTime('2024-01-15');
$dt = DateTime::createFromFormat('d/m/Y', '15/01/2024');

// modify dates
$dt->modify('+1 month');
$dt->modify('-2 days');
$tomorrow = date('Y-m-d', strtotime('tomorrow'));
$nextWeek = date('Y-m-d', strtotime('+1 week'));

// difference
$start = new DateTime('2024-01-01');
$end = new DateTime('2024-12-31');
$diff = $start->diff($end);
echo $diff->days;  // 365

// timezone
$dt = new DateTime('now', new DateTimeZone('Asia/Shanghai'));
$dt->setTimezone(new DateTimeZone('UTC'));

비밀번호 해싱 & 보안

password_hash()는 bcrypt(또는 가능한 경우 Argon2)와 자동 솔트 생성을 사용 — 절대 직접 해싱을 만들지 마세요. password_verify()는 해시와 비밀번호를 안전하게 확인(타이밍 공격 방지를 위한 상수 시간 비교). password_needs_rehash()는 비용 요소를 증가시킬 때 해시를 업그레이드할 수 있게 합니다. 랜덤 토큰(CSRF, API 키, 비밀번호 재설정)의 경우 예측 가능한 rand()나 mt_rand()가 아닌 항상 random_bytes()를 사용하세요. 메시지 인증에는 hash_hmac을 사용하세요.

php
<?php
// hash password (bcrypt by default)
$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$... (includes algorithm, cost, salt)

// verify password
if (password_verify($input, $hash)) {
    echo "Valid password";
}

// check if hash needs rehash (algorithm upgrade)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // update stored hash
}

// NEVER use md5() or sha1() for passwords!
// NEVER store plaintext passwords!

// generate secure random
$token = bin2hex(random_bytes(32));    // 64-char hex string
$bytes = random_bytes(16);             // raw bytes
$int = random_int(1, 1000);            // cryptographically secure

// hash for integrity (not passwords)
$checksum = hash('sha256', $data);
$hmac = hash_hmac('sha256', $data, $secretKey);

출력 이스케이핑 & XSS 방지

XSS는 #1 웹 취약점 — 항상 컨텍스트에 따라 출력을 이스케이프하세요. HTML에는 htmlspecialchars()(ENT_QUOTES는 작은따옴표와 큰따옴표 모두 이스케이프). URL에는 urlencode(). JavaScript 컨텍스트에는 hex 플래그와 함께 json_encode(). 사용자 입력을 절대 신뢰하지 마세요 — 입력이 아닌 출력에서 이스케이프(다른 곳에서 원시 데이터가 필요할 수 있음). 심층 방어를 위해 Content-Security-Policy 헤더를 설정하세요. 기본적으로 자동 이스케이프하는 템플릿 엔진(Twig, Blade)을 고려하세요.

php
<?php
// XSS: Cross-Site Scripting — always escape output!
$name = $_GET['name']; // could be: <script>alert('xss')</script>

// HTML context
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// converts < > " ' & to HTML entities

// in HTML template
?>
<p>Hello, <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p>
<input value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>">

<?php
// URL context
$url = "https://example.com/search?q=" . urlencode($query);

// JavaScript context
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
    var data = <?= $json ?>;
</script>

<?php
// Content Security Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

JSON & API 응답

json_encode/decode는 표준 JSON 함수입니다. API 응답에는 항상 Content-Type: application/json을 설정하세요. 중국어/이모지를 읽을 수 있게 유지하려면 JSON_UNESCAPED_UNICODE를 사용하세요(그렇지 않으면 \uXXXX가 됨). json_decode에 true를 전달하면 연관 배열을 반환(PHP에서 더 일반적). 신뢰할 수 없는 JSON 디코딩 후 항상 json_last_error()를 확인하세요. REST API의 경우 적절한 HTTP 상태 코드(200, 201, 400, 404, 500)를 설정하고 일관된 응답 구조를 사용하세요.

php
<?php
// encode PHP array/object to JSON
$data = ['name' => 'Alice', 'age' => 30, 'hobbies' => ['reading', 'coding']];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// {"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]}

// decode JSON to PHP
$obj = json_decode($json);       // stdClass object
$arr = json_decode($json, true); // associative array

// handle errors
$data = json_decode($badJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException("JSON error: " . json_last_error_msg());
}

// API response
header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode([
    'status' => 'success',
    'data' => $users,
    'meta' => ['page' => 1, 'total' => 100],
], JSON_UNESCAPED_UNICODE);

// API request handling
$input = json_decode(file_get_contents('php://input'), true);

Composer & 의존성 관리

Composer는 PHP의 패키지 매니저 — 현대 PHP에 필수적입니다. require는 프로덕션 의존성을 지정; require-dev는 개발용(테스트 등). PSR-4 오토로딩은 네임스페이스를 디렉토리에 매핑합니다. 항상 composer.json과 composer.lock을 커밋하세요(정확한 버전 잠금). 프로덕션에서는 composer install(lock에서), 최신을 위해 composer update를 사용하세요. 인기 패키지: Monolog(로깅), Guzzle(HTTP), PHPUnit(테스트), Symfony 컴포넌트, Laravel 프레임워크. vendor/ 디렉토리를 절대 커밋하지 마세요.

php
<?php
// composer.json
// {
//   "require": {
//     "monolog/monolog": "^3.0",
//     "guzzlehttp/guzzle": "^7.0"
//   },
//   "autoload": {
//     "psr-4": {"App\\": "src/"}
//   }
// }

// install: composer install
// update:  composer update
// add:    composer require monolog/monolog

// autoload (at the top of your app)
require 'vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::WARNING));
$log->warning('User not found', ['user_id' => 42]);

// environment variables (vlucas/phpdotenv)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dbHost = $_ENV['DB_HOST'];
10

세션 & 쿠키

쿠키 설정 & 읽기

쿠키는 사용자 브라우저에 소량의 데이터를 저장합니다. setcookie()는 HTML 출력 전에 호출해야 합니다(HTTP 헤더 설정). httponly 플래그는 JavaScript가 쿠키를 읽지 못하게 합니다(XSS 완화), secure는 HTTPS를 통해서만 전송을 보장합니다. 쿠키는 일치하는 도메인/경로로 모든 요청에 전송되므로 큰 데이터를 저장하지 마세요. 민감한 데이터의 경우 세션을 대신 사용하세요(데이터는 서버에 있음). 쿠키 값은 클라이언트에서 오며 변조될 수 있으므로 항상 검증하고 살균하세요.

php
// Set a cookie (must be before any output!)
setcookie("user", "Alice", time() + 3600, "/", "", true, true);
// params: name, value, expiry, path, domain, secure, httponly

// Read cookies (from $_COOKIE superglobal)
if (isset($_COOKIE['user'])) {
    echo "Welcome back, " . htmlspecialchars($_COOKIE['user']);
}

// Delete a cookie: set expiry in the past
setcookie("user", "", time() - 3600, "/");

// Cookie limitations:
// - Sent with every HTTP request (adds bandwidth)
// - Max 4KB per cookie, ~50 per domain
// - Stored client-side (don't store sensitive data!)
// - httponly=true prevents JavaScript access (XSS protection)
// - secure=true sends only over HTTPS

세션 관리

세션은 쿠키에 저장된 세션 ID로 식별하여 서버에 데이터를 저장합니다. 쿠키와 달리 세션 데이터는 사용자에게 보이지 않습니다(민감한 데이터에 더 안전). session_start()는 출력 전에 세션을 사용하는 모든 페이지에서 호출해야 합니다. session_regenerate_id(true)는 새 ID를 생성하고 이전 ID를 삭제하여 세션 고정 공격을 방지 — 로그인 후 호출하세요. 로그아웃 시 항상 세션을 파ꇴ하세요. 대규모 앱의 경우 여러 서버 간 공유를 위해 파일(기본) 대신 Redis/데이터베이스에 세션을 저장하세요.

php
// Start or resume a session (must be before output)
session_start();

// Store data in the session (lives on server, identified by session ID cookie)
$_SESSION['user_id'] = 42;
$_SESSION['username'] = 'alice';
$_SESSION['cart'] = ['item1', 'item2'];

// Read session data
if (isset($_SESSION['user_id'])) {
    echo "User: " . $_SESSION['username'];
}

// Remove a single session variable
unset($_SESSION['cart']);

// Destroy the entire session
session_unset();   // clear all variables
session_destroy(); // destroy session data on server
setcookie(session_name(), '', time() - 3600, '/'); // clear session cookie

// Regenerate ID to prevent session fixation attacks
session_regenerate_id(true);

세션 설정 (php.ini)

세션 보안 설정이 중요합니다. cookie_httponly는 XSS가 세션 ID를 탈취하는 것을 방지합니다. cookie_secure는 HTTPS에서만 세션이 작동하도록 보장합니다. SameSite=Strict은 CSRF를 방지합니다(쿠키가 크로스 사이트 요청에 전송되지 않음). use_strict_mode는 초기화되지 않은 세션 ID를 거부합니다. gc_maxlifetime은 비활성 타임아웃을 설정합니다. 다중 서버 배포의 경우 데이터베이스나 Redis에 세션을 저장하기 위해 사용자 정의 SessionHandlerInterface를 구현 — 기본 파일 기반 저장은 서버 간에 작동하지 않습니다. 프로덕션에서는 항상 이 설정을 구성하세요.

php
// php.ini session settings (or ini_set at runtime)
ini_set('session.cookie_lifetime', 0);     // 0 = until browser closes
ini_set('session.cookie_httponly', 1);     // prevent JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Strict'); // CSRF protection
ini_set('session.use_strict_mode', 1);     // reject uninitialized IDs
ini_set('session.gc_maxlifetime', 1800);   // 30 min inactivity timeout

// Custom session handler (store in database/Redis)
class MySessionHandler implements SessionHandlerInterface {
    public function open($savePath, $sessionName) { /* connect DB */ }
    public function close() { /* close DB */ }
    public function read($id) { /* fetch from DB */ }
    public function write($id, $data) { /* save to DB */ }
    public function destroy($id) { /* delete from DB */ }
    public function gc($maxlifetime) { /* cleanup old sessions */ }
}
session_set_save_handler(new MySessionHandler(), true);
session_start();

플래시 메시지 (일회성 알림)

플래시 메시지는 한 번 표시되는 세션 기반 알림입니다(예: '성공적으로 저장됨!'). 패턴: POST 요청에서 $_SESSION에 메시지를 저장, 다음 GET 요청에서 읽고 unset. 이는 Post/Redirect/Get(PRG) 패턴을 구현 — 폼 제출 후, 새로고침 시 재제출을 방지하기 위해 리다이렉트, 리다이렉트된 페이지에서 플래시 메시지 표시. Laravel($request->session()->flash())과 Symfony 같은 프레임워크는 내장 플래시 메시지 지원을 제공합니다.

php
// Flash messages: shown once, then deleted
session_start();

// Set a flash message (e.g., after form submission)
$_SESSION['flash'] = [
    'type' => 'success',
    'message' => 'Item added to cart!'
];

// On the next page, display and clear:
if (isset($_SESSION['flash'])) {
    $flash = $_SESSION['flash'];
    unset($_SESSION['flash']);  // delete so it shows only once
    echo "<div class='alert alert-{$flash['type']}'>{$flash['message']}</div>";
}

// Helper function pattern
function flash(string $key, string $value = null): ?string {
    if ($value !== null) {
        $_SESSION['flash_' . $key] = $value;
        return null;
    }
    $val = $_SESSION['flash_' . $key] ?? null;
    unset($_SESSION['flash_' . $key]);
    return $val;
}

세션 보안 모범 사례

세션 보안은 여러 계층이 필요합니다. 고정 공격(공격자가 알려진 세션 ID를 설정)을 방지하기 위해 로그인 후 세션 ID를 재생성하세요. 유휴 타임아웃을 구현하기 위해 last_activity를 추적하세요. 선택적으로 하이재킹 감지를 위해 세션을 IP/사용자 에이전트에 바인딩(주의: IP가 변경되는 모바일 네트워크에서 오탐지 가능). 세션에 비밀번호나 신용카드 번호를 절대 저장하지 마세요 — 사용자 ID만 저장하고 필요할 때 데이터베이스에서 민감한 데이터를 가져오세요. 세션 ID 가로채기를 방지하기 위해 프로덕션에서는 항상 HTTPS를 사용하세요.

php
// 1. Always start session before output
session_start();

// 2. Regenerate ID after login (prevent fixation)
if ($loginSuccessful) {
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user->id;
}

// 3. Validate session on each request
if (isset($_SESSION['user_id'])) {
    // Check session hasn't expired
    if (isset($_SESSION['last_activity']) &&
        time() - $_SESSION['last_activity'] > 1800) {
        session_unset();
        session_destroy();
        header('Location: /login');
        exit;
    }
    $_SESSION['last_activity'] = time();

    // Optional: verify IP/user-agent hasn't changed (anti-hijacking)
    if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
        session_destroy();
        exit('Session hijacking detected');
    }
}

// 4. Use prepared statements for session storage in DB
// 5. Set secure cookie flags (see previous example)
// 6. Never store sensitive data (passwords, CC numbers) in sessions
11

REST API 개발

JSON 요청 & 응답 처리

REST API는 JSON을 교환합니다. 폼 제출($_POST 채움)과 달리 JSON 요청은 php://input에서 읽고 json_decode로 디코딩해야 합니다. 응답에는 항상 Content-Type: application/json을 설정하고 적절한 HTTP 상태 코드를 위해 http_response_code()를 사용하세요. 모든 입력을 검증 — json_decode는 예상 구조를 보장하지 않습니다. 안전한 접근을 위해 널 병합 연산자(??)를 사용하세요. JSON_PRETTY_PRINT는 디버깅에 유용하지만 더 작은 페이로드를 위해 프로덕션에서는 생략하세요.

php
// Get JSON from request body (not $_POST for JSON!)
$json = file_get_contents('php://input');
$data = json_decode($json, true);  // true = associative array

if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON']);
    exit;
}

// Process the data
$name = $data['name'] ?? '';
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);

// Return JSON response
header('Content-Type: application/json');
http_response_code(200);  // 200 OK, 201 Created, 400 Bad Request, etc.
echo json_encode([
    'success' => true,
    'data' => ['id' => 1, 'name' => $name],
    'message' => 'User created'
], JSON_PRETTY_PRINT);

라우팅 & HTTP 메서드

REST API는 HTTP 메서드를 CRUD 작업에 매핑: GET(읽기), POST(생성), PUT/PATCH(수정), DELETE(삭제). 라우팅은 메서드 + URL 경로를 핸들러에 매칭합니다. 매개변수화된 라우트(예: /api/users/42)에는 preg_match를 사용하세요. 프로덕션에서는 더 깔끔한 라우팅, 미들웨어, 의존성 주입을 위해 라우터 라이브러리(FastRoute, Symfony Routing)나 프레임워크(Laravel, Slim)를 사용하세요. 항상 적절한 HTTP 상태 코드 반환: 200(OK), 201(Created), 204(No Content), 400(Bad Request), 404(Not Found), 500(Server Error).

php
// Simple REST router based on method + path
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = trim($path, '/');

// Route table: method => [pattern => handler]
switch (true) {
    case ($method === 'GET' && $path === 'api/users'):
        echo json_encode(getAllUsers());
        break;
    case ($method === 'GET' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        echo json_encode(getUser((int)$m[1]));
        break;
    case ($method === 'POST' && $path === 'api/users'):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(createUser($data));
        http_response_code(201);
        break;
    case ($method === 'PUT' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(updateUser((int)$m[1], $data));
        break;
    case ($method === 'DELETE' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        deleteUser((int)$m[1]);
        http_response_code(204);  // No Content
        break;
    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not found']);
}

API 인증 (JWT)

JWT는 상태 없는 인증을 가능하게 — 서버가 세션을 저장할 필요가 없습니다. 토큰은 비밀 키로 서명된 페이로드(사용자 ID, 만료)를 포함합니다. 클라이언트는 Authorization 헤더(Bearer 토큰)로 토큰을 전송합니다. 서버는 서명을 확인하여 토큰이 변조되지 않았는지 보장합니다. JWT는 API와 마이크로서비스에 적합(공유 세션 저장소 불필요). 하지만 JWT는 만료 전에 취소할 수 없습니다 — 짧은 만료 시간과 리프레시 토큰 전략을 사용하세요. 프로덕션에서는 직접 암호화를 구현하지 말고 firebase/php-jwt 라이브러리를 사용하세요. JWT 페이로드에 민감한 데이터를 절대 저장하지 마세요 — base64로 인코딩된 것이지 암호화되지 않았습니다.

php
// JWT (JSON Web Token) authentication flow
// 1. Login: verify credentials, issue JWT
function login($email, $password) {
    $user = findUserByEmail($email);
    if ($user && password_verify($password, $user['password_hash'])) {
        // Create JWT: header.payload.signature
        $header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
        $payload = base64url_encode(json_encode([
            'user_id' => $user['id'],
            'exp' => time() + 3600  // expires in 1 hour
        ]));
        $signature = hash_hmac('sha256', "$header.$payload", SECRET_KEY, true);
        $jwt = "$header.$payload." . base64url_encode($signature);
        return $jwt;
    }
    return null;
}

// 2. Verify JWT on protected routes
function verifyJWT($token) {
    $parts = explode('.', $token);
    if (count($parts) !== 3) return false;
    [$header, $payload, $signature] = $parts;
    $expected = base64url_encode(
        hash_hmac('sha256', "$header.$payload", SECRET_KEY, true)
    );
    if (!hash_equals($expected, $signature)) return false;
    $data = json_decode(base64url_decode($payload), true);
    return ($data['exp'] ?? 0) > time() ? $data : false;
}

// Use firebase/php-jwt library in production!

입력 검증 & 살균

입력 검증은 API 보안에 중요합니다. PHP의 filter_var는 내장 검증기(FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_INT, FILTER_VALIDATE_URL)와 min/max 범위 같은 옵션을 제공합니다. 항상 서버 측에서 검증 — 클라이언트 측 검증은 보안이 아닌 UX용입니다. HTML 출력 시 XSS를 방지하기 위해 htmlspecialchars로 문자열을 살균하세요. JSON API의 경우 검증 에러에 대해 설명적인 메시지와 함께 422(Unprocessable Entity)를 반환하세요. 복잡한 규칙의 경우 검증 라이브러리(Respect/Validation, Symfony Validator)를 고려하세요. 사용자 입력을 절대 신뢰하지 마세요 — 타입, 길이, 형식, 비즈니스 규칙을 검증하세요.

php
// Validate and sanitize API input
function validateUserInput(array $data): array {
    $errors = [];

    // Required fields
    if (empty($data['name'])) {
        $errors[] = 'Name is required';
    }

    // Email validation
    $email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
    if (!$email) {
        $errors[] = 'Valid email is required';
    }

    // Age: integer between 18 and 120
    $age = filter_var($data['age'] ?? null, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 18, 'max_range' => 120]
    ]);
    if ($age === false) {
        $errors[] = 'Age must be between 18 and 120';
    }

    // String sanitization (remove tags, trim)
    $name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES, 'UTF-8');

    // URL validation
    $website = filter_var($data['website'] ?? '', FILTER_VALIDATE_URL);

    return ['errors' => $errors, 'data' => compact('email', 'age', 'name')];
}

$result = validateUserInput($input);
if (!empty($result['errors'])) {
    http_response_code(422);  // Unprocessable Entity
    echo json_encode(['errors' => $result['errors']]);
    exit;
}

CORS (크로스 오리진 리소스 공유)

CORS는 브라우저에서 어떤 도메인이 API에 접근할 수 있는지 제어합니다. 브라우저는 단순하지 않은 요청(PUT/DELETE, 사용자 정의 헤더)에 대해 사전 OPTIONS 요청을 보냅니다. 서버는 적절한 Access-Control-Allow-* 헤더로 응답해야 합니다. 보안을 위해 '*'보다 정확한 origin을 지정하세요(특히 자격 증명 사용 시). API가 쿠키나 Authorization 헤더를 사용하는 경우 Access-Control-Allow-Credentials: true가 필요합니다. Vary: Origin은 응답이 origin에 따라 다름을 캐시에 알립니다. 잘못 구성된 CORS는 API를 모든 웹사이트에 노출할 수 있습니다 — 항상 신뢰할 수 있는 origin을 화이트리스트에 추가하세요.

php
// Enable CORS for API requests from browsers
header('Access-Control-Allow-Origin: https://example.com');  // specific origin
// OR: header('Access-Control-Allow-Origin: *');  // any origin (less secure)
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');  // for cookies/auth
header('Access-Control-Max-Age: 86400');  // cache preflight for 24h

// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Dynamic origin (check against whitelist)
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
    header('Access-Control-Allow-Origin: ' . $origin);
    header('Vary: Origin');  // important for caching
}
12

보안 (XSS, CSRF, SQL 인젝션)

XSS 방지 (크로스 사이트 스크립팅)

XSS는 신뢰할 수 없는 데이터가 이스케이프 없이 HTML에 삽입될 때 발생하여 공격자가 피해자의 브라우저에서 JavaScript를 실행할 수 있습니다. 해결책: 항상 htmlspecialchars로 출력을 이스케이프(<, >, &, ", '를 HTML 엔티티로 변환). 다른 컨텍스트는 다른 이스케이핑이 필요: HTML 본문(htmlspecialchars), HTML 속성(htmlspecialchars와 ENT_QUOTES), JavaScript(json_encode), URL(urlencode). Content Security Policy(CSP) 헤더는 스크립트가 로드될 수 있는 곳을 제한하여 심층 방어를 추가합니다. 사용자 입력으로 eval(), innerHTML, document.write()를 절대 사용하지 마세요. Twig와 Blade 같은 프레임워크는 기본적으로 자동 이스케이프합니다.

php
// XSS: attacker injects malicious JavaScript into your page
// BAD: outputting user input without escaping
echo "<p>" . $_GET['name'] . "</p>";
// If name = <script>alert('xss')</script>, it executes!

// GOOD: escape output with htmlspecialchars
echo "<p>" . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "</p>";
// & < > " ' are converted to HTML entities

// For HTML attributes:
echo '<input value="' . htmlspecialchars($value, ENT_QUOTES) . '">';

// For JavaScript context (JSON encode):
echo '<script>var name = ' . json_encode($name) . ';</script>';

// For URL parameters:
echo '<a href="?q=' . urlencode($query) . '">Search</a>';

// Content Security Policy (CSP) header — defense in depth
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Use prepared statements (prevents SQL injection too)

CSRF 방지 (크로스 사이트 요청 위조)

CSRF는 인증된 사용자의 브라우저를 속여 사용자 모르게 사이트에 요청(예: 송금)을 보내게 합니다. 방어: 공격자가 추측할 수 없는 토큰을 폼에 포함. 토큰은 세션에 저장되고 제출 시 검증됩니다. 타이밍 안전 비교를 위해 hash_equals()를 사용하세요(타이밍 공격 방지). AJAX/API 호출의 경우 SameSite=Strict 쿠키와 사용자 정의 헤더(예: X-Requested-With) 요구가 보호를 제공합니다. GET 요청은 절대 데이터를 수정하지 않아야 합니다(이미지 태그나 링크로 트리거 가능). Laravel과 Symfony 같은 프레임워크는 내장 CSRF 미들웨어가 있습니다.

php
// CSRF: attacker tricks a logged-in user into submitting a form
// to your site (using their session cookie)

// Defense: anti-CSRF tokens
session_start();

// Generate a token (once per session)
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// Include token in forms as a hidden field
echo '<form method="POST" action="/transfer">';
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
echo '<input type="text" name="amount">';
echo '<button type="submit">Transfer</button>';
echo '</form>';

// Verify token on POST/PUT/DELETE requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'], $token)) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
    // Process the form...
}

// For APIs: use SameSite cookies + custom headers
// (browsers block cross-origin requests without explicit CORS permission)

SQL 인젝션 방지

SQL 인젝션은 #1 웹 취약점 — 공격자가 전체 데이터베이스를 읽기/수정/삭제할 수 있습니다. 보편적 해결책: 준비된 문장(매개변수화된 쿼리). 쿼리 구조와 데이터가 별도로 전송되어 사용자 입력이 SQL로 해석될 수 없습니다. 사용자 입력을 쿼리에 절대 연결하지 마세요. PDO의 prepare/execute가 자동으로 이스케이프를 처리합니다. 매개변수를 타입과 바인드(PDO::PARAM_INT, PDO::PARAM_STR). 가변 항목이 있는 IN 절의 경우 자리 표시자를 동적으로 생성하세요. 더 나은 보안을 위해 PDO::ATTR_EMULATE_PREPARES를 false로 설정하세요(실제 서버 측 준비된 문장).

php
// SQL injection: attacker manipulates queries via unescaped input
// BAD: string concatenation
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
// If id = "1 OR 1=1", returns ALL users!
// If id = "1; DROP TABLE users; --", deletes the table!

// GOOD: prepared statements with parameter binding
$pdo = new PDO('mysql:host=localhost;dbname=myapp', $user, $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // real prepared statements

$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// Multiple parameters:
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');
$stmt->execute([
    ':name' => $name,
    ':email' => $email,
    ':age' => $age
]);

// IN clause with variable arguments:
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

비밀번호 해싱 & 인증

password_hash()는 랜덤 솔트와 함께 bcrypt(또는 Argon2)를 사용 — 비밀번호 저장의 업계 표준입니다. 솔트는 해시에 포함되어 있어 별도로 관리할 필요가 없습니다. password_verify()는 입력과 저장된 해시를 안전하게 비교(타이밍 안전). password_needs_rehash()는 비용 요소를 증가시키거나 알고리즘을 전환할 때 해시를 업그레이드할 수 있게 — 현재 설정과 일치하는지 확인하고 다음 로그인 시 재해시합니다. 비밀번호에 MD5, SHA1, 일반 텍스트를 절대 사용하지 마세요 — 쉽게 크랙 가능합니다. 강력한 비밀번호 정책을 시행하지만 복잡성보다 길이를 선호하세요(NIST는 최소 8자 이상 권장).

php
// NEVER store plain-text or MD5/SHA1 passwords!
// Use password_hash() (bcrypt/argon2 by default)

// Hash a password (on registration)
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD_DEFAULT = bcrypt (or argon2id in PHP 7.3+)
// Store $hash in the database

// Verify a password (on login)
if (password_verify($inputPassword, $storedHash)) {
    // Password is correct
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user['id'];
} else {
    echo "Invalid credentials";
}

// Rehash if algorithm was upgraded (migration)
if (password_verify($input, $hash) &&
    password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // Update database with $newHash
}

// Password requirements validation
if (strlen($password) < 8 ||
    !preg_match('/[A-Z]/', $password) ||
    !preg_match('/[a-z]/', $password) ||
    !preg_match('/[0-9]/', $password)) {
    echo "Password must be 8+ chars with upper, lower, and number";
}

파일 업로드 보안

파일 업로드는 주요 공격 벡터입니다. $_FILES['type']을 절대 신뢰하지 마세요(브라우저가 설정, 쉽게 위조) — finfo로 실제 MIME 타입을 감지하세요. 사용자 제공 파일명을 절대 사용하지 마세요(경로 탐색 포함 가능 ../../script.php) — 랜덤 이름을 생성하세요. 웹 루트 외부나 PHP 실행이 비활성화된 디렉토리에 업로드를 저장하세요. 이미지의 경우 재인코딩(imagecreatefromjpeg + imagejpeg)하여 EXIF 데이터에 숨겨진 PHP 코드를 제거하세요. 서비스 거부를 방지하기 위해 파일 크기를 제한하세요. 확장자, MIME 타입, 매직 바이트를 검증하세요. 추가 보안을 위해 안티바이러스(ClamAV)로 업로드를 스캔하는 것을 고려하세요.

php
// Secure file upload handling
$upload = $_FILES['avatar'];

// 1. Check for upload errors
if ($upload['error'] !== UPLOAD_ERR_OK) {
    die('Upload failed: error ' . $upload['error']);
}

// 2. Validate file size
$maxSize = 2 * 1024 * 1024;  // 2MB
if ($upload['size'] > $maxSize) {
    die('File too large (max 2MB)');
}

// 3. Validate MIME type (don't trust $_FILES['type']!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($upload['tmp_name']);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowedTypes)) {
    die('Invalid file type');
}

// 4. Generate a safe filename (never use user-supplied name)
$safeName = bin2hex(random_bytes(16)) . '.jpg';

// 5. Store OUTSIDE the web root (or in a non-executable dir)
$dest = __DIR__ . '/uploads/' . $safeName;
if (!move_uploaded_file($upload['tmp_name'], $dest)) {
    die('Failed to save file');
}

// 6. For images: re-encode to strip malicious metadata
$img = imagecreatefromjpeg($dest);
imagejpeg($img, $dest, 90);  // re-encode strips embedded PHP/scripts
13

cURL & HTTP 요청

기본 cURL GET & POST

cURL은 PHP의 가장 강력한 HTTP 클라이언트로 GET, POST, 사용자 정의 메서드, 헤더, 쿠키, SSL을 지원합니다. 항상 CURLOPT_RETURNTRANSFER을 설정하여 응답을 문자열로 받으세요(그렇지 않으면 직접 출력). CURLOPT_TIMEOUT은 느린 서버에서 멈춤을 방지합니다. JSON이 있는 POST의 경우 Content-Type과 Content-Length 헤더를 명시적으로 설정하세요. 연결 에러는 curl_errno()로, HTTP 상태는 curl_getinfo(CURLINFO_HTTP_CODE)로 확인하세요. 리소스 해제를 위해 항상 curl_close()로 cURL 핸들을 닫으세요. 더 간단한 코드를 위해 Guzzle(더 깔끔한 API의 cURL 래퍼)을 고려하세요.

php
// GET request
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
    echo 'Error: ' . curl_error($ch);
}
curl_close($ch);
$data = json_decode($response, true);

// POST request with JSON body
$ch = curl_init('https://api.example.com/users');
$payload = json_encode(['name' => 'Alice', 'email' => '[email protected]']);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
        'Content-Length: ' . strlen($payload)
    ],
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);

인증 & 쿠키가 있는 cURL

cURL은 여러 인증 방법을 지원합니다. CURLOPT_USERPWD는 HTTP Basic Auth를 설정합니다. Bearer 토큰은 Authorization 헤더에 들어갑니다. 쿠키 기반 세션(웹사이트 로그인 같은)의 경우 CURLOPT_COOKIEJAR로 쿠키를 저장하고 CURLOPT_COOKIEFILE로 후속 요청에 전송 — 여러 cURL 호출 간 세션을 유지합니다. 쿠키용 임시 파일을 사용하고 unlink()로 정리하세요. API 호출의 경우 쿠키보다 토큰 기반 인증(Bearer)을 선호하세요. 항상 HTTPS를 사용하세요(cURL은 기본적으로 SSL을 확인 — 프로덕션에서 CURLOPT_SSL_VERIFYPEER를 비활성화하지 마세요).

php
// Basic Auth
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

// Bearer token (API key)
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Accept: application/json'
]);

// Cookie-based session (login then reuse cookies)
$cookieFile = tempnam(sys_get_temp_dir(), 'cookie');

// Step 1: Login (saves cookies)
$ch = curl_init('https://example.com/login');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query(['user' => 'alice', 'pass' => 'secret']),
    CURLOPT_COOKIEJAR => $cookieFile,   // save cookies here
    CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);

// Step 2: Access protected page (sends saved cookies)
$ch = curl_init('https://example.com/dashboard');
curl_setopt_array($ch, [
    CURLOPT_COOKIEFILE => $cookieFile,  // send cookies from here
    CURLOPT_RETURNTRANSFER => true,
]);
$dashboard = curl_exec($ch);
curl_close($ch);
unlink($cookieFile);  // cleanup

파일 다운로드 & 스트리밍

큰 파일 다운로드의 경우 CURLOPT_FILE를 사용하여 파일 핸들에 직접 쓰기 — 전체 응답을 메모리에 로드하지 않습니다. CURLOPT_FOLLOWLOCATION은 HTTP 리다이렉트(301, 302)를 따릅니다. 스트리밍(예: 실시간 데이터)의 경우 CURLOPT_WRITEFUNCTION을 사용하여 청크가 도착하는 대로 처리 — 데이터를 스트리밍하는 API에 유용합니다. CURLOPT_PROGRESSFUNCTION은 다운로드/업로드 진행률을 모니터링합니다. 큰 파일에는 넉넉한 CURLOPT_TIMEOUT을 설정하세요. 매우 큰 업로드의 경우 메모리에 로드하는 대신 CURLOPT_INFILE로 파일에서 스트리밍하세요. 리소스 누수를 방지하기 위해 항상 파일 핸들과 cURL 핸들을 닫으세요.

php
// Download a file to disk
$ch = curl_init('https://example.com/large-file.zip');
$fp = fopen('downloaded.zip', 'w');
curl_setopt_array($ch, [
    CURLOPT_FILE => $fp,           // write directly to file
    CURLOPT_FOLLOWLOCATION => true, // follow redirects
    CURLOPT_TIMEOUT => 300,        // 5 min for large files
]);
curl_exec($ch);
fclose($fp);
curl_close($ch);

// Stream response with callback (progress monitoring)
$ch = curl_init('https://example.com/stream');
curl_setopt_array($ch, [
    CURLOPT_WRITEFUNCTION => function($ch, $chunk) {
        echo $chunk;  // stream to output
        ob_flush();
        flush();
        return strlen($chunk);
    },
    CURLOPT_PROGRESSFUNCTION => function($ch, $dlTotal, $dlNow, $ulTotal, $ulNow) {
        if ($dlTotal > 0) {
            printf("
Progress: %.1f%%", ($dlNow / $dlTotal) * 100);
        }
        return 0;
    },
]);
curl_exec($ch);
curl_close($ch);

동시 요청 (Multi cURL)

curl_multi_exec는 여러 HTTP 요청을 병렬로 실행 — 여러 엔드포인트에서 데이터가 필요할 때 순차 요청보다 훨씬 빠릅니다. 패턴: multi 핸들 생성, 개별 cURL 핸들 추가, 루프에서 multi 핸들 실행(효율성을 위해 curl_multi_exec + curl_multi_select), 결과 수집. 이는 여러 API에서 데이터 집계, 리소스 프리페치, 배치 작업에 유용합니다. 더 고급 동시성의 경우 ReactPHP나 Amp(비동기 PHP 프레임워크)를 고려하세요. multi-cURL은 여전히 PHP 프로세스를 차단합니다 — 진정한 비동기를 위해서는 이벤트 루프나 메시지 큐를 사용하세요.

php
// Fetch multiple URLs in parallel (much faster than sequential)
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments'
];

$multi = curl_multi_init();
$handles = [];

// Create individual handles
foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_multi_add_handle($multi, $handles[$i]);
}

// Execute all requests concurrently
$active = null;
do {
    $status = curl_multi_exec($multi, $active);
    if ($active) {
        curl_multi_select($multi);  // wait for activity
    }
} while ($active && $status === CURLM_OK);

// Collect results
$responses = [];
foreach ($handles as $i => $ch) {
    $responses[$i] = json_decode(curl_multi_getcontent($ch), true);
    curl_multi_remove_handle($multi, $ch);
    curl_close($ch);
}
curl_multi_close($multi);

// $responses[0], [1], [2] now contain all three API responses

Guzzle 사용 (현대 HTTP 클라이언트)

Guzzle은 현대 PHP의 표준 HTTP 클라이언트 — 원시 cURL보다 훨씬 깔끔합니다. 유창한 API, PSR-7 준수 요청/응답 객체, 미들웨어(로깅, 재시도), Promise를 통한 비동기 요청을 제공합니다. 'json' 옵션은 본문을 자동 인코딩하고 Content-Type을 설정합니다. getAsync/postAsync는 multi-cURL 복잡성 없이 동시 요청을 위한 Promise를 반환합니다. 예외 처리가 내장: RequestException은 HTTP 에러(4xx, 5xx)를 catch합니다. Guzzle은 대부분의 프레임워크에서 사용됩니다(Laravel의 HTTP 클라이언트는 Guzzle 래핑). Composer로 설치: composer require guzzlehttp/guzzle.

php
// Guzzle is the most popular PHP HTTP client (composer require guzzlehttp/guzzle)
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client([
    'base_uri' => 'https://api.example.com',
    'timeout'  => 30,
    'headers'  => ['Accept' => 'application/json'],
]);

// GET request
$response = $client->get('/users/42');
$body = json_decode($response->getBody(), true);
echo $response->getStatusCode();  // 200

// POST with JSON
$response = $client->post('/users', [
    'json' => ['name' => 'Alice', 'email' => '[email protected]'],
    'headers' => ['Authorization' => 'Bearer ' . $token],
]);

// Concurrent requests (Promise-based)
$promises = [
    'users' => $client->getAsync('/users'),
    'posts' => $client->getAsync('/posts'),
];
$results = GuzzleHttp\Promise\Utils::settle($promises)->wait();

// Error handling with try/catch
try {
    $response = $client->get('/nonexistent');
} catch (RequestException $e) {
    echo $e->getResponse()->getStatusCode();  // 404
}
14

DateTime 심층 가이드

DateTime 생성 & 조작

DateTime은 PHP의 강력한 날짜/시간 클래스입니다. DateTimeImmutable이 DateTime보다 권장 — 수정 시 새 객체를 반환하여 우발적 변형 버그를 방지(같은 날짜가 여러 곳에서 사용될 때 중요). createFromFormat은 사용자 정의 형식을 파싱합니다. modify()는 '+1 week'나 'last day of next month' 같은 상대 표현식을 허용합니다. 서버 설정 의존적 동작을 피하기 위해 항상 시간대를 명시적으로 지정하세요. 날짜 계산(간격 추가)의 경우 add()/sub()와 함께 DateInterval('P1D' = 1일, 'P2W' = 2주, 'PT2H' = 2시간)을 사용하세요.

php
$now = new DateTime();  // current time
$now->setTimezone(new DateTimeZone('Asia/Shanghai'));

// From string (many formats supported)
$date = new DateTime('2024-06-15 14:30:00');
$date = new DateTime('first day of next month');
$date = new DateTime('next Friday');

// From format (precise parsing)
$date = DateTime::createFromFormat('d/m/Y', '15/06/2024');

// Modify dates with relative expressions
$date->modify('+1 week');
$date->modify('-2 days');
$date->modify('last day of this month');
$date->modify('+1 year 3 months');

// Immutable version (recommended — doesn't modify original)
$dt = new DateTimeImmutable('2024-06-15');
$next = $dt->modify('+1 day');  // $dt unchanged, $next is new object
echo $dt->format('Y-m-d');   // 2024-06-15 (unchanged!)
echo $next->format('Y-m-d'); // 2024-06-16

형식화 & 시간대

format()은 패턴 문자로 출력을 사용자 정의 — Y(4자리 연도), m(2자리 월), d(2자리 일), H(24시간), i(분), s(초). ISO 8601(API에서 사용)의 경우 'Y-m-d\TH:i:sP' 또는 'c' 약어를 사용하세요. 시간대 변환: 소스 시간대로 생성, setTimezone으로 변환. 항상 데이터베이스에 UTC로 날짜를 저장하고 표시만을 위해 사용자 시간대로 변환하세요. PHP의 시간대 데이터베이스는 포괄적(DST 규칙 포함). DateTimeZone::listIdentifiers()로 모든 지원 시간대를 가져오세요. format()의 'T' 이스케이프 문자는 리터럴 'T'를 출력(ISO 8601용).

php
// Format codes (most common):
// Y=2024  y=24  m=06  n=6  d=15  j=15
// H=14 (24h)  h=02 (12h)  i=30  s=00  A=PM  a=pm
// D=Mon  l=Monday  M=Jun  F=June  N=1 (Mon=1..Sun=7)

$date = new DateTime('2024-06-15 14:30:00');
echo $date->format('Y-m-d H:i:s');  // 2024-06-15 14:30:00
echo $date->format('l, F j, Y');    // Saturday, June 15, 2024
echo $date->format('Y-m-d\TH:i:sP'); // 2024-06-15T14:30:00+08:00 (ISO 8601)

// Timezone conversion
$utc = new DateTime('2024-06-15 14:00:00', new DateTimeZone('UTC'));
$utc->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $utc->format('H:i');  // 22:00 (UTC+8)

// List supported timezones
$tzs = DateTimeZone::listIdentifiers();
// ['UTC', 'America/New_York', 'Asia/Shanghai', ...]

// Get timezone offset
$tz = new DateTimeZone('America/Los_Angeles');
$offset = $tz->getOffset(new DateTime());  // seconds from UTC

날짜 간격 & 차이

DateInterval은 ISO 8601 기간 형식(P1Y2M3DT4H5M6S)으로 시간 지속을 나타냅니다. add()와 sub()는 날짜에 간격을 적용합니다. diff()는 두 날짜 간의 차이를 나타내는 DateInterval을 반환 — 'days' 속성은 총 일수, 'y', 'm', 'd'는 구성 요소 분해를 제공합니다. 'invert' 속성은 방향을 나타냅니다(두 번째 날짜가 더 이르면 1). 월 산술에 주의: 1월 31일에 'P1M'을 추가하면 2월 31일이 아닌 3월 2일이 됩니다(2월은 28-29일). 영업일 계산의 경우 수동으로 반복하여 주말/휴일을 건너뛰거나 nesbot/carbon 같은 라이브러리를 사용하세요.

php
// DateInterval: represents a duration
$interval = new DateInterval('P1Y2M3DT4H5M6S');
// P = period, 1Y = 1 year, 2M = 2 months, 3D = 3 days
// T = time separator, 4H = 4 hours, 5M = 5 minutes, 6S = 6 seconds

$date = new DateTime('2024-06-15');
$date->add(new DateInterval('P1M'));  // +1 month → 2024-07-15
$date->sub(new DateInterval('P10D')); // -10 days → 2024-07-05

// Difference between two dates
$d1 = new DateTime('2024-01-01');
$d2 = new DateTime('2024-12-31');
$diff = $d1->diff($d2);
echo $diff->days;     // 365 (total days)
echo $diff->m;        // 0 (months component)
echo $diff->y;        // 0 (years component)
echo $diff->format('%a days');  // 365 days
echo $diff->format('%y years, %m months, %d days');  // 0 years, 11 months, 30 days

// Check if inverted (d2 < d1)
echo $diff->invert;   // 0 if d2 >= d1, 1 if d2 < d1

DatePeriod (날짜 범위 반복)

DatePeriod는 지정된 간격으로 날짜 범위를 반복 — 달력, 보고서, 반복 이벤트 생성에 완벽합니다. 생성자는 (start, interval, end) 또는 (start, interval, recurrences)를 받습니다. 종료일은 배타적입니다. 일반적인 사용 사례: 달력 보기의 한 달의 모든 날짜 생성, 급여 기간 나열, 반복 이벤트 일정 생성. iterator_to_array()로 기간을 배열로 구체화하세요. 복잡한 반복 규칙(예: '매월 둘째 화요일')의 경우 rrule(RFC 5545 반복 규칙) 같은 전용 라이브러리를 고려하세요.

php
// DatePeriod iterates over a range of dates
$start = new DateTime('2024-06-01');
$end = new DateTime('2024-06-10');
$interval = new DateInterval('P1D');  // 1 day

$period = new DatePeriod($start, $interval, $end);
foreach ($period as $day) {
    echo $day->format('Y-m-d (D)') . "\n";
}
// 2024-06-01 (Sat)
// 2024-06-02 (Sun)
// ... through 2024-06-09 (end is exclusive)

// Generate next 12 months
$start = new DateTime('first day of this month');
$interval = new DateInterval('P1M');
$period = new DatePeriod($start, $interval, 12);  // 12 recurrences
foreach ($period as $month) {
    echo $month->format('F Y') . "\n";
}

// Every Monday for a year
$start = new DateTime('next Monday');
$end = new DateTime('+1 year');
$period = new DatePeriod($start, new DateInterval('P1W'), $end);
$mondays = iterator_to_array($period);

Carbon 라이브러리 (향상된 DateTime)

Carbon은 DateTime을 유창하고 표현력 있는 API로 확장 — PHP 생태계의 사실상 표준입니다(Laravel에서 사용). diffForHumans()는 '5 days ago', '3 hours from now'를 생성 — UI 타임스탬프에 완벽합니다. 유창한 API는 메서드를 체인합니다(addYear()->subMonth()->endOfMonth()). 비교 메서드(isWeekend, isPast, isToday)는 일반적인 확인을 단순화합니다. 현지화는 사람이 읽을 수 있는 출력을 위해 50개 이상의 언어를 지원합니다. Carbon 3(2024+)은 기본적으로 불변입니다. Composer로 설치: composer require nesbot/carbon. Laravel을 사용하는 경우 Carbon이 이미 포함되어 있습니다.

php
// Carbon: the most popular DateTime library (composer require nesbot/carbon)
use Carbon\Carbon;

$now = Carbon::now('Asia/Shanghai');
$tomorrow = Carbon::tomorrow();
$lastWeek = Carbon::now()->subWeek();

// Human-readable differences
echo Carbon::now()->diffForHumans(Carbon::now()->subDays(5));
// "5 days ago"
echo Carbon::now()->addHours(3)->diffForHumans();
// "3 hours from now"

// Fluent API
$date = Carbon::create(2024, 6, 15, 14, 30, 0)
    ->addYear()
    ->subMonth()
    ->endOfMonth()
    ->setTimezone('UTC');

// Comparison methods
if ($date->isWeekend()) { echo "Weekend!"; }
if ($date->isPast()) { echo "Past"; }
if ($date->isFuture()) { echo "Future"; }
if ($date->isToday()) { echo "Today"; }
if ($date->isLeapYear()) { echo "Leap year"; }

// Localization
Carbon::setLocale('zh');
echo Carbon::now()->subDay()->diffForHumans();  // "1天前"
15

네임스페이스 & 오토로딩

네임스페이스 기본

네임스페이스는 코드를 계층적 패키지로 구성하여 라이브러리 간의 클래스 이름 충돌을 방지합니다. 네임스페이스 선언은 첫 번째 명령문이어야 합니다(declare() 이후). 'use' 명령문은 다른 네임스페이스에서 클래스를 임포트 — use 명령문은 파일 상단에 배치하세요. 별칭(as)은 두 클래스가 같은 이름을 가질 때 충돌을 해결합니다. 선행 백슬래시(\DateTime)는 전역 네임스페이스를 참조합니다. PHP 네임스페이스는 백슬래시(\)를 구분자로 사용하여 PSR-4 오토로딩에서 디렉토리 구조에 매핑됩니다. 그룹 use 명령문(use App\Models\{User, Post})은 보일러플레이트를 줄입니다.

php
<?php
// Namespaces prevent name collisions (like packages in Java)
namespace App\Services;

class UserService {
    public function find($id) { /* ... */ }
}

// Using namespaced classes
use App\Services\UserService;
use App\Models\User;

$service = new UserService();
$user = new User();

// Aliasing (resolve conflicts or shorten names)
use App\Services\UserService as USvc;
use App\Models\User as UserModel;

// Global namespace (backslash prefix)
$now = new \DateTime();  // \ means root/global namespace
$array = new \ArrayObject();

// Multiple use statements grouped
use App\Models\{User, Post, Comment};
use App\Services\{UserService, PostService};

PSR-4 오토로딩 표준

PSR-4는 표준 오토로딩 사양 — 네임스페이스를 디렉토리 경로에 매핑하여 수동 require/include 명령문이 필요 없습니다. 규칙: App\Services\UserService는 src/Services/UserService.php에 매핑(App\ → src/). composer.json의 autoload 섹션에서 매핑을 구성하세요. 새 클래스 추가 후 'composer dump-autoload'를 실행하여 클래스 맵을 재생성하세요. vendor/autoload.php 파일(Composer가 생성)이 로딩을 처리 — 진입점(index.php)에 한 번 포함하세요. PSR-4는 클래스 이름이 파일 이름과 일치하도록 강제합니다(UserService → UserService.php).

php
// PSR-4: namespace structure maps to file paths
// App\Services\UserService → src/Services/UserService.php

// composer.json PSR-4 configuration:
// {
//   "autoload": {
//     "psr-4": {
//       "App\\": "src/"
//     }
//   }
// }

// File: src/Services/UserService.php
namespace App\Services;

class UserService {
    public function getUser($id) {
        return "User $id";
    }
}

// File: src/Models/User.php
namespace App\Models;

class User {
    public $name;
    public function __construct($name) {
        $this->name = $name;
    }
}

// After adding classes, regenerate autoloader:
// $ composer dump-autoload

// The autoloader is included once in your entry point:
require __DIR__ . '/vendor/autoload.php';

$service = new App\Services\UserService();
$user = new App\Models\User('Alice');

Composer 없는 오토로딩 (spl_autoload)

spl_autoload_register는 클래스가 아직 로드되지 않았을 때 호출되는 함수를 등록 — 완전히 자격이 갖춘 클래스 이름을 받고 해당 파일을 require해야 합니다. 여러 오토로더를 등록할 수 있습니다(순서대로 호출). 이것이 Composer가 내부적으로 사용하는 방식입니다. 프로덕션의 경우 항상 Composer의 PSR-4 오토로더를 사용 — 최적화되어 있고 엣지 케이스를 처리하며 더 빠른 조회를 위해 클래스 맵을 생성합니다. spl_autoload_register를 직접 사용하는 것은 아주 작은 프로젝트나 Composer를 사용할 수 없을 때만 사용하세요. class_exists()의 'true' 매개변수는 클래스가 로드되지 않은 경우 오토로딩을 트리거합니다.

php
<?php
// Simple autoloader for small projects (no Composer needed)
spl_autoload_register(function ($className) {
    // Convert namespace separators to directory separators
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $className) . '.php';

    // App\Services\UserService → src/App/Services/UserService.php
    // Adjust prefix mapping as needed:
    $file = str_replace('App/', '', $file);  // remove 'App' prefix

    if (file_exists($file)) {
        require $file;
    }
});

// Now classes are loaded automatically on first use
$service = new App\Services\UserService();

// Multiple autoloaders (called in order until one loads the class)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/lib/' . $class . '.php';
    if (file_exists($path)) require $path;
});

// Check if a class is autoloadable
if (class_exists('App\Helper', true)) {  // true = attempt autoload
    $helper = new App\Helper();
}

네임스페이스 상수 & 함수

네임스페이스는 클래스뿐만 아니라 상수와 함수도 포함할 수 있습니다. 'use const'와 'use function'(PHP 5.6+)으로 임포트하세요. 이는 설정 상수와 유틸리티 함수에 유용합니다. 자격 없는 함수/상수 호출에는 대체 동작이 있습니다: PHP는 먼저 현재 네임스페이스에서 찾고, 전역 네임스페이스로 대체됩니다. 백슬래시 없이 strlen()을 호출할 수 있는 이유입니다 — 하지만 성능과 명확성을 위해 네임스페이스가 있는 코드에서는 전역 함수에 \를 접두어로 붙이세요. 그룹 임포트(use App\Config\{const DB_HOST, function connect})는 장황함을 줄입니다.

php
<?php
namespace App\Config;

// Constants in namespaces
const DB_HOST = 'localhost';
const DB_PORT = 3306;

// Functions in namespaces
function connect() {
    return 'Connected to ' . DB_HOST;
}

// Using namespaced constants and functions
use const App\Config\DB_HOST;
use function App\Config\connect;

echo DB_HOST;      // localhost
echo connect();    // Connected to localhost

// Or with fully-qualified names:
echo \App\Config\DB_HOST;
echo \App\Config\connect();

// Namespace-level use for multiple imports
use App\Config\{const DB_HOST, const DB_PORT, function connect};

// Fallback: unqualified function calls fall back to global
// if not found in current namespace
namespace App;
$len = strlen('hello');  // calls global strlen() (fallback)

익명 클래스 & 오토로딩

익명 클래스(PHP 7+)를 사용하면 명명된 클래스를 정의하지 않고 간단한 일회용 객체를 생성할 수 있습니다 — 인터페이스, 모의 객체, 콜백에 유용합니다. 인터페이스를 구현하고, 클래스를 확장하고, 생성자를 가질 수 있으며, 트레이트를 사용할 수 있습니다. 클래스는 런타임에 자동 생성된 이름(class@anonymous)으로 생성됩니다. 익명 클래스는 즉시 로드됩니다(오토로딩 불필요). 단순 전략 패턴, 테스트 더블/모의, 이벤트 리스너, DTO에 사용하세요. 재사용 가능한 클래스의 경우 항상 적절한 PSR-4 오토로딩으로 명명된 클래스를 정의하세요. 익명 클래스는 테스트에서 가벼운 스텁을 만드는 데 특히 편리합니다.

php
<?php
namespace App\Factory;

// Anonymous class (PHP 7+): create a one-off class inline
interface Logger {
    public function log(string $msg): void;
}

class App {
    private $logger;
    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }
}

$app = new App();
$app->setLogger(new class implements Logger {
    public function log(string $msg): void {
        echo "[LOG] $msg\n";
    }
});

// Anonymous class with constructor
$comparator = new class($ascending = true) {
    private $asc;
    public function __construct(bool $asc) { $this->asc = $asc; }
    public function compare($a, $b): int {
        return $this->asc ? $a <=> $b : $b <=> $a;
    }
};

// Get the auto-generated class name
echo get_class($comparator);  // class@anonymous...
16

OOP 심화 (Traits, Interfaces, Abstract)

추상 클래스 & 메서드

추상 클래스는 서브클래스가 확장하는 공유 구현이 있는 기반을 제공합니다. 직접 인스턴스화할 수 없습니다. 추상 메서드는 구체적인 서브클래스가 구현해야 하는 계약(서명만)을 정의 — 이는 '템플릿 메서드 패턴'입니다. 인터페이스와 달리 추상 클래스는 속성, 생성자, 구체적인 메서드를 가질 수 있습니다. 서브클래스가 중요한 구현을 공유할 때('is-a' 관계) 추상 클래스를 사용하세요. 어떤 클래스든 구현할 수 있는 계약만 필요할 때('can-do' 관계) 인터페이스를 사용하세요. 클래스는 하나의 추상 클래스만 확장할 수 있지만 여러 인터페이스를 구현할 수 있습니다.

php
<?php
// Abstract class: can't be instantiated, may have abstract methods
abstract class Animal {
    protected $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // Abstract method: must be implemented by subclasses
    abstract public function makeSound(): string;

    // Concrete method: shared implementation
    public function describe(): string {
        return $this->name . " says " . $this->makeSound();
    }
}

class Dog extends Animal {
    public function makeSound(): string {
        return "Woof!";
    }
}

class Cat extends Animal {
    public function makeSound(): string {
        return "Meow!";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof!
// new Animal("test");  // Error: cannot instantiate abstract class

인터페이스 & 다중 구현

인터페이스는 계약을 정의 — 구현 없는 메서드 서명입니다. 클래스는 여러 인터페이스를 구현할 수 있습니다(클래스의 단일 상속과 달리). 인터페이스는 다형성을 가능하게: Comparable을 구현하는 어떤 클래스든 정렬할 수 있습니다(구체적 타입과 관계없이). 클래스 계층을 가로지르는 능력(Comparable, Serializable, Iterable)을 정의하는 데 인터페이스를 사용하세요. 인터페이스로 타입 힌팅(function sort(Comparable $a))은 구체적 클래스보다 유연합니다. 인터페이스 상속(interface A extends B, C)은 계약을 결합합니다. 현대 PHP는 인터페이스 상수와 인터페이스의 static 메서드도 지원합니다.

php
<?php
// Interface: pure contract (no implementation)
interface Comparable {
    public function compareTo($other): int;
}

interface Serializable {
    public function serialize(): string;
    public function unserialize(string $data): void;
}

// A class can implement MULTIPLE interfaces
class Product implements Comparable, Serializable {
    private $price;

    public function __construct(float $price) {
        $this->price = $price;
    }

    public function compareTo($other): int {
        return $this->price <=> $other->price;  // spaceship operator
    }

    public function serialize(): string {
        return serialize($this->price);
    }

    public function unserialize(string $data): void {
        $this->price = unserialize($data);
    }
}

// Type hinting with interfaces
function sortItems(array $items): array {
    usort($items, fn($a, $b) => $a->compareTo($b));
    return $items;
}

// Interface inheritance
interface Repository extends Comparable, Serializable {
    public function find(int $id): ?object;
}

트레이트 (상속 없는 코드 재사용)

트레이트는 수평적 코드 재사용을 제공 — 상속 없이 어떤 클래스에든 '붙여넣기'할 수 있는 메서드입니다. 이는 다이아몬드 문제를 해결합니다(PHP는 단일 상속). 일반적인 트레이트 사용: 로깅, 싱글톤 패턴, 소프트 삭제, 타임스탬프. 클래스는 여러 트레이트를 사용할 수 있습니다. 트레이트에 충돌하는 메서드가 있을 때 'insteadof'로 하나를 선택하고 'as'로 다른 것에 별칭을 지정하세요. 트레이트는 추상 메서드(사용하는 클래스가 구현하도록 강제)와 static 메서드/속성을 가질 수 있습니다. 트레이트를 너무 많이 사용하지 마세요 — 코드 추적을 어렵게 만듭니다. 복잡한 동작에는 트레이트보다 합성(의존성 주입)을 선호하세요.

php
<?php
// Trait: reusable method groups (PHP's answer to multiple inheritance)
trait Logger {
    protected function log(string $msg, string $level = 'INFO'): void {
        echo "[$level] " . date('Y-m-d H:i:s') . " $msg\n";
        // In real code: write to file/database
    }
}

trait Singleton {
    private static $instance;
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

class UserService {
    use Logger, Singleton;

    public function findUser($id) {
        $this->log("Finding user $id");
        return "User $id";
    }
}

$svc = UserService::getInstance();
$svc->findUser(42);  // [INFO] 2024-06-15 14:30:00 Finding user 42

// Conflict resolution when traits have same method
trait A { public function hello() { return 'A'; } }
trait B { public function hello() { return 'B'; } }
class C {
    use A, B {
        B::hello insteadof A;  // use B's hello
        A::hello as helloFromA; // alias A's as helloFromA
    }
}

지연 정적 바인딩 (static:: vs self::)

지연 정적 바인딩(LSB)은 self:: (컴파일 타임, 항상 정의 클래스 참조)와 static:: (런타임, 호출 클래스 참조)의 차이입니다. 이는 상속에서 중요: Base에 self::$table을 사용하는 메서드가 있으면 Child에서 호출해도 항상 Base의 $table을 봅니다. static::$table을 사용하면 Child의 $table을 봅니다. LSB는 팩토리 패턴(new static()은 호출 클래스의 인스턴스 생성), ActiveRecord(각 모델은 자체 테이블), 싱글톤 패턴에 필수적입니다. 'static' 반환 타입(PHP 8+)은 메서드가 호출 클래스의 인스턴스를 반환함을 선언합니다.

php
<?php
class Base {
    protected static $table = 'base';

    public static function getTable(): string {
        // self:: refers to the class where the method is DEFINED
        return self::$table;  // always 'base'
    }

    public static function getTableStatic(): string {
        // static:: refers to the class that was CALLED (runtime)
        return static::$table;  // late static binding
    }

    public static function create(): static {
        // 'static' return type + new static() = factory pattern
        return new static();
    }
}

class Child extends Base {
    protected static $table = 'child';
}

echo Child::getTable();        // 'base' (self:: = Base)
echo Child::getTableStatic();  // 'child' (static:: = Child)
echo get_class(Child::create()); // 'Child' (new static = Child)

// self:: is resolved at compile time (the defining class)
// static:: is resolved at runtime (the calling class)
// This is "Late Static Binding" — essential for factory patterns

매직 메서드

매직 메서드는 객체 작업을 가로채는 특수 메서드입니다. __get/__set은 동적 속성을 생성(데이터 전송 객체, ORM에 유용). __toString은 echo $object를 가능하게. __invoke는 객체를 함수처럼 호출 가능하게. __isset/__unset은 동적 속성에 대해 isset()/unset()을 지원. __debugInfo는 var_dump 출력을 사용자 정의. 기타 매직 메서드: __construct, __destruct, __clone(깊은 복제용), __call/__callStatic(정의되지 않은 메서드, 유창한 API와 mixin 가능), __serialize/__unserialize(PHP 7.4+에서 __sleep/__wakeup 대체). 매직 메서드는 드물게 사용하세요 — 디버깅하기 어려운 '매직' 동작을 추가합니다. 명확히 문서화하세요.

php
<?php
class MagicBox {
    private $data = [];

    // Called when accessing undefined properties
    public function __get($name) {
        return $this->data[$name] ?? null;
    }

    // Called when setting undefined properties
    public function __set($name, $value) {
        $this->data[$name] = $value;
    }

    // Called when isset() or empty() on undefined property
    public function __isset($name): bool {
        return isset($this->data[$name]);
    }

    // Called when unset() on undefined property
    public function __unset($name): void {
        unset($this->data[$name]);
    }

    // Called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // Called when object is called as function
    public function __invoke($arg) {
        return "Called with $arg";
    }

    // Called for var_dump/debugging
    public function __debugInfo(): array {
        return ['keys' => array_keys($this->data)];
    }
}

$box = new MagicBox();
$box->name = "Alice";       // __set
echo $box->name;            // __get → Alice
echo $box;                  // __toString → {"name":"Alice"}
echo $box("test");          // __invoke → Called with test
17

Composer 패키지 관리

composer.json 기본

composer.json은 PHP 프로젝트의 매니페스트입니다. require는 버전 제약과 함께 프로덕션 의존성을 나열합니다(^는 마이너 업데이트 허용, ~는 패치 허용). autoload는 PSR-4 네임스페이스-디렉토리 매핑을 정의합니다. require-dev는 개발 전용 의존성을 담습니다. composer install로 프로젝트를 설정하세요.

php
{
    "name": "myorg/myapp",
    "type": "project",
    "require": {
        "php": ">=8.1",
        "monolog/monolog": "^3.0",
        "symfony/console": "^7.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": { "MyApp\\": "src/" }
    }
}

설치 & 업데이트

composer install은 정확한 버전을 위해 composer.lock을 읽습니다(재현 가능한 빌드). composer require는 패키지를 추가하고 의존성을 해결합니다. composer update는 제약 내에서 더 새 버전을 가져옵니다. 프로덕션에는 --no-dev 사용. --optimize-autoloader는 프로덕션에서 더 빠른 오토로딩을 위해 PSR-4를 classmap으로 변환합니다.

php
# Install all dependencies from composer.lock
composer install

# Add a package (modifies composer.json)
composer require monolog/monolog
composer require --dev phpunit/phpunit

# Update all packages to latest allowed versions
composer update

# Update a single package
composer update monolog/monolog

# Production install (no dev, optimized autoloader)
composer install --no-dev --optimize-autoloader

# Show installed packages
composer show

버전 제약

캐럿(^)은 가장 일반적인 제약: 가장 왼쪽의 0이 아닌 숫자를 수정하지 않는 변경을 허용합니다. 틸드(~)는 패치 수준으로 잠급니다. 0.x 버전의 경우 ^0.3은 0.3.x를 허용하지만 0.4는 허용하지 않습니다. 항상 제약을 사용하여 보안 패치를 받으면서 파괴적 변경을 피하세요. 재현 가능성을 위해 composer.lock에 정확한 버전을 고정하세요.

php
"require": {
    // Caret: >=1.2.0, <2.0.0 (allows minor+patch)
    "vendor/pkg": "^1.2",

    // Tilde: >=1.2.0, <1.3.0 (patch only)
    "vendor/pkg2": "~1.2",

    // Exact version
    "vendor/pkg3": "1.2.3",

    // Range
    "vendor/pkg4": ">=1.0 <2.0",

    // Wildcard
    "vendor/pkg5": "1.2.*",

    // Stability flags
    "vendor/pkg6": "dev-main",
    "vendor/pkg7": "2.0@beta"
}

PSR-4 오토로딩

PSR-4 오토로딩은 네임스페이스 접두어를 디렉토리에 매핑: MyApp\Services\UserService는 src/Services/User.php로 해석됩니다. 새 클래스 추가 후 composer dump-autoload를 실행하세요. 프로덕션의 경우 --optimize를 사용하여 classmap을 생성합니다(파일 시스템 확인 대신 하나의 배열 조회). classmap 오토로딩은 디렉토리를 스캔하며 고정된 코드베이스에 가장 빠릅니다.

php
// PSR-4: "MyApp\\": "src/" means
// MyApp\User -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php

namespace MyApp\Services;

class UserService
{
    public function find(int $id): ?User
    {
        // ...
    }
}

// After running composer dump-autoload:
// require 'vendor/autoload.php';
// $service = new \MyApp\Services\UserService();

// Classmap (faster for production)
// "autoload": { "classmap": ["src/", "lib/"] }

스크립트 & 훅

Composer 스크립트는 프로젝트별 명령을 정의합니다. composer <name>으로 실행하세요. 내장 이벤트(post-install-cmd, post-update-cmd, pre-autoload-dump)는 자동으로 발생합니다. 스크립트는 @name으로 다른 스크립트를 참조할 수 있습니다. 스크립트를 사용하여 팀 멤버 간 개발 워크플로우를 표준화하세요.

php
{
    "scripts": {
        "test": "phpunit",
        "lint": "phpcs --standard=PSR12 src/",
        "fix": "phpcbf --standard=PSR12 src/",
        "post-install-cmd": [
            "MyApp\\Setup::postInstall",
            "php artisan migrate"
        ],
        "post-update-cmd": "@post-install-cmd"
    }
}

// Run: composer test, composer lint, etc.
18

고급 cURL

다중 요청 (병렬)

curl_multi_exec는 여러 요청을 병렬로 실행하여 배치 API 호출의 총 시간을 크게 줄입니다. curl_multi_select는 활동이 있을 때까지 차단하여 바쁜 대기를 피합니다. 리소스 해제를 위해 항상 핸들과 multi 핸들을 닫으세요. 이는 고성능 HTTP 스크래핑과 API 집계의 기초입니다.

php
<?php
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments',
];

$multi = curl_multi_init();
$handles = [];

foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_setopt($handles[$i], CURLOPT_TIMEOUT, 10);
    curl_multi_add_handle($multi, $handles[$i]);
}

do {
    $status = curl_multi_exec($multi, $active);
    if ($active) curl_multi_select($multi);
} while ($active && $status === CURLM_OK);

foreach ($handles as $i => $ch) {
    $responses[$i] = curl_multi_getcontent($ch);
    curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);

스트리밍 응답

CURLOPT_WRITEFUNCTION은 응답의 각 청크에 대한 콜백을 제공하여 전체를 메모리에 로드하지 않고 큰 파일의 스트리밍 처리를 가능하게 합니다. 청크 길이를 반환하여 소비를 알립니다. 이는 큰 파일 다운로드, 스트리밍 API 처리, CSV/JSON 점진적 파싱에 필수적입니다.

php
<?php
$ch = curl_init('https://example.com/large-file.csv');
$file = fopen('download.csv', 'w');

curl_setopt($ch, CURLOPT_FILE, $file);  // Write to file
// OR use callback for streaming processing:
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) use ($file) {
    fwrite($file, $chunk);
    // Or parse incrementally:
    // $lines = explode("\n", $chunk);
    return strlen($chunk);  // Must return bytes consumed
});

curl_exec($ch);
fclose($file);
curl_close($ch);

인증 & 쿠키

CURLOPT_HTTPHEADER로 인증(Bearer 토큰, API 키)을 위한 사용자 정의 헤더를 설정하세요. COOKIEJAR/COOKIEFILE은 세션 기반 인증을 위해 요청 간에 쿠키를 유지합니다. CURLOPT_USERPWD는 HTTP Basic Auth를 설정합니다. POST의 경우 CURLOPT_POSTFIELDS와 Content-Type 헤더로 JSON을 설정하세요. 응답 형식을 제어하려면 항상 Accept를 설정하세요.

php
<?php
$ch = curl_init('https://api.example.com/data');

// Bearer token
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $token,
    'Content-Type: application/json',
]);

// Cookie jar (persist across requests)
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');   // Save
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');  // Load

// Basic auth
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');

// POST with JSON body
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['key' => 'value']));

$response = curl_exec($ch);

에러 처리 & 재시도

항상 curl_exec 반환 값을 확인(실패 시 false)하고 메시지는 curl_error로 확인하세요. curl_getinfo는 HTTP 상태 코드, 타이밍, 리다이렉트 정보를 제공합니다. 속도 제한과 일시적 실패를 처리하기 위해 재시도에 지수 백오프를 구현하세요. 네트워크 에러(curl 에러)와 HTTP 에러(상태 코드)를 구별하세요.

php
<?php
function fetchWithRetry(string $url, int $max = 3): ?string
{
    for ($attempt = 1; $attempt <= $max; $attempt++) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);

        $response = curl_exec($ch);
        $error = curl_error($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($response !== false && $code >= 200 && $code < 300) {
            return $response;
        }

        if ($attempt < $max) {
            sleep(pow(2, $attempt));  // Exponential backoff
        }
    }
    throw new RuntimeException("Failed: {$error}");
}

cURL 옵션 참조

CURLOPT_FOLLOWLOCATION은 HTTP 리다이렉트(3xx)를 따릅니다. MITM 공격을 방지하기 위해 프로덕션에서는 항상 SSL_VERIFYPEER를 true로 유지; curl.haxx.se에서 cacert.pem을 다운로드하세요. CURLOPT_ENCODING은 압축을 활성화합니다. 연결 문제 디버깅을 위해 STDERR와 함께 CURLOPT_VERBOSE를 사용하세요. 멈추지 않도록 합리적인 타임아웃을 설정하세요.

php
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  // Return not echo
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);   // Follow redirects
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);

// SSL (keep VERIFYPEER true in production!)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');

// Performance
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip');

// Debug
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, fopen('curl.log', 'w'));
19

이미지 처리 (GD)

이미지 생성 & 로드

imagecreatetruecolor는 트루 컬러 이미지(수백만 색상)를 생성합니다. imagecolorallocate는 색상을 등록하고 식별자를 반환합니다. imagecreatefromjpeg/png/webp는 기존 파일을 로드합니다. 항상 반환 값을 확인하세요(실패 시 false). imagesx/imagesy로 크기를 가져옵니다. 완료 후 imagedestroy로 메모리를 해제하세요.

php
<?php
// Create a blank image
$img = imagecreatetruecolor(400, 300);

// Allocate colors
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);

// Fill background
imagefill($img, 0, 0, $white);

// Load existing images
$photo = imagecreatefromjpeg('photo.jpg');
$png = imagecreatefrompng('logo.png');
$webp = imagecreatefromwebp('image.webp');

// Get dimensions
$width = imagesx($photo);
$height = imagesy($photo);

도형 & 텍스트 그리기

GD는 도형 그리기 기본 요소를 제공: 사각형, 타원, 선, 다각형, 호. 채워진 변형(imagefilled*)은 단색 도형을 그립니다. imagettftext는 각도와 크기 제어로 TrueType 글꼴을 렌더링합니다. 이미지 데이터 출력 전에 항상 Content-Type 헤더를 보내세요. 메모리 해제를 위해 imagedestroy를 호출하세요.

php
<?php
$img = imagecreatetruecolor(400, 300);
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);
$blue = imagecolorallocate($img, 0, 0, 255);
imagefill($img, 0, 0, $white);

// Shapes
imagerectangle($img, 50, 50, 150, 120, $red);
imagefilledrectangle($img, 200, 50, 300, 120, $blue);
imageellipse($img, 100, 200, 80, 80, $red);
imageline($img, 0, 0, 400, 300, $red);

// Text with TrueType font
imagettftext($img, 20, 0, 50, 270, $red, 'arial.ttf', 'Hello GD!');

header('Content-Type: image/png');
imagepng($img);
imagedestroy($img);

크기 조정 & 자르기

imagecopyresampled은 imagecopyresized보다 더 높은 품질의 결과를 생성합니다(보간 사용). 원본에서 크기를 계산하여 종횡비를 유지하세요. 썸네일의 경우 일관된 레이아웃을 위해 정사각형으로 중앙 자르기하세요. 배치 처리에서 메모리 누수를 방지하기 위해 복사 후 항상 소스 이미지를 파괴하세요.

php
<?php
function resizeImage($src, $maxW, $maxH) {
    list($w, $h) = getimagesize($src);
    $ratio = min($maxW / $w, $maxH / $h);
    $newW = (int)($w * $ratio);
    $newH = (int)($h * $ratio);

    $srcImg = imagecreatefromjpeg($src);
    $dstImg = imagecreatetruecolor($newW, $newH);

    // High-quality resampling
    imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0,
        $newW, $newH, $w, $h);

    imagedestroy($srcImg);
    return $dstImg;
}

// Center crop to square
function cropSquare($src, $size) {
    list($w, $h) = getimagesize($src);
    $min = min($w, $h);
    $x = (int)(($w - $min) / 2);
    $y = (int)(($h - $min) / 2);
    $dst = imagecreatetruecolor($size, $size);
    $img = imagecreatefromjpeg($src);
    imagecopyresampled($dst, $img, 0, 0, $x, $y, $size, $size, $min, $min);
    return $dst;
}

필터 & 효과

imagefilter는 내장 효과를 적용: 그레이스케일, 밝기(범위 -255 ~ 255), 대비(음수가 증가), 블러, 에지 감지, 반전, 색상화(RGB + 알파). Pixelate는 모자이크 효과를 생성합니다. 이것들은 빠르지만 기본적입니다; 고급 효과의 경우 합성 행렬과 사용자 정의 필터를 지원하는 ImageMagick(Imagick 확장)을 사용하세요.

php
<?php
$img = imagecreatefromjpeg('photo.jpg');

// Built-in filters
imagefilter($img, IMG_FILTER_GRAYSCALE);      // B&W
imagefilter($img, IMG_FILTER_BRIGHTNESS, 30);  // Brighten
imagefilter($img, IMG_FILTER_CONTRAST, -20);   // More contrast
imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR);   // Blur
imagefilter($img, IMG_FILTER_EDGEDETECT);      // Edge detection
imagefilter($img, IMG_FILTER_NEGATE);          // Invert colors

// Colorize (tint)
imagefilter($img, IMG_FILTER_COLORIZE, 0, 0, 100, 0);  // Blue tint

// Pixelate
imagefilter($img, IMG_FILTER_PIXELATE, 10, true);

imagepng($img, 'filtered.png');
imagedestroy($img);

워터마크 & 합성

imagecopymerge는 조정 가능한 불투명도(0-100)로 한 이미지를 다른 이미지 위에 겹칩니다. 알파 채널이 있는 PNG 워터마크는 자연스럽게 혼합됩니다. 텍스트 워터마크의 경우 반투명 텍스트를 위해 imagecolorallocatealpha를 사용하세요. imagejpeg 품질은 0(최악)에서 100(최고)까지; 75-90이 웹에 적절한 균형입니다. 항상 두 이미지를 모두 파괴하세요.

php
<?php
$photo = imagecreatefromjpeg('photo.jpg');
$watermark = imagecreatefrompng('logo.png');

$pw = imagesx($photo); $ph = imagesy($photo);
$ww = imagesx($watermark); $wh = imagesy($watermark);

// Position: bottom-right with 20px padding
$destX = $pw - $ww - 20;
$destY = $ph - $wh - 20;

// Merge with 50% opacity
imagecopymerge($photo, $watermark, $destX, $destY, 0, 0, $ww, $wh, 50);

// Text watermark
$color = imagecolorallocatealpha($photo, 255, 255, 255, 60);
imagettftext($photo, 30, 0, 20, $ph - 20, $color, 'arial.ttf', '© 2025');

imagejpeg($photo, 'watermarked.jpg', 90);  // 90% quality
imagedestroy($photo);
imagedestroy($watermark);
20

세션 & 쿠키 심화

세션 보안

안전한 세션에는 다음이 필요: HttpOnly 쿠키(JavaScript 접근 불가), Secure 플래그(HTTPS만), SameSite=Strict(CSRF 보호), strict 모드(초기화되지 않은 세션 ID 거부). 세션 고정을 방지하기 위해 권한 변경(로그인, 관리자 접근) 후 항상 세션 ID를 재생성하세요. PHP 노출을 피하기 위해 사용자 정의 세션 이름을 사용하세요.

php
<?php
// php.ini or runtime configuration
ini_set('session.cookie_httponly', 1);    // No JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);     // Reject uninitialized IDs
ini_set('session.gc_maxlifetime', 3600);   // 1 hour

session_name('APP_SID');  // Custom name
session_start();

// Regenerate ID after login (prevent fixation)
session_regenerate_id(true);

$_SESSION['user_id'] = 123;
$_SESSION['login_time'] = time();

사용자 정의 세션 핸들러

사용자 정의 세션 핸들러는 파일 대신 데이터베이스, Redis, Memcached에 세션 데이터를 저장합니다. open, close, read, write, destroy, gc 메서드로 SessionHandlerInterface를 구현하세요. 데이터베이스 저장은 여러 서버 간 세션 공유를 가능하게 합니다(로드 밸런싱). 세션 ID의 SQL 인젝션을 방지하기 위해 항상 매개변수화된 쿼리를 사용하세요.

php
<?php
class DbSessionHandler implements SessionHandlerInterface
{
    private PDO $pdo;
    public function __construct(PDO $pdo) { $this->pdo = $pdo; }

    public function open($path, $name): bool { return true; }
    public function close(): bool { return true; }

    public function read($id): string|false {
        $stmt = $this->pdo->prepare(
            'SELECT data FROM sessions WHERE id = ? AND expires > ?'
        );
        $stmt->execute([$id, time()]);
        return $stmt->fetchColumn() ?: '';
    }

    public function write($id, $data): bool {
        $exp = time() + (int)ini_get('session.gc_maxlifetime');
        return $this->pdo->prepare(
            'REPLACE INTO sessions (id, data, expires) VALUES (?, ?, ?)'
        )->execute([$id, $data, $exp]);
    }

    public function destroy($id): bool {
        return $this->pdo->prepare('DELETE FROM sessions WHERE id = ?')
            ->execute([$id]);
    }

    public function gc($max): int|false {
        return $this->pdo->prepare('DELETE FROM sessions WHERE expires < ?')
            ->execute([time()]);
    }
}

session_set_save_handler(new DbSessionHandler($pdo), true);
session_start();

쿠키 관리

명확성과 SameSite 설정을 위해 setcookie의 옵션 배열 형식(PHP 7.3+)을 사용하세요. 보안 쿠키는 HTTPS가 필요합니다. HttpOnly는 XSS 기반 쿠키 도용을 방지합니다. SameSite=Lax는 크로스 사이트 POST를 차단(대부분의 CSRF 보호에 충분); Strict는 모든 크로스 사이트 요청을 차단합니다. 동일한 경로/도메인으로 과거 만료를 설정하여 쿠키를 삭제하세요.

php
<?php
// Set a cookie with all security options
setcookie('preferences', json_encode(['theme' => 'dark']), [
    'expires' => time() + 86400 * 30,  // 30 days
    'path' => '/',
    'domain' => '.example.com',
    'secure' => true,                   // HTTPS only
    'httponly' => true,                 // No JavaScript access
    'samesite' => 'Lax'                 // CSRF protection
]);

// Read cookies
$theme = $_COOKIE['preferences'] ?? 'default';

// Delete a cookie (set expiration in the past)
setcookie('preferences', '', [
    'expires' => time() - 3600,
    'path' => '/',
]);

플래시 메시지

플래시 메시지는 세션에 일회성 알림을 저장하고 리다이렉트(Post/Redirect/Get 패턴) 후 표시됩니다. 메시지는 리다이렉트 전에 설정되고 표시 후 지워집니다. 이는 재제출 경고를 방지하고 UI를 깔끔하게 유지합니다. 여러 메시지를 위해 배열로 저장하세요. 오래된 표시를 방지하기 위해 읽기 후 즉시 지우세요.

php
<?php
// Set a flash message (one-time notification)
function flash(string $key, string $message): void {
    $_SESSION['_flash'][$key] = $message;
}

// Get and clear flash message
function getFlash(string $key): ?string {
    $msg = $_SESSION['_flash'][$key] ?? null;
    unset($_SESSION['_flash'][$key]);
    return $msg;
}

// Usage in controller
flash('success', 'Item saved!');
header('Location: /items');
exit;

// In view after redirect
if ($msg = getFlash('success')) {
    echo "<div class='alert'>$msg</div>";
}

JWT 인증

JWT는 상태 없는 인증을 가능하게: 서버가 세션 데이터를 저장하지 않아 API와 마이크로서비스에 이상적입니다. 토큰은 비밀로 서명된 클레임(사용자 ID, 역할, 만료)을 포함합니다. 트레이드오프: 토큰은 쉽게 취소할 수 없습니다(짧은 만료 + 리프레시 토큰 사용), 요청 크기가 증가합니다. XSS 토큰 도용을 방지하기 위해 HttpOnly 쿠키를 사용하세요.

php
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Generate token on login
$payload = [
    'user_id' => 123,
    'role' => 'admin',
    'iat' => time(),           // Issued at
    'exp' => time() + 3600,    // Expires in 1 hour
];
$token = JWT::encode($payload, $secretKey, 'HS256');

// Send to client (cookie or Authorization header)
setcookie('auth_token', $token, [
    'expires' => time() + 3600,
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Lax',
]);

// Verify on each request
try {
    $decoded = JWT::decode(
        $_COOKIE['auth_token'],
        new Key($secretKey, 'HS256')
    );
    $userId = $decoded->user_id;
} catch (Exception $e) {
    http_response_code(401);
    exit('Unauthorized');
}
21

REST API 심화

라우팅 & 요청 처리

REST API는 HTTP 메서드를 CRUD 작업에 매핑: GET(읽기), POST(생성), PUT/PATCH(수정), DELETE(삭제). 리소스 식별을 위해 URL 경로를 파싱하세요. POST/PUT의 경우 php://input에서 요청 본문을 읽으세요. 항상 적절한 HTTP 상태 코드(200, 201, 400, 404, 500)와 Content-Type 헤더가 있는 JSON 응답을 반환하세요.

php
<?php
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($path, '/'));

switch (true) {
    case $method === 'GET' && $segments[0] === 'users':
        if (isset($segments[1])) getUser((int)$segments[1]);
        else listUsers();
        break;

    case $method === 'POST' && $segments[0] === 'users':
        $data = json_decode(file_get_contents('php://input'), true);
        createUser($data);
        break;

    case $method === 'PUT' && $segments[0] === 'users' && isset($segments[1]):
        $data = json_decode(file_get_contents('php://input'), true);
        updateUser((int)$segments[1], $data);
        break;

    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not Found']);
}

응답 & 상태 코드

API 응답에는 항상 Content-Type: application/json을 설정하세요. 올바른 상태 코드 사용: 200(OK), 201(Created), 204(No Content), 400(Bad Request), 401(Unauthorized), 403(Forbidden), 404(Not Found), 422(Unprocessable Entity), 429(Too Many Requests), 500(Server Error). 디버깅을 위해 에러 세부 정보를 포함하지만 프로덕션에서는 스택 추적을 노출하지 마세요.

php
<?php
function jsonResponse($data, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    exit;
}

// Success responses
jsonResponse(['data' => $users], 200);          // OK
jsonResponse(['data' => $user], 201);           // Created

// Error responses
jsonResponse(['error' => 'Validation failed'], 400);
jsonResponse(['error' => 'Unauthorized'], 401);
jsonResponse(['error' => 'Forbidden'], 403);
jsonResponse(['error' => 'Not Found'], 404);
jsonResponse(['error' => 'Server error'], 500);

페이지네이션 & 필터링

LIMIT/OFFSET로 페이지네이션을 구현하고 메타데이터(총합, 현재 페이지, 총 페이지)를 반환하세요. SQL 인젝션을 방지하기 위해 정렬 열을 화이트리스트에 대해 검증하고 살균하세요. 과도한 쿼리를 방지하기 위해 per_page를 제한하세요. 검색에는 와일드카드와 함께 LIKE를 사용하세요. 데이터와 별도의 meta 객체에 페이지네이션 메타데이터를 반환하세요.

php
<?php
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 20)));
$offset = ($page - 1) * $perPage;

// Whitelist sort columns (prevent SQL injection)
$sort = in_array($_GET['sort'] ?? '', ['name', 'email'])
    ? $_GET['sort'] : 'id';
$order = strtolower($_GET['order'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';

$sql = "SELECT * FROM users ORDER BY $sort $order LIMIT ? OFFSET ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$perPage, $offset]);
$users = $stmt->fetchAll();

// Total count for pagination metadata
$total = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

jsonResponse([
    'data' => $users,
    'meta' => [
        'page' => $page,
        'per_page' => $perPage,
        'total' => $total,
        'total_pages' => ceil($total / $perPage),
    ],
]);

속도 제한

속도 제한은 API 남용을 방지합니다. 고정 윈도우(단순) 또는 슬라이딩 윈도우(더 정확) 알고리즘을 사용하세요. 분산 시스템의 경우 카운터를 Redis에 저장하세요. 클라이언트가 자체 규제할 수 있도록 X-RateLimit 헤더(Limit, Remaining, Reset)를 반환하세요. HTTP 429와 Retry-After는 클라이언트에게 재시도 시기를 알립니다. 프로덕션의 경우 Redis나 전용 속도 제한기를 사용하세요.

php
<?php
function checkRateLimit(int $userId, int $max = 100, int $window = 3600): bool
{
    $file = sys_get_temp_dir() . "/rate_{$userId}_" . floor(time() / $window);
    $count = file_exists($file) ? (int)file_get_contents($file) : 0;

    if ($count >= $max) {
        $reset = (floor(time() / $window) + 1) * $window;
        header("X-RateLimit-Limit: $max");
        header("X-RateLimit-Remaining: 0");
        header("Retry-After: " . ($reset - time()));
        http_response_code(429);
        echo json_encode(['error' => 'Rate limit exceeded']);
        return false;
    }

    file_put_contents($file, $count + 1);
    header("X-RateLimit-Remaining: " . ($max - $count - 1));
    return true;
}

if (!checkRateLimit($userId)) exit;

API 버전 관리

API 버전 관리 전략: URL 접두어(/v1/)가 가장 명시적이고 캐시 친화적; Accept 헤더는 RESTful이지만 테스트가 더 어렵습니다. OpenAPI(Swagger) 어노테이션으로 API를 문서화하세요. swagger-php 같은 도구로 대화형 문서를 생성하세요. 처음부터 버전을 관리하세요; 파괴적 변경은 새 버전이 필요합니다. Sunset 헤더로 이전 버전을 사용 중단하세요.

php
<?php
// Versioning via URL prefix: /v1/users, /v2/users
$version = $segments[0] ?? 'v1';

// Versioning via Accept header
// Accept: application/vnd.myapp.v2+json
preg_match('/vnd\.myapp\.(v\d+)\+json/',
    $_SERVER['HTTP_ACCEPT'] ?? '', $m);
$version = $m[1] ?? 'v1';

// OpenAPI/Swagger documentation
/**
 * @OA\Get(path="/api/users",
 *   @OA\Response(response=200, description="List users")
 * )
 */
22

보안 심화 (XSS/CSRF)

XSS 방지

XSS(크로스 사이트 스크립팅)는 웹 페이지에 악성 스크립트를 주입합니다. 컨텍스트에 따라 출력을 인코딩하여 방지: HTML은 htmlspecialchars, JavaScript는 json_encode, URL은 urlencode. ENT_QUOTES는 작은따옴표와 큰따옴표 모두 이스케이프합니다. Content-Security-Policy(CSP)는 스크립트 소스를 제한하여 심층 방어를 추가합니다. 사용자 입력을 절대 신뢰하지 마세요.

php
<?php
// Output encoding (prevents XSS)
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// JavaScript string (use json_encode)
echo '<script>var name = ' . json_encode($name) . ';</script>';

// URL parameter
echo 'redirect=' . urlencode($url);

// Content-Security-Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Disable inline scripts
header("X-XSS-Protection: 1; mode=block");

CSRF 보호

CSRF(크로스 사이트 요청 위조)는 사용자를 속여 원치 않는 작업을 제출하게 합니다. 안티 CSRF 토큰으로 방지: 세션당 랜덤 토큰 생성, 폼에 숨겨진 필드로 포함, POST/PUT/DELETE에서 검증. 타이밍 안전 비교를 위해 hash_equals를 사용하세요. AJAX의 경우 토큰을 사용자 정의 헤더로 보내세요. SameSite=Strict 쿠키는 추가 보호를 제공합니다.

php
<?php
// Generate CSRF token
function csrfToken(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// Verify CSRF token (timing-safe)
function verifyCsrf(string $token): bool {
    return !empty($_SESSION['csrf_token'])
        && hash_equals($_SESSION['csrf_token'], $token);
}

// In the form
echo '<input type="hidden" name="csrf_token" value="' . csrfToken() . '">';

// On POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
}

SQL 인젝션 방지

SQL 인젝션은 공격자가 임의의 SQL을 실행할 수 있게 합니다. 매개변수화된 쿼리와 함께 준비된 문장을 항상 사용하세요: 데이터베이스가 SQL 로직과 데이터를 분리하여 인젝션을 불가능하게 합니다. 사용자 입력을 SQL 문자열에 절대 연결하지 마세요. 동적 쿼리(IN 절, ORDER BY)의 경우 자리 표시자로 SQL 구조를 빌드하고 값을 매개변수로 전달하세요.

php
<?php
// BAD: string concatenation (SQL injection vulnerable)
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'";
// Attack: ?name=' OR '1'='1

// GOOD: prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ? AND status = ?');
$stmt->execute([$_GET['name'], 'active']);
$users = $stmt->fetchAll();

// Named parameters
$stmt = $pdo->prepare(
    'INSERT INTO users (name, email) VALUES (:name, :email)'
);
$stmt->execute([':name' => $name, ':email' => $email]);

// Dynamic IN clause (still safe)
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

비밀번호 해싱

일반 텍스트 비밀번호를 절대 저장하지 마세요. password_hash는 자동 솔트 생성과 함께 bcrypt(또는 가능한 경우 Argon2)를 사용합니다. 해시에는 알고리즘, 비용, 솔트가 포함되어 있어 password_verify가 어떤 형식이든 확인할 수 있습니다. 비용 요소를 증가시키거나 알고리즘을 전환할 때 해시를 업그레이드하려면 password_needs_rehash를 사용하세요. 새 애플리케이션에는 Argon2가 권장됩니다.

php
<?php
// Hash a password (uses bcrypt by default, auto-salts)
$hash = password_hash('myPassword123', PASSWORD_DEFAULT);
// $2y$10$... (bcrypt with cost 10)

// Verify a password
if (password_verify($inputPassword, $storedHash)) {
    // Check if rehash is needed (algorithm upgrade)
    if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
        $newHash = password_hash($inputPassword, PASSWORD_DEFAULT);
        // Update stored hash in database
    }
    // Log user in
}

// Argon2 (PHP 7.2+, requires libsodium)
$hash = password_hash('password', PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,  // 64 MB
    'time_cost'   => 4,      // iterations
    'threads'     => 2,
]);

입력 검증

항상 서버 측에서 입력을 검증하세요(클라이언트 측 검증은 UX 전용). 타입 확인에는 FILTER_VALIDATE_*와 함께 filter_input을, 정리에는 FILTER_SANITIZE_*를 사용하세요. 사용자 정의 규칙의 경우 정규식이나 전용 검증 라이브러리(Respect/Validation, Symfony Validator)를 사용하세요. 화이트리스트 접근 방식 사용: 알려진 필드만 수락하고 나머지는 거부하세요.

php
<?php
// Validate email
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
    $errors[] = 'Invalid email';
}

// Validate integer with range
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 150]
]);

// Validate URL
$url = filter_input(INPUT_POST, 'website', FILTER_VALIDATE_URL);

// Custom validation with regex
function validateUsername(string $u): ?string {
    if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $u)) {
        return 'Username must be 3-20 alphanumeric chars';
    }
    return null;
}

// Whitelist approach for arrays
$allowed = ['name', 'email', 'age'];
$input = array_intersect_key($_POST, array_flip($allowed));
23

네임스페이스 & 오토로딩 심화

네임스페이스 선언

네임스페이스는 클래스 이름 충돌을 방지하고 코드를 계층적으로 조직합니다. 네임스페이스 선언은 첫 번째 명령문이어야 합니다. use는 클래스, 함수, 상수를 임포트합니다. 별칭(as)은 충돌을 해결합니다. PHP 네임스페이싱은 백슬래시를 사용합니다. PSR-4 표준은 네임스페이스 구분자를 디렉토리 구분자에 매핑: MyApp\Services\UserService -> src/Services/UserService.php.

php
<?php
// File: src/Services/UserService.php
namespace MyApp\Services;

use MyApp\Models\User;
use MyApp\Repositories\UserRepository;
use MyApp\Exceptions\NotFoundException;

class UserService
{
    public function __construct(
        private UserRepository $repo
    ) {}

    public function find(int $id): User
    {
        $user = $this->repo->findById($id);
        if (!$user) {
            throw new NotFoundException("User {$id} not found");
        }
        return $user;
    }
}

PSR-4 오토로딩

PSR-4는 표준 오토로딩 사양: 네임스페이스 접두어가 기본 디렉토리에 매핑되고 각 네임스페이스 구분자가 디렉토리 구분자가 됩니다. Composer가 클래스 이름을 파일 경로로 자동 해결하는 오토로더를 생성합니다. 새 클래스 추가 후 composer dump-autoload를 실행하세요. 오토로더는 클래스가 처음 참조될 때만 로드합니다(지연 로딩).

php
// composer.json
{
    "autoload": {
        "psr-4": {
            "MyApp\\": "src/",
            "Tests\\": "tests/"
        }
    }
}

// After composer install:
require __DIR__ . '/vendor/autoload.php';

// Now all classes auto-load:
use MyApp\Services\UserService;
use MyApp\Controllers\UserController;

// PSR-4 rules:
// MyApp\User          -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php
// MyApp\Models\User\Profile -> src/Models/User/Profile.php

사용자 정의 오토로더

spl_autoload_register는 오토로더 스택에 함수를 추가합니다. 클래스가 참조되었지만 로드되지 않았을 때, PHP는 하나가 클래스를 로드할 때까지 등록된 각 오토로더를 순서대로 호출합니다. 여러 오토로더가 공존할 수 있습니다(예: PSR-4용 하나, 레거시 클래스용 하나). 에러를 피하기 위해 require 전에 파일이 존재하는지 항상 확인하세요. Composer는 내부적으로 이 메커니즘을 사용합니다.

php
<?php
spl_autoload_register(function (string $class): void {
    $prefix = 'MyApp\\';
    $baseDir = __DIR__ . '/src/';

    // Check if class uses our namespace prefix
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;  // Not our class
    }

    // Get relative class name
    $relativeClass = substr($class, $len);

    // Replace namespace separators with directory separators
    $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';

    if (file_exists($file)) {
        require $file;
    }
});

// Multiple autoloaders can be registered (stack-based)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/legacy/' . $class . '.php';
    if (file_exists($path)) require $path;
});

Classmap & Files 오토로딩

classmap 오토로딩은 dump-autoload 시점에 디렉토리를 스캔하고 클래스 이름을 파일 경로에 매핑하는 배열을 빌드합니다. 이는 PSR-4보다 빠릅니다(파일 시스템 확인 대신 하나의 배열 조회)하며 프로덕션에 권장됩니다. files는 매 요청마다 특정 파일을 오토로드하며, 클래스로 오토로드할 수 없는 헬퍼 함수와 상수에 유용합니다. 프로덕션 classmap에는 --optimize를 사용하세요.

php
{
    "autoload": {
        "psr-4": { "MyApp\\": "src/" },
        "classmap": ["src/Legacy/", "lib/OldClasses.php"],
        "files": ["src/helpers.php", "src/constants.php"]
    }
}

// classmap: scans directories and builds class-to-file map
// Faster than PSR-4 for production (no filesystem checks)

// files: always-loaded files (for functions and constants)
// src/helpers.php:
<?php
function dd($var) { var_dump($var); die; }
function env(string $key, $default = null) { /* ... */ }

네임스페이스 해석

네임스페이스가 있는 코드에서 자격 없는 클래스 이름은 먼저 임포트를 통해, 그 다음 현재 네임스페이스에서 해석됩니다. 내장 클래스(DateTime, PDO, Exception)는 전역 네임스페이스에 있습니다; 선행 백슬래시로 참조하거나 임포트하세요. 함수와 상수는 로컬에서 찾지 못하면 전역 네임스페이스로 대체됩니다. 절대 참조를 위해 FQCN(선행 백슬래시)을 사용하세요.

php
<?php
namespace MyApp\Services;

use MyApp\Models\User;

class UserService
{
    public function create(): User
    {
        // User resolves to MyApp\Models\User (imported)
        return new User();
    }

    public function find(): \MyApp\Models\User
    {
        // Fully Qualified Class Name (leading backslash)
        return new \MyApp\Models\User();
    }

    public function date(): \DateTime
    {
        // Global classes need backslash or import
        return new \DateTime();
    }
}
24

제너레이터 & Yield

기본 제너레이터

제너레이터는 yield로 값을 지연 생성, 한 번에 하나씩, 전체 컬렉션을 메모리에 빌드하지 않습니다. 이는 크거나 무한 시퀀스에 메모리 효율적입니다. 함수는 Iterator를 구현하는 Generator 객체를 반환합니다. 각 yield는 실행을 일시 정지, 다음 반복에서 재개합니다. 큰 파일 읽기, 데이터베이스 커서, 계산 시퀀스에 제너레이터를 사용하세요.

php
<?php
function rangeGen(int $start, int $end, int $step = 1): Generator
{
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

// Iterate without loading all values into memory
foreach (rangeGen(1, 5) as $value) {
    echo $value . ' ';  // 1 2 3 4 5
}

// Memory efficient for large sequences
foreach (rangeGen(1, 1000000) as $value) {
    if ($value % 100000 === 0) echo "Reached {$value}\n";
}

Yield 키-값 쌍

제너레이터는 연관 배열처럼 yield key => value 구문으로 키-값 쌍을 yield할 수 있습니다. 이는 변환을 통해 키를 보존합니다. 값을 필터링하려면 단순히 yield하지 마세요. Generator는 반복에서의 위치를 유지하므로 각 제너레이터가 스트림을 변환하거나 필터링하는 파이프라인 스타일 처리를 구축할 수 있습니다.

php
<?php
function mapGen(array $data): Generator
{
    foreach ($data as $key => $value) {
        yield $key => strtoupper($value);
    }
}

foreach (mapGen(['a' => 'hello', 'b' => 'world']) as $key => $value) {
    echo "{$key} => {$value}\n";
}
// a => HELLO
// b => WORLD

// Filter by not yielding unwanted values
function filterGen(array $data): Generator
{
    foreach ($data as $value) {
        if ($value > 0) yield $value;
    }
}

제너레이터에 값 보내기

send() 메서드는 제너레이터에 값을 전달하고, 이는 yield 표현식의 결과가 됩니다. 이는 코루틴과 상태 머신에 유용한 양방향 통신을 가능하게 합니다. current()는 제너레이터를 시작합니다. getReturn()은 제너레이터 완료 후 반환 값을 검색합니다. finally 블록은 제너레이터가 파괴될 때 실행되어 리소스 정리를 가능하게 합니다.

php
<?php
function accumulator(): Generator
{
    $total = 0;
    while (true) {
        $value = yield $total;
        if ($value === null) break;
        $total += $value;
    }
    return $total;
}

$gen = accumulator();
$gen->current();  // Start: 0
$gen->send(10);   // Returns 10
$gen->send(20);   // Returns 30
$gen->send(5);    // Returns 35
$gen->send(null); // Triggers return
echo $gen->getReturn();  // 35

Yield from (위임)

yield from은 다른 제너레이터, 배열, 또는 Traversable에 위임하여 그 값을 외부 제너레이터로 평평하게 합니다. 내부 제너레이터의 반환 값은 외부 제너레이터에서 사용 가능합니다. 이는 합성을 가능하게: 단순한 제너레이터에서 복잡한 파이프라인을 구축하세요. yield from은 수동으로 반복하고 재-yield하는 것보다 더 효율적입니다.

php
<?php
function innerGen(): Generator
{
    yield 1;
    yield 2;
    return 'inner done';
}

function outerGen(): Generator
{
    yield 0;
    $result = yield from innerGen();
    echo "Inner returned: {$result}\n";
    yield 3;
}

foreach (outerGen() as $value) {
    echo $value . ' ';  // 0 1 2 3
}
// Inner returned: inner done

// Yield from arrays
function mixedGen(): Generator {
    yield from [10, 20, 30];
    yield from new ArrayObject([40, 50]);
}

실제 사용 사례

제너레이터는 크거나 무한 데이터 스트림 처리에 뛰어납니다: 파일을 한 줄씩 읽기, 데이터베이스 커서 반복, 페이지네이션된 API 가져오기, 수학 시퀀스. take() 패턴은 무한 제너레이터를 제한합니다. 제너레이터는 잘 합성됩니다: 필터링, 매핑, 축소를 위해 여러 제너레이터를 통해 데이터를 파이프하세요. 메모리는 데이터 크기와 관계없이 일정하게 유지됩니다.

php
<?php
// 1. Read large files line by line (memory efficient)
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (!feof($handle)) {
        $line = fgets($handle);
        if ($line !== false) yield rtrim($line);
    }
    fclose($handle);
}

foreach (readLines('large.log') as $line) {
    if (str_contains($line, 'ERROR')) {
        echo "{$line}\n";
    }
}

// 2. Infinite sequence with take()
function naturals(): Generator {
    $n = 1;
    while (true) yield $n++;
}

function take(Generator $gen, int $n): Generator {
    for ($i = 0; $i < $n; $i++) {
        yield $gen->current();
        $gen->next();
    }
}

foreach (take(naturals(), 5) as $num) {
    echo $num . ' ';  // 1 2 3 4 5
}
25

보안

SQL 인젝션 방지

SQL 인젝션은 사용자 입력이 SQL에 연결될 때 발생합니다. 매개변수화된 쿼리와 함께 준비된 문장을 항상 사용하세요. PDO와 MySQLi 모두 지원합니다. 사용자 입력을 절대 신뢰하지 마세요. 모든 외부 데이터를 검증하고 살균하세요.

php
// BAD: vulnerable
$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";
// GOOD: prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);
$users = $stmt->fetchAll();

XSS 방지

XSS(크로스 사이트 스크립팅)는 악성 스크립트를 주입합니다. htmlspecialchars는 특수 문자를 HTML 엔티티로 변환합니다. ENT_QUOTES는 작은따옴표와 큰따옴표 모두 이스케이프합니다. 사용자 데이터를 HTML로 출력할 때 항상 이스케이프하세요. 심층 방어를 위해 Content-Security-Policy 헤더를 사용하세요.

php
// BAD: output without escaping
echo $_GET['name'];
// GOOD: escape output
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// For HTML attributes
echo 'value="' . htmlspecialchars($value, ENT_QUOTES) . '"';

비밀번호 해싱

password_hash는 자동 솔트 생성과 함께 bcrypt(또는 argon2)를 사용합니다. 비밀번호에 md5나 sha1을 절대 사용하지 마세요. password_verify는 해시와 비밀번호를 확인합니다. password_needs_rehash는 해시 알고리즘 업그레이드를 허용합니다. 솔트는 해시 문자열에 포함되어 있습니다.

php
// Hash a password
$hash = password_hash('mypassword', PASSWORD_DEFAULT);
// Verify
if (password_verify('mypassword', $hash)) {
    echo 'Valid password';
}
// Check if needs rehash
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash('mypassword', PASSWORD_DEFAULT);
}

CSRF 보호

CSRF(크로스 사이트 요청 위조)는 사용자를 속여 원치 않는 작업을 하게 합니다. 세션당 랜덤 토큰을 생성하세요. 폼에 숨겨진 필드로 포함하세요. hash_equals(타이밍 안전 비교)로 POST에서 검증하세요. SameSite 쿠키는 추가 보호를 제공합니다.

php
session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
// In form
echo '<input type="hidden" name="token" value="' . $_SESSION['token'] . '">';
// Verify
if (!hash_equals($_SESSION['token'], $_POST['token'] ?? '')) {
    die('CSRF token mismatch');
}

세션 보안

cookie_httponly는 JavaScript 접근을 방지합니다. cookie_secure는 HTTPS만 보장합니다. samesite=Strict은 CSRF를 방지합니다. use_strict_mode는 초기화되지 않은 세션 ID를 거부합니다. session_regenerate_id는 세션 고정을 방지합니다. 권한 변경 후 항상 재생성하세요.

php
// Secure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);  // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();
// Regenerate ID after login
session_regenerate_id(true);

Was this helpful?