Skip to content

Instantly share code, notes, and snippets.

@jacking75
Created March 20, 2025 07:20
Show Gist options
  • Save jacking75/1a95a80d554dba50d3993365a2c012f0 to your computer and use it in GitHub Desktop.
Save jacking75/1a95a80d554dba50d3993365a2c012f0 to your computer and use it in GitHub Desktop.
PHP 8 학습 가이드 (게임 서버 개발 중심)

PHP 8 학습 가이드 (게임 서버 개발 중심)

이 문서는 PHP 8 기준으로 게임 서버 개발에 필요한 핵심 개념과 패턴을 소개합니다. 각 섹션마다 예제 코드와 간단한 연습 문제를 포함하고 있습니다.

목차

  1. 네임스페이스와 오토로딩
  2. 객체지향 프로그래밍
  3. 타입 시스템
  4. 트레이트
  5. 예외 처리
  6. 데이터베이스 작업
  7. 세션 관리
  8. HTTP 요청/응답 처리
  9. JSON 처리
  10. 날짜 및 시간 처리
  11. 캐싱
  12. 로깅
  13. 보안
  14. PHP 8 신규 기능

1. 네임스페이스와 오토로딩

개념

네임스페이스는 코드를 논리적으로 그룹화하고 이름 충돌을 방지합니다. 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);
    }
}

composer.json 설정

{
  "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;
    }
}

2. 객체지향 프로그래밍

개념

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();

3. 타입 시스템

개념

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);
}

4. 트레이트

개념

트레이트는 클래스 간에 메서드를 재사용할 수 있는 방법을 제공합니다. 다중 상속의 한계를 극복하는 데 유용합니다.

예제

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;
    }
}

5. 예외 처리

개념

예외 처리는 오류를 구조적으로 관리하는 방법입니다. 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 [];
    }
}

6. 데이터베이스 작업

개념

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";
}

7. 세션 관리

개념

세션은 여러 요청 간에 사용자 데이터를 유지하는 메커니즘입니다. 게임 서버에서는 사용자 인증 상태를 유지하기 위해 중요합니다.

예제

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;
}

8. HTTP 요청/응답 처리

개념

게임 서버는 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()]);
}

9. JSON 처리

개념

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}

10. 날짜 및 시간 처리

개념

게임 서버에서 날짜와 시간은 이벤트 스케줄링, 유저 활동 추적 등에 중요합니다. 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";

11. 캐싱

개념

캐싱은 자주 액세스하는 데이터를 빠르게 검색할 수 있도록 저장하는 기술입니다. PHP에서는 Memcached, Redis, APC 등의 캐싱 시스템을 사용할 수 있습니다.

예제: Memcached

// 연결 생성
$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

// 연결 생성
$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);

12. 로깅

개념

로깅은 시스템 작동 방식을 모니터링하고 문제를 해결하는 데 중요합니다. 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();  // 로그 저장

13. 보안

개념

보안은 게임 서버에서 중요한 측면입니다. 입력 검증, 암호화, 인증 등을 통해 보안을 강화할 수 있습니다.

예제: 입력 검증

// 입력 검증
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";

14. PHP 8 신규 기능

개념

PHP 8은 이전 버전에 비해 많은 새로운 기능과 개선 사항을 도입했습니다. 이러한 기능들은 코드를 더 간결하고 안전하게 작성하는 데 도움이 됩니다.

주요 기능

1. 명명된 인수

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]'
);

2. 생성자 속성 승격

// 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;
    }
}

3. Match 표현식

// 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'
};

4. Null-safe 연산자

// PHP 8 이전
$country = null;
if ($user !== null) {
    $address = $user->getAddress();
    if ($address !== null) {
        $country = $address->getCountry();
    }
}

// PHP 8
$country = $user?->getAddress()?->getCountry();

5. 유니온 타입

// PHP 8
function process(string|array $data): int|float {
    if (is_array($data)) {
        return count($data);
    }
    return strlen($data);
}

6. 속성(Attributes)

// 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을 배우는 데 도움이 되길 바랍니다. 각 섹션을 순서대로 학습하고 예제와 연습 문제를 통해 실습해보세요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment