이 문서는 PHP 8 기준으로 게임 서버 개발에 필요한 핵심 개념과 패턴을 소개합니다. 각 섹션마다 예제 코드와 간단한 연습 문제를 포함하고 있습니다.
- 네임스페이스와 오토로딩
- 객체지향 프로그래밍
- 타입 시스템
- 트레이트
- 예외 처리
- 데이터베이스 작업
- 세션 관리
- HTTP 요청/응답 처리
- JSON 처리
- 날짜 및 시간 처리
- 캐싱
- 로깅
- 보안
- PHP 8 신규 기능
네임스페이스는 코드를 논리적으로 그룹화하고 이름 충돌을 방지합니다. PSR-4 오토로딩은 클래스 파일을 자동으로 로드하는 표준입니다.
// 네임스페이스 선언
namespace Server\Service;
// 다른 네임스페이스의 클래스 사용
use Server\Domain\User;
use Server\Persistent\Repository\UserRepository;
class UserService {
public function getUser(int $userId): ?User {
return UserRepository::getInstance()->get($userId);
}
}
{
"autoload": {
"psr-4": {
"Server\\": "src/Server",
"Background\\": "src/Background"
}
}
}
문제: 다음 파일 구조에 맞는 네임스페이스를 작성하고, App\Models\Product 클래스를 사용하는 코드를 작성하세요.
project/
- src/
- App/
- Models/
- Product.php
- Services/
- ProductService.php
답안:
// src/App/Models/Product.php
namespace App\Models;
class Product {
public int $id;
public string $name;
}
// src/App/Services/ProductService.php
namespace App\Services;
use App\Models\Product;
class ProductService {
public function getProduct(int $id): Product {
$product = new Product();
$product->id = $id;
$product->name = "제품 {$id}";
return $product;
}
}
PHP는 클래스, 인터페이스, 추상 클래스, 정적 메서드 등 완전한 객체지향 기능을 제공합니다.
namespace Server\Handler;
interface Handler {
public function handle($request);
}
class LoginHandler implements Handler {
public function handle($request) {
// 로그인 처리 로직
return new Response();
}
}
namespace Server\Base;
trait Singleton {
protected static $instance = null;
public static function getInstance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
final private function __clone() {}
final private function __wakeup() {}
}
class Config {
use Singleton;
// Config 클래스 구현
}
// 사용
$config = Config::getInstance();
문제: 상품(Product)에 대한 저장소(Repository) 인터페이스를 정의하고 메모리 기반 구현체를 작성하세요. 싱글톤 패턴을 적용하세요.
답안:
namespace App\Repository;
interface ProductRepository {
public function findById(int $id);
public function save($product);
public function delete(int $id);
}
trait Singleton {
private static $instance = null;
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {}
private function __clone() {}
}
class MemoryProductRepository implements ProductRepository {
use Singleton;
private array $products = [];
public function findById(int $id) {
return $this->products[$id] ?? null;
}
public function save($product) {
$this->products[$product->id] = $product;
}
public function delete(int $id) {
unset($this->products[$id]);
}
}
// 사용
$repository = MemoryProductRepository::getInstance();
PHP 8은 향상된 타입 시스템을 제공하여 코드의 안정성을 높입니다. 매개변수 타입, 반환 타입, 유니온 타입, 속성 타입 등을 지원합니다.
class User {
public function __construct(
private int $id,
private string $name,
private ?string $email = null // null 허용
) {}
public function getId(): int {
return $this->id;
}
public function getName(): string {
return $this->name;
}
// 유니온 타입 (PHP 8)
public function processData(array|string $data): bool|int {
if (is_array($data)) {
return count($data);
}
return !empty($data);
}
}
문제: 다음 메서드에 적절한 타입 힌팅을 추가하세요.
function calculateTotal($items, $taxRate) {
$total = 0;
foreach ($items as $item) {
$total += $item->price * $item->quantity;
}
return $total * (1 + $taxRate);
}
답안:
function calculateTotal(array $items, float $taxRate): float {
$total = 0;
foreach ($items as $item) {
$total += $item->price * $item->quantity;
}
return $total * (1 + $taxRate);
}
트레이트는 클래스 간에 메서드를 재사용할 수 있는 방법을 제공합니다. 다중 상속의 한계를 극복하는 데 유용합니다.
trait Loggable {
private function log(string $message): void {
echo "[LOG] {$message}\n";
}
public function logInfo(string $message): void {
$this->log("INFO: {$message}");
}
public function logError(string $message): void {
$this->log("ERROR: {$message}");
}
}
class UserService {
use Loggable;
public function createUser(string $name): void {
// 사용자 생성 로직
$this->logInfo("사용자 '{$name}' 생성됨");
}
}
문제: 캐시 기능(get, set, has)을 제공하는 Cacheable 트레이트를 작성하고, ProductService 클래스에 적용하세요.
답안:
trait Cacheable {
private array $cache = [];
public function cacheGet(string $key) {
return $this->cache[$key] ?? null;
}
public function cacheSet(string $key, $value): void {
$this->cache[$key] = $value;
}
public function cacheHas(string $key): bool {
return isset($this->cache[$key]);
}
public function cacheClear(): void {
$this->cache = [];
}
}
class ProductService {
use Cacheable;
public function getProduct(int $id) {
$cacheKey = "product_{$id}";
if ($this->cacheHas($cacheKey)) {
return $this->cacheGet($cacheKey);
}
// 데이터베이스에서 상품 조회 (예시)
$product = ['id' => $id, 'name' => "Product {$id}"];
$this->cacheSet($cacheKey, $product);
return $product;
}
}
예외 처리는 오류를 구조적으로 관리하는 방법입니다. PHP는 try-catch-finally 구문과 예외 계층 구조를 지원합니다.
namespace Server\Base\Exception;
class Exception extends \Exception {}
class SystemException extends Exception {}
class GameServerException extends Exception {}
class DesignDataException extends Exception {}
// 사용
try {
$user = UserRepository::getInstance()->get($userId);
if ($user === false) {
throw new GameServerException("사용자가 존재하지 않습니다", ServerResultCode::USER_NOT_EXIST);
}
// 비즈니스 로직
} catch (GameServerException $e) {
// 게임 관련 오류 처리
LogManager::getInstance()->add(new ErrorLog($userId, $e));
} catch (SystemException $e) {
// 시스템 오류 처리
} catch (\Exception $e) {
// 기타 오류 처리
} finally {
// 정리 작업
LockManager::unLock(LockManager::getUserKey($userId));
}
문제: 다음 코드에 적절한 예외 처리를 추가하세요. 파일을 열 수 없는 경우와 파일 내용을 파싱할 수 없는 경우를 별도로 처리해야 합니다.
function loadConfig(string $filename) {
$content = file_get_contents($filename);
return json_decode($content, true);
}
답안:
class FileNotFoundException extends \Exception {}
class JsonParseException extends \Exception {}
function loadConfig(string $filename) {
try {
$content = @file_get_contents($filename);
if ($content === false) {
throw new FileNotFoundException("파일을 열 수 없습니다: {$filename}");
}
$config = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new JsonParseException("JSON 파싱 오류: " . json_last_error_msg());
}
return $config;
} catch (FileNotFoundException $e) {
// 파일 없음 오류 처리
error_log($e->getMessage());
return [];
} catch (JsonParseException $e) {
// JSON 파싱 오류 처리
error_log($e->getMessage());
return [];
} catch (\Exception $e) {
// 기타 오류
error_log("알 수 없는 오류: " . $e->getMessage());
return [];
}
}
PHP는 PDO(PHP Data Objects)를 통해 데이터베이스 작업을 추상화합니다. 준비된 구문을 사용하여 SQL 주입 공격을 방지합니다.
// 연결 생성
$dsn = "mysql:host=localhost;port=3306;dbname=gameserver;charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
];
$pdo = new PDO($dsn, $username, $password, $options);
// 쿼리 실행
try {
// 준비된 구문
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->bindValue(':id', $userId, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch();
} catch (PDOException $e) {
// 데이터베이스 오류 처리
}
// 트랜잭션
try {
$pdo->beginTransaction();
// 여러 쿼리 실행
$stmt1 = $pdo->prepare("INSERT INTO orders (user_id, product_id) VALUES (:user_id, :product_id)");
$stmt1->execute([':user_id' => $userId, ':product_id' => $productId]);
$orderId = $pdo->lastInsertId();
$stmt2 = $pdo->prepare("UPDATE inventory SET quantity = quantity - 1 WHERE product_id = :product_id");
$stmt2->execute([':product_id' => $productId]);
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
throw $e;
}
문제: 사용자 테이블에서 특정 레벨 이상의 사용자를 조회하는 함수를 작성하세요. 결과는 레벨 내림차순으로 정렬되어야 합니다.
답안:
function getUsersByMinLevel(PDO $pdo, int $minLevel): array {
try {
$stmt = $pdo->prepare("
SELECT id, username, level, exp
FROM users
WHERE level >= :min_level
ORDER BY level DESC, exp DESC
");
$stmt->bindValue(':min_level', $minLevel, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
} catch (PDOException $e) {
error_log("데이터베이스 오류: " . $e->getMessage());
return [];
}
}
// 사용 예
$highLevelUsers = getUsersByMinLevel($pdo, 30);
foreach ($highLevelUsers as $user) {
echo "ID: {$user['id']}, 이름: {$user['username']}, 레벨: {$user['level']}\n";
}
세션은 여러 요청 간에 사용자 데이터를 유지하는 메커니즘입니다. 게임 서버에서는 사용자 인증 상태를 유지하기 위해 중요합니다.
class SessionManager {
private static function getKey($userId) {
return "session:{$userId}";
}
public static function createSession($userId, $deviceId) {
$sessionId = sha1($userId . time() . mt_rand());
$session = [
'sessionId' => $sessionId,
'userId' => $userId,
'deviceId' => $deviceId,
'createdAt' => time(),
'expiresAt' => time() + 3600 // 1시간 후 만료
];
// 세션 저장 (캐시 서버 등)
$memcached = MemcachedConnection::getInstance()->getMemcached();
$memcached->set(self::getKey($userId), $session, 3600);
return $sessionId;
}
public static function validateSession($userId, $sessionId) {
$memcached = MemcachedConnection::getInstance()->getMemcached();
$session = $memcached->get(self::getKey($userId));
if ($session === false || $session['sessionId'] !== $sessionId) {
throw new Exception("세션이 유효하지 않습니다");
}
if ($session['expiresAt'] < time()) {
throw new Exception("세션이 만료되었습니다");
}
// 세션 갱신
$session['expiresAt'] = time() + 3600;
$memcached->set(self::getKey($userId), $session, 3600);
return true;
}
}
문제: 위 SessionManager 클래스에 세션을 파기하는 destroySession
메서드를 추가하세요.
답안:
// SessionManager 클래스에 추가할 메서드
public static function destroySession($userId, $sessionId) {
$memcached = MemcachedConnection::getInstance()->getMemcached();
$session = $memcached->get(self::getKey($userId));
// 세션이 존재하고 일치하는 경우에만 삭제
if ($session !== false && $session['sessionId'] === $sessionId) {
$memcached->delete(self::getKey($userId));
return true;
}
return false;
}
게임 서버는 HTTP 요청을 처리하고 응답을 생성합니다. PHP는 요청 데이터 접근과 응답 헤더 설정을 위한 기능을 제공합니다.
class HttpRequest {
public function getPostData() {
$rawPost = file_get_contents('php://input');
if (empty($rawPost)) {
throw new Exception('Raw post data is empty');
}
return $rawPost;
}
public function getHeaderValue($headerName) {
$name = strtoupper($headerName);
if (!isset($_SERVER[$name])) {
throw new Exception("Header not found: {$headerName}");
}
return $_SERVER[$name];
}
}
class HttpResponse {
public function flush($api, $userId, $encryptedResponse) {
header("Content-Type: application/octet-stream");
header("Content-Length: " . strlen($encryptedResponse));
header("Cache-Control: no-cache, must-revalidate");
header("Pragma: no-cache");
if (ob_get_level() > 0) {
ob_clean();
}
echo $encryptedResponse;
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
}
문제: HttpRequest 클래스에 JSON 요청을 처리하는 메서드를 추가하세요.
답안:
// HttpRequest 클래스에 추가할 메서드
public function getJsonData() {
$rawPost = $this->getPostData();
$data = json_decode($rawPost, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("JSON 파싱 오류: " . json_last_error_msg());
}
return $data;
}
// 사용 예
try {
$request = new HttpRequest();
$jsonData = $request->getJsonData();
// JSON 데이터 처리
$response = processRequest($jsonData);
$httpResponse = new HttpResponse();
$httpResponse->flush('api_name', $jsonData['userId'] ?? 0, json_encode($response));
} catch (Exception $e) {
// 오류 처리
header("HTTP/1.1 400 Bad Request");
echo json_encode(['error' => $e->getMessage()]);
}
JSON은 게임 서버와 클라이언트 간의 통신에 널리 사용됩니다. PHP는 JSON 인코딩 및 디코딩을 위한 기본 함수를 제공합니다.
// JsonSerializable 구현
class User implements \JsonSerializable {
private $id;
private $name;
private $level;
private $lastLoginAt;
public function __construct($id, $name, $level, $lastLoginAt) {
$this->id = $id;
$this->name = $name;
$this->level = $level;
$this->lastLoginAt = $lastLoginAt;
}
// 직렬화할 속성 지정
public function jsonSerialize(): array {
return [
'id' => $this->id,
'name' => $this->name,
'level' => $this->level,
'lastLogin' => $this->lastLoginAt
];
}
}
// 사용
$user = new User(1, "Player1", 10, "2023-03-15 14:30:00");
$json = json_encode($user);
echo $json; // {"id":1,"name":"Player1","level":10,"lastLogin":"2023-03-15 14:30:00"}
// JSON 디코딩
$data = json_decode('{"name":"Player2","level":20}', true);
echo $data['name']; // Player2
문제: 상품 정보를 JSON으로 직렬화할 수 있는 Product 클래스를 작성하세요. 상품은 ID, 이름, 가격, 재고 속성을 가집니다. 재고가 0인 경우 'in_stock' 속성을 false로 직렬화해야 합니다.
답안:
class Product implements \JsonSerializable {
private int $id;
private string $name;
private float $price;
private int $stock;
public function __construct(int $id, string $name, float $price, int $stock) {
$this->id = $id;
$this->name = $name;
$this->price = $price;
$this->stock = $stock;
}
public function jsonSerialize(): array {
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
'stock' => $this->stock,
'in_stock' => $this->stock > 0
];
}
// 필요한 게터/세터 메서드
public function getId(): int {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function getPrice(): float {
return $this->price;
}
public function getStock(): int {
return $this->stock;
}
}
// 테스트
$product1 = new Product(1, "게임 아이템 A", 100.5, 10);
$product2 = new Product(2, "게임 아이템 B", 200.0, 0);
echo json_encode($product1) . "\n";
// {"id":1,"name":"게임 아이템 A","price":100.5,"stock":10,"in_stock":true}
echo json_encode($product2) . "\n";
// {"id":2,"name":"게임 아이템 B","price":200,"stock":0,"in_stock":false}
게임 서버에서 날짜와 시간은 이벤트 스케줄링, 유저 활동 추적 등에 중요합니다. PHP의 DateTime 클래스는 강력한 날짜/시간 조작 기능을 제공합니다.
// 현재 날짜 및 시간
$now = new DateTime();
echo $now->format('Y-m-d H:i:s'); // 2023-03-15 15:30:45
// 특정 날짜/시간 생성
$date = new DateTime('2023-01-15 10:00:00');
// 타임존 설정
$date->setTimezone(new DateTimeZone('Asia/Seoul'));
echo $date->format('Y-m-d H:i:s P'); // 2023-01-15 10:00:00 +09:00
// 날짜 연산
$date->modify('+7 days');
echo $date->format('Y-m-d'); // 2023-01-22
// 날짜 차이 계산
$diff = $now->diff($date);
echo $diff->days . " 일 차이"; // X 일 차이
// DateInterval 사용
$interval = new DateInterval('P1M2DT3H'); // 1달 2일 3시간
$date->add($interval);
문제: 특정 게임 이벤트가 시작되는 날짜부터 종료 날짜까지 남은 시간(일, 시간, 분)을 계산하는 함수를 작성하세요.
답안:
function getTimeRemaining(string $eventEndDateStr): array {
try {
$now = new DateTime();
$endDate = new DateTime($eventEndDateStr);
// 이벤트가 이미 종료된 경우
if ($now > $endDate) {
return [
'ended' => true,
'days' => 0,
'hours' => 0,
'minutes' => 0,
'seconds' => 0
];
}
$interval = $now->diff($endDate);
return [
'ended' => false,
'days' => $interval->days,
'hours' => $interval->h,
'minutes' => $interval->i,
'seconds' => $interval->s,
'total_hours' => $interval->days * 24 + $interval->h,
'total_minutes' => ($interval->days * 24 + $interval->h) * 60 + $interval->i
];
} catch (Exception $e) {
return [
'error' => $e->getMessage()
];
}
}
// 테스트
$eventEnd = '2023-12-31 23:59:59';
$remaining = getTimeRemaining($eventEnd);
echo "이벤트 종료까지 남은 시간:\n";
echo "{$remaining['days']}일 {$remaining['hours']}시간 {$remaining['minutes']}분\n";
echo "총 {$remaining['total_hours']}시간 남았습니다.\n";
캐싱은 자주 액세스하는 데이터를 빠르게 검색할 수 있도록 저장하는 기술입니다. PHP에서는 Memcached, Redis, APC 등의 캐싱 시스템을 사용할 수 있습니다.
// 연결 생성
$memcached = new Memcached();
$memcached->addServer('localhost', 11211);
// 데이터 저장
$memcached->set('user:1', $userData, 3600); // 1시간 만료
// 데이터 조회
$data = $memcached->get('user:1');
if ($data === false && $memcached->getResultCode() === Memcached::RES_NOTFOUND) {
// 캐시 미스: DB에서 데이터 로드
$data = loadUserFromDatabase(1);
$memcached->set('user:1', $data, 3600);
}
// 연결 생성
$redis = new Redis();
$redis->connect('localhost', 6379);
// 문자열 저장/조회
$redis->set('user:name:1', 'Player1', 3600); // 1시간 만료
$name = $redis->get('user:name:1');
// 해시 저장/조회
$redis->hSet('user:1', 'name', 'Player1');
$redis->hSet('user:1', 'level', 10);
$userData = $redis->hGetAll('user:1');
// 리스트 조작
$redis->lPush('recent_users', 1);
$recentUsers = $redis->lRange('recent_users', 0, -1);
// 정렬된 세트
$redis->zAdd('highscores', 1000, 'player1');
$redis->zAdd('highscores', 2000, 'player2');
$topPlayers = $redis->zRevRange('highscores', 0, 2); // 상위 3명
문제: Redis를 사용하여 사용자 접속 상태를 캐싱하는 함수를 작성하세요. 사용자 ID를 키로, 접속 상태와 최종 접속 시간을 저장해야 합니다.
답안:
class UserStatusCache {
private Redis $redis;
private int $expireTime;
public function __construct(Redis $redis, int $expireTime = 3600) {
$this->redis = $redis;
$this->expireTime = $expireTime;
}
public function setUserOnline(int $userId): void {
$key = "user:status:{$userId}";
$now = time();
$this->redis->hMSet($key, [
'status' => 'online',
'last_seen' => $now
]);
$this->redis->expire($key, $this->expireTime);
// 온라인 사용자 목록에 추가
$this->redis->sAdd('online_users', $userId);
}
public function setUserOffline(int $userId): void {
$key = "user:status:{$userId}";
$now = time();
$this->redis->hMSet($key, [
'status' => 'offline',
'last_seen' => $now
]);
// 온라인 사용자 목록에서 제거
$this->redis->sRem('online_users', $userId);
}
public function getUserStatus(int $userId): array {
$key = "user:status:{$userId}";
$status = $this->redis->hGetAll($key);
if (empty($status)) {
return [
'status' => 'unknown',
'last_seen' => 0
];
}
return $status;
}
public function getOnlineUsers(): array {
return $this->redis->sMembers('online_users');
}
public function getOnlineCount(): int {
return $this->redis->sCard('online_users');
}
}
// 사용 예
$redis = new Redis();
$redis->connect('localhost', 6379);
$userStatusCache = new UserStatusCache($redis);
// 사용자가 로그인할 때
$userStatusCache->setUserOnline(1001);
// 상태 확인
$status = $userStatusCache->getUserStatus(1001);
echo "사용자 상태: {$status['status']}, 최근 접속: " . date('Y-m-d H:i:s', $status['last_seen']) . "\n";
// 온라인 사용자 수 확인
echo "온라인 사용자 수: " . $userStatusCache->getOnlineCount() . "\n";
// 사용자가 로그아웃할 때
$userStatusCache->setUserOffline(1001);
로깅은 시스템 작동 방식을 모니터링하고 문제를 해결하는 데 중요합니다. PHP는 내장 오류 로깅 기능을 제공하지만, 구조화된 로깅에는 사용자 정의 시스템이나 라이브러리를 사용할 수 있습니다.
class LogManager {
private static $instance = null;
private $logStore;
private function __construct() {
$this->logStore = new FileLogStore('/var/log/gameserver/');
}
public static function getInstance() {
if (is_null(self::$instance)) {
self::$instance = new LogManager();
}
return self::$instance;
}
public function add(Log $log) {
$this->logStore->add($log);
}
public function transfer() {
$this->logStore->flush();
$this->logStore->clear();
}
}
// 로그 클래스
abstract class Log implements \JsonSerializable {
public $date;
public function __construct() {
$this->date = (new DateTime())->format('Y-m-d H:i:s');
}
abstract public function getLogName();
public function jsonSerialize(): array {
return get_object_vars($this);
}
}
// 구체적인 로그 클래스
class ErrorLog extends Log {
public $userId;
public $api;
public $errorCode;
public $errorMessage;
public function __construct($userId, $api, $errorCode, $errorMessage) {
parent::__construct();
$this->userId = $userId;
$this->api = $api;
$this->errorCode = $errorCode;
$this->errorMessage = $errorMessage;
}
public function getLogName() {
return 'error_log';
}
}
// 사용
try {
// 비즈니스 로직
} catch (Exception $e) {
LogManager::getInstance()->add(new ErrorLog(
$userId,
$api,
$e->getCode(),
$e->getMessage()
));
}
문제: 게임 내 아이템 구매를 로깅하는 PurchaseLog 클래스를 작성하세요. 로그는 사용자 ID, 아이템 ID, 수량, 가격, 구매 시간을 포함해야 합니다.
답안:
class PurchaseLog extends Log {
public int $userId;
public int $itemId;
public int $quantity;
public float $price;
public float $totalAmount;
public string $currency;
public string $purchaseId;
public function __construct(
int $userId,
int $itemId,
int $quantity,
float $price,
string $currency = 'USD',
string $purchaseId = null
) {
parent::__construct();
$this->userId = $userId;
$this->itemId = $itemId;
$this->quantity = $quantity;
$this->price = $price;
$this->totalAmount = $price * $quantity;
$this->currency = $currency;
$this->purchaseId = $purchaseId ?? uniqid('purchase_', true);
}
public function getLogName() {
return 'purchase_log';
}
// JsonSerializable 인터페이스 구현
public function jsonSerialize(): array {
return [
'date' => $this->date,
'user_id' => $this->userId,
'item_id' => $this->itemId,
'quantity' => $this->quantity,
'price' => $this->price,
'total_amount' => $this->totalAmount,
'currency' => $this->currency,
'purchase_id' => $this->purchaseId
];
}
}
// 사용 예
$purchaseLog = new PurchaseLog(
userId: 1001,
itemId: 5001,
quantity: 5,
price: 9.99,
currency: 'USD'
);
LogManager::getInstance()->add($purchaseLog);
LogManager::getInstance()->transfer(); // 로그 저장
보안은 게임 서버에서 중요한 측면입니다. 입력 검증, 암호화, 인증 등을 통해 보안을 강화할 수 있습니다.
// 입력 검증
function validateUsername(string $username): bool {
return preg_match('/^[a-zA-Z0-9_]{3,16}$/', $username) === 1;
}
function validateEmail(string $email): bool {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
// 사용
if (!validateUsername($username)) {
throw new Exception("유효하지 않은 사용자 이름입니다");
}
class Cryptographer {
const METHOD = "aes-256-cbc";
private $key;
private $iv;
public function __construct(string $key, string $iv) {
$this->key = $key;
$this->iv = $iv;
}
public function encrypt(string $plainText): string {
$encrypted = openssl_encrypt(
$plainText,
self::METHOD,
$this->key,
OPENSSL_RAW_DATA,
$this->iv
);
return base64_encode($encrypted);
}
public function decrypt(string $encryptedText): string {
$encrypted = base64_decode($encryptedText);
$decrypted = openssl_decrypt(
$encrypted,
self::METHOD,
$this->key,
OPENSSL_RAW_DATA,
$this->iv
);
if ($decrypted === false) {
throw new Exception("복호화 실패");
}
return $decrypted;
}
}
문제: 간단한 토큰 기반 인증 시스템을 구현하세요. 토큰은 사용자 ID, 만료 시간, 보안 서명을 포함해야 합니다.
답안:
class TokenManager {
private string $secretKey;
public function __construct(string $secretKey) {
$this->secretKey = $secretKey;
}
public function generateToken(int $userId, int $expiresIn = 3600): string {
$now = time();
$expires = $now + $expiresIn;
$payload = [
'user_id' => $userId,
'created' => $now,
'expires' => $expires
];
$payloadEncoded = base64_encode(json_encode($payload));
$signature = hash_hmac('sha256', $payloadEncoded, $this->secretKey);
return $payloadEncoded . '.' . $signature;
}
public function validateToken(string $token): ?array {
$parts = explode('.', $token);
if (count($parts) !== 2) {
return null;
}
[$payloadEncoded, $signature] = $parts;
// 서명 검증
$expectedSignature = hash_hmac('sha256', $payloadEncoded, $this->secretKey);
if (!hash_equals($expectedSignature, $signature)) {
return null;
}
// 페이로드 디코딩
$payload = json_decode(base64_decode($payloadEncoded), true);
// 만료 확인
if (!isset($payload['expires']) || $payload['expires'] < time()) {
return null;
}
return $payload;
}
public function getUserIdFromToken(string $token): ?int {
$payload = $this->validateToken($token);
if ($payload === null || !isset($payload['user_id'])) {
return null;
}
return $payload['user_id'];
}
}
// 사용 예
$tokenManager = new TokenManager('your-secret-key-here');
// 토큰 생성
$token = $tokenManager->generateToken(1001, 3600); // 1시간 유효
// 토큰 검증
$payload = $tokenManager->validateToken($token);
if ($payload !== null) {
echo "유효한 토큰: 사용자 ID {$payload['user_id']}, 만료 시간 " .
date('Y-m-d H:i:s', $payload['expires']) . "\n";
} else {
echo "유효하지 않은 토큰\n";
}
// 토큰에서 사용자 ID 추출
$userId = $tokenManager->getUserIdFromToken($token);
echo "토큰의 사용자 ID: " . ($userId ?? '없음') . "\n";
PHP 8은 이전 버전에 비해 많은 새로운 기능과 개선 사항을 도입했습니다. 이러한 기능들은 코드를 더 간결하고 안전하게 작성하는 데 도움이 됩니다.
function createUser(string $name, string $email, int $age = 18) {
// ...
}
// PHP 8 이전
createUser('John Doe', '[email protected]', 25);
// PHP 8
createUser(
name: 'John Doe',
email: '[email protected]',
age: 25
);
// 순서 변경 가능
createUser(
age: 25,
name: 'John Doe',
email: '[email protected]'
);
// PHP 8 이전
class User {
private $name;
private $email;
public function __construct(string $name, string $email) {
$this->name = $name;
$this->email = $email;
}
}
// PHP 8
class User {
public function __construct(
private string $name,
private string $email
) {}
// 게터는 별도로 정의해야 함
public function getName(): string {
return $this->name;
}
}
// PHP 8 이전 (switch)
$result = '';
switch ($status) {
case 'success':
$result = 'Operation completed';
break;
case 'pending':
$result = 'Operation in progress';
break;
case 'failed':
$result = 'Operation failed';
break;
default:
$result = 'Unknown status';
}
// PHP 8 (match)
$result = match ($status) {
'success' => 'Operation completed',
'pending' => 'Operation in progress',
'failed' => 'Operation failed',
default => 'Unknown status'
};
// PHP 8 이전
$country = null;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->getCountry();
}
}
// PHP 8
$country = $user?->getAddress()?->getCountry();
// PHP 8
function process(string|array $data): int|float {
if (is_array($data)) {
return count($data);
}
return strlen($data);
}
// PHP 8
#[Route('/api/users', methods: ['GET'])]
class UserController {
#[Required]
private string $name;
#[Deprecated('Use newMethod() instead')]
public function oldMethod() {
// ...
}
}
문제: PHP 8의 새로운 기능을 사용하여 게임 아이템 클래스를 작성하세요. 아이템에는 ID, 이름, 타입, 가격이 있으며, 타입은 'weapon', 'armor', 'consumable' 중 하나여야 합니다.
답안:
enum ItemType: string {
case WEAPON = 'weapon';
case ARMOR = 'armor';
case CONSUMABLE = 'consumable';
}
class Item implements JsonSerializable {
public function __construct(
private int $id,
private string $name,
private ItemType $type,
private float $price = 0.0,
private ?string $description = null
) {}
public function getPrice(): float {
return match($this->type) {
ItemType::WEAPON => $this->price * 1.1, // 무기는 10% 추가 세금
ItemType::ARMOR => $this->price * 1.05, // 방어구는 5% 추가 세금
ItemType::CONSUMABLE => $this->price, // 소모품은 세금 없음
};
}
public function getDescription(): string {
return $this->description ?? "No description available for {$this->name}";
}
public function jsonSerialize(): array {
return [
'id' => $this->id,
'name' => $this->name,
'type' => $this->type->value,
'base_price' => $this->price,
'final_price' => $this->getPrice(),
'description' => $this->getDescription()
];
}
}
// 사용 예
$items = [
new Item(
id: 1001,
name: "Steel Sword",
type: ItemType::WEAPON,
price: 100.0,
description: "A sharp steel sword"
),
new Item(
id: 2001,
name: "Leather Armor",
type: ItemType::ARMOR,
price: 80.0
),
new Item(
id: 3001,
name: "Health Potion",
type: ItemType::CONSUMABLE,
price: 20.0,
description: "Restores 50 HP"
)
];
foreach ($items as $item) {
$data = json_encode($item, JSON_PRETTY_PRINT);
echo $data . "\n";
// 가격 출력
$finalPrice = $item->getPrice();
echo "{$item->getDescription()}: {$finalPrice} 골드\n\n";
}
이 학습 가이드가 PHP 8을 배우는 데 도움이 되길 바랍니다. 각 섹션을 순서대로 학습하고 예제와 연습 문제를 통해 실습해보세요.