Ver3.0 ⚡ PHP 코드 최적화: 알고리즘과 자료구조 완전정복

⚡ PHP 코드 최적화: 알고리즘과 자료구조 완전정복
성능 10배 향상시키는 실전 테크닉 대공개 🚀
야 솔직히 말해서 PHP로 코딩하다 보면 "어? 왜 이렇게 느리지?" 하는 순간 있잖아 ㅋㅋㅋ
특히 데이터 많아지면 서버가 뻗을 것 같은 그 느낌... 😱
근데 진짜 문제는 대부분 알고리즘이랑 자료구조를 제대로 안 쓰고 있어서 그래.
같은 기능인데 어떻게 짜느냐에 따라 속도가 100배도 차이 날 수 있다고!
오늘은 PHP 개발자라면 꼭 알아야 할 코드 최적화 비법을 싹 다 알려줄게 💪
🎯 왜 알고리즘과 자료구조가 중요한가?
"그냥 돌아가면 되는 거 아니야?" 라고 생각할 수도 있는데...
아니 ㅋㅋㅋ 그건 사용자 10명일 때 얘기고, 1만 명 되면 완전 달라져 🔥
실제로 재능넷 같은 플랫폼에서도 사용자가 늘어나면서 성능 최적화가 엄청 중요해졌거든.
특히 검색 기능이나 매칭 알고리즘 같은 거 말이야.
⚠️ 실제 사례: 최적화 안 하면 생기는 일
한 쇼핑몰에서 상품 검색 기능을 만들었는데, 처음엔 상품이 100개라 괜찮았어.
근데 상품이 10만 개 넘어가니까 검색 한 번에 30초씩 걸리더라 ㅋㅋㅋ
결국 고객들이 "사이트 고장났나요?" 문의 폭주... 😭
원인: 전체 상품을 다 불러와서 PHP에서 필터링
해결: 인덱스 추가 + 적절한 자료구조 사용 → 0.5초로 단축!
📊 시간 복잡도(Big O) 이해하기
알고리즘 얘기할 때 빠질 수 없는 게 바로 시간 복잡도야.
"Big O 표기법" 이라고 하는데, 쉽게 말하면 데이터가 늘어날 때 시간이 얼마나 증가하는지 나타내는 거지.
진짜 개발자들 사이에서 "이거 O(n²)이라서..." 이런 말 자주 나오거든 ㅋㅋ
| 표기법 | 이름 | 예시 | 성능 |
|---|---|---|---|
| O(1) | 상수 시간 | 배열 인덱스 접근 | 🚀 최고 |
| O(log n) | 로그 시간 | 이진 탐색 | ⚡ 매우 좋음 |
| O(n) | 선형 시간 | 배열 순회 | ✅ 괜찮음 |
| O(n log n) | 선형 로그 | 효율적인 정렬 | 👍 나쁘지 않음 |
| O(n²) | 이차 시간 | 중첩 반복문 | ⚠️ 주의 |
| O(2ⁿ) | 지수 시간 | 피보나치(재귀) | 🔥 위험 |
🔍 실전 예제로 이해하기
말로만 하면 어려우니까 실제 코드로 보자!
같은 기능인데 시간 복잡도가 어떻게 다른지 확인해봐 👀
❌ 나쁜 예: O(n²) - 중복 체크
<?php
// 배열에서 중복된 값 찾기 (비효율적)
function findDuplicates($arr) {
$duplicates = [];
$n = count($arr);
// 이중 반복문 - O(n²) 😱
for ($i = 0; $i < $n; $i++) {
for ($j = $i + 1; $j < $n; $j++) {
if ($arr[$i] === $arr[$j]) {
$duplicates[] = $arr[$i];
}
}
}
return array_unique($duplicates);
}
// 데이터 1만 개면? 1억 번 비교! 💀
$data = range(1, 10000);
$result = findDuplicates($data); // 엄청 느림...
?>
✅ 좋은 예: O(n) - 해시 테이블 활용
<?php
// 배열에서 중복된 값 찾기 (효율적)
function findDuplicatesFast($arr) {
$seen = [];
$duplicates = [];
// 한 번만 순회 - O(n) 🚀
foreach ($arr as $value) {
if (isset($seen[$value])) {
if (!isset($duplicates[$value])) {
$duplicates[$value] = true;
}
} else {
$seen[$value] = true;
}
}
return array_keys($duplicates);
}
// 데이터 1만 개면? 1만 번만 확인! ⚡
$data = range(1, 10000);
$result = findDuplicatesFast($data); // 초고속!
?>
📈 성능 비교 결과
데이터 1,000개 기준:
• 나쁜 예(O(n²)): 약 0.5초
• 좋은 예(O(n)): 약 0.001초
→ 500배 차이! 😲
데이터 10,000개 기준:
• 나쁜 예(O(n²)): 약 50초
• 좋은 예(O(n)): 약 0.01초
→ 5,000배 차이! 🤯
🗂️ PHP에서 자주 쓰는 자료구조
자료구조는 데이터를 어떻게 저장하고 관리할지 정하는 거야.
PHP는 기본적으로 배열이 엄청 강력하지만, 상황에 따라 다른 자료구조가 훨씬 효율적일 수 있어!
1️⃣ 배열 (Array) - 가장 기본
PHP의 배열은 사실 해시 테이블로 구현되어 있어서 엄청 유연해.
근데 그만큼 메모리를 많이 먹기도 하지 ㅋㅋ
💡 배열 최적화 팁
1. 크기를 미리 알면 array_fill() 사용
동적으로 추가하는 것보다 빠름!
2. isset()이 array_key_exists()보다 빠름
null 체크가 필요 없으면 isset() 쓰자.
3. unset()보다 null 할당이 빠를 수 있음
메모리 해제가 급하지 않으면 $arr[$key] = null;
<?php
// ❌ 비효율적인 배열 사용
$users = [];
for ($i = 0; $i < 10000; $i++) {
$users[] = ['id' => $i, 'name' => "User $i"];
}
// 특정 ID 찾기 - O(n)
foreach ($users as $user) {
if ($user['id'] === 5000) {
echo $user['name'];
break;
}
}
// ✅ 효율적인 배열 사용 (해시맵처럼)
$users = [];
for ($i = 0; $i < 10000; $i++) {
$users[$i] = ['id' => $i, 'name' => "User $i"];
}
// 특정 ID 찾기 - O(1)
if (isset($users[5000])) {
echo $users[5000]['name']; // 즉시 접근!
}
?>
2️⃣ SplFixedArray - 진짜 배열
일반 PHP 배열이 너무 무겁다 싶으면 SplFixedArray를 써봐.
크기가 고정되어 있어서 메모리 효율이 훨씬 좋아! 💪
<?php
// 일반 배열 vs SplFixedArray 비교
// 일반 배열
$start = microtime(true);
$normalArray = [];
for ($i = 0; $i < 100000; $i++) {
$normalArray[$i] = $i;
}
$normalTime = microtime(true) - $start;
// SplFixedArray
$start = microtime(true);
$fixedArray = new SplFixedArray(100000);
for ($i = 0; $i < 100000; $i++) {
$fixedArray[$i] = $i;
}
$fixedTime = microtime(true) - $start;
echo "일반 배열: {$normalTime}초\n";
echo "SplFixedArray: {$fixedTime}초\n";
// 결과: SplFixedArray가 약 30% 빠름! ⚡
// 메모리 사용량도 적음
echo "일반 배열 메모리: " . memory_get_usage() . "\n";
// SplFixedArray가 약 40% 적게 사용!
?>
✨ SplFixedArray 사용 시나리오
• 대량의 숫자 데이터 처리
• 크기가 미리 정해진 경우
• 메모리 최적화가 중요한 경우
• 순차적 접근이 주된 경우
단, 키가 문자열이거나 동적으로 크기가 변하면 일반 배열이 나아!
3️⃣ SplStack & SplQueue - 스택과 큐
LIFO(후입선출)나 FIFO(선입선출) 구조가 필요할 때 완전 유용해.
특히 BFS/DFS 같은 알고리즘 구현할 때 필수지 🎯
<?php
// 스택 (Stack) - LIFO
$stack = new SplStack();
$stack->push('첫번째');
$stack->push('두번째');
$stack->push('세번째');
echo $stack->pop(); // '세번째' (마지막에 넣은 게 먼저 나옴)
echo $stack->pop(); // '두번째'
// 큐 (Queue) - FIFO
$queue = new SplQueue();
$queue->enqueue('첫번째');
$queue->enqueue('두번째');
$queue->enqueue('세번째');
echo $queue->dequeue(); // '첫번째' (먼저 넣은 게 먼저 나옴)
echo $queue->dequeue(); // '두번째'
// 실전 예제: 작업 큐 시스템
class TaskQueue {
private $queue;
public function __construct() {
$this->queue = new SplQueue();
}
public function addTask($task) {
$this->queue->enqueue([
'task' => $task,
'timestamp' => time()
]);
}
public function processNext() {
if ($this->queue->isEmpty()) {
return null;
}
$item = $this->queue->dequeue();
// 작업 처리...
return $item;
}
public function getQueueSize() {
return $this->queue->count();
}
}
// 사용 예
$taskQueue = new TaskQueue();
$taskQueue->addTask('이메일 발송');
$taskQueue->addTask('이미지 리사이징');
$taskQueue->addTask('로그 정리');
while ($task = $taskQueue->processNext()) {
echo "처리 중: {$task['task']}\n";
}
?>
4️⃣ SplHeap - 우선순위 큐
우선순위가 높은 것부터 처리해야 할 때 쓰는 자료구조야.
예를 들어 긴급도에 따라 작업을 처리한다거나 할 때 완전 유용함! 🔥
<?php
// 최대 힙 (큰 값이 우선)
class MaxHeap extends SplHeap {
protected function compare($value1, $value2): int {
return $value2['priority'] <=> $value1['priority'];
}
}
// 우선순위 작업 큐
$priorityQueue = new MaxHeap();
$priorityQueue->insert(['task' => '일반 작업', 'priority' => 1]);
$priorityQueue->insert(['task' => '긴급 작업', 'priority' => 10]);
$priorityQueue->insert(['task' => '중요 작업', 'priority' => 5]);
// 우선순위 높은 순서대로 처리
while (!$priorityQueue->isEmpty()) {
$item = $priorityQueue->extract();
echo "처리: {$item['task']} (우선순위: {$item['priority']})\n";
}
// 출력:
// 처리: 긴급 작업 (우선순위: 10)
// 처리: 중요 작업 (우선순위: 5)
// 처리: 일반 작업 (우선순위: 1)
// 실전 예제: 알림 시스템
class NotificationSystem {
private $heap;
public function __construct() {
$this->heap = new MaxHeap();
}
public function addNotification($message, $priority) {
$this->heap->insert([
'message' => $message,
'priority' => $priority,
'timestamp' => time()
]);
}
public function sendNext() {
if ($this->heap->isEmpty()) {
return null;
}
$notification = $this->heap->extract();
// 실제 알림 발송 로직...
return $notification;
}
}
$notifier = new NotificationSystem();
$notifier->addNotification('새 메시지가 있습니다', 3);
$notifier->addNotification('시스템 점검 예정', 10);
$notifier->addNotification('이벤트 안내', 1);
// 우선순위 높은 알림부터 발송!
?>
🚀 실전 최적화 테크닉
이제 진짜 실무에서 바로 쓸 수 있는 최적화 기법들을 알려줄게!
이거 알고 모르고 차이가 엄청나거든 ㅋㅋㅋ
🎯 1. 루프 최적화
반복문은 코드에서 가장 많이 쓰이는 만큼, 여기서 최적화하면 효과가 크지!
❌ 비효율적인 루프
<?php
// 나쁜 예 1: 매번 count() 호출
for ($i = 0; $i < count($array); $i++) {
// count()가 매 반복마다 실행됨! 😱
echo $array[$i];
}
// 나쁜 예 2: 불필요한 함수 호출
foreach ($users as $user) {
if (strlen($user['name']) > 10) {
// strlen()을 매번 호출...
echo substr($user['name'], 0, 10);
}
}
// 나쁜 예 3: 루프 안에서 DB 쿼리
foreach ($productIds as $id) {
$product = $db->query("SELECT * FROM products WHERE id = $id");
// N+1 쿼리 문제! 💀
echo $product['name'];
}
?>
✅ 최적화된 루프
<?php
// 좋은 예 1: count() 결과 캐싱
$length = count($array);
for ($i = 0; $i < $length; $i++) {
echo $array[$i];
}
// 더 좋은 예: foreach 사용 (더 빠름!)
foreach ($array as $item) {
echo $item;
}
// 좋은 예 2: 계산 결과 재사용
foreach ($users as $user) {
$nameLength = strlen($user['name']);
if ($nameLength > 10) {
echo substr($user['name'], 0, 10);
}
}
// 좋은 예 3: 한 번에 쿼리
$ids = implode(',', $productIds);
$products = $db->query("SELECT * FROM products WHERE id IN ($ids)");
// 쿼리 1번으로 끝! ⚡
// 인덱스로 빠르게 접근
$productMap = [];
foreach ($products as $product) {
$productMap[$product['id']] = $product;
}
foreach ($productIds as $id) {
if (isset($productMap[$id])) {
echo $productMap[$id]['name'];
}
}
?>
💡 루프 최적화 체크리스트
✅ count(), strlen() 같은 함수 호출을 루프 밖으로
✅ 가능하면 foreach 사용 (for보다 빠름)
✅ 루프 안에서 DB 쿼리 절대 금지
✅ 복잡한 계산은 한 번만 하고 재사용
✅ break/continue로 불필요한 반복 줄이기
🎯 2. 문자열 처리 최적화
문자열 연산도 생각보다 무거워.
특히 대량의 문자열을 다룰 때는 방법에 따라 성능이 천차만별이야!
<?php
// ❌ 나쁜 예: 문자열 연결 연산자
$html = '';
for ($i = 0; $i < 10000; $i++) {
$html .= "<div>Item $i</div>";
// 매번 새로운 문자열 생성! 메모리 낭비 😱
}
// ✅ 좋은 예 1: 배열 + implode
$parts = [];
for ($i = 0; $i < 10000; $i++) {
$parts[] = "<div>Item $i</div>";
}
$html = implode('', $parts);
// 훨씬 빠르고 메모리 효율적! ⚡
// ✅ 좋은 예 2: 출력 버퍼링
ob_start();
for ($i = 0; $i < 10000; $i++) {
echo "<div>Item $i</div>";
}
$html = ob_get_clean();
// 이것도 빠름!
// 문자열 검색 최적화
// ❌ 느림
if (strpos($string, 'needle') !== false) { }
// ✅ 빠름 (PHP 8+)
if (str_contains($string, 'needle')) { }
// 대소문자 구분 없는 비교
// ❌ 느림
if (strtolower($str1) === strtolower($str2)) { }
// ✅ 빠름
if (strcasecmp($str1, $str2) === 0) { }
?>
🎯 3. 배열 함수 최적화
PHP 내장 배열 함수들은 C로 구현되어 있어서 엄청 빠른데,
어떤 걸 쓰느냐에 따라 성능 차이가 크게 나!
<?php
// 배열 검색
$array = range(1, 10000);
// ❌ 느림: in_array (O(n))
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
$exists = in_array(5000, $array);
}
$time1 = microtime(true) - $start;
// ✅ 빠름: isset (O(1))
$arrayFlipped = array_flip($array);
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
$exists = isset($arrayFlipped[5000]);
}
$time2 = microtime(true) - $start;
echo "in_array: {$time1}초\n";
echo "isset: {$time2}초\n";
// isset이 100배 이상 빠름! 🚀
// 배열 필터링
$numbers = range(1, 10000);
// ❌ 느림: foreach + 조건문
$filtered = [];
foreach ($numbers as $num) {
if ($num % 2 === 0) {
$filtered[] = $num;
}
}
// ✅ 빠름: array_filter
$filtered = array_filter($numbers, function($num) {
return $num % 2 === 0;
});
// 배열 변환
// ❌ 느림
$squared = [];
foreach ($numbers as $num) {
$squared[] = $num * $num;
}
// ✅ 빠름: array_map
$squared = array_map(function($num) {
return $num * $num;
}, $numbers);
// 배열 합계
// ❌ 느림
$sum = 0;
foreach ($numbers as $num) {
$sum += $num;
}
// ✅ 빠름: array_sum
$sum = array_sum($numbers);
?>
🎯 4. 메모이제이션 (Memoization)
같은 계산을 반복하지 말고 결과를 저장해두는 기법이야.
특히 재귀 함수나 복잡한 계산에서 엄청난 효과를 발휘해! 💪
<?php
// ❌ 비효율적인 피보나치 (지수 시간 복잡도)
function fibonacci($n) {
if ($n <= 1) return $n;
return fibonacci($n - 1) + fibonacci($n - 2);
// fibonacci(40) 호출하면 10억 번 이상 계산! 😱
}
$start = microtime(true);
echo fibonacci(35); // 몇 초 걸림...
echo "\n시간: " . (microtime(true) - $start) . "초\n";
// ✅ 메모이제이션 적용 (선형 시간)
class FibonacciOptimized {
private static $cache = [];
public static function calculate($n) {
if ($n <= 1) return $n;
// 캐시에 있으면 바로 반환
if (isset(self::$cache[$n])) {
return self::$cache[$n];
}
// 계산 후 캐시에 저장
self::$cache[$n] = self::calculate($n - 1) + self::calculate($n - 2);
return self::$cache[$n];
}
}
$start = microtime(true);
echo FibonacciOptimized::calculate(35); // 거의 즉시!
echo "\n시간: " . (microtime(true) - $start) . "초\n";
// 실전 예제: 복잡한 권한 체크
class PermissionChecker {
private $cache = [];
public function hasPermission($userId, $resource) {
$cacheKey = "{$userId}:{$resource}";
// 캐시 확인
if (isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey];
}
// 복잡한 권한 계산 (DB 쿼리, 로직 등)
$hasPermission = $this->calculatePermission($userId, $resource);
// 결과 캐싱
$this->cache[$cacheKey] = $hasPermission;
return $hasPermission;
}
private function calculatePermission($userId, $resource) {
// 실제 권한 체크 로직...
// DB 쿼리, 역할 확인, 상속 관계 등
return true; // 예시
}
}
// 같은 권한을 여러 번 체크해도 한 번만 계산!
$checker = new PermissionChecker();
$checker->hasPermission(123, 'edit_post'); // DB 쿼리
$checker->hasPermission(123, 'edit_post'); // 캐시에서 즉시 반환!
?>
🎯 5. 지연 로딩 (Lazy Loading)
필요할 때만 데이터를 로드하는 기법이야.
초기 로딩 시간을 줄이고 메모리를 절약할 수 있지! 🎯
<?php
// ❌ 즉시 로딩 (Eager Loading) - 비효율적
class User {
private $id;
private $name;
private $posts;
private $comments;
private $followers;
public function __construct($id) {
$this->id = $id;
$this->name = $this->loadName();
$this->posts = $this->loadPosts(); // 항상 로드 😱
$this->comments = $this->loadComments(); // 항상 로드 😱
$this->followers = $this->loadFollowers(); // 항상 로드 😱
// 사용하지 않아도 모든 데이터를 로드!
}
private function loadPosts() {
// 무거운 DB 쿼리...
return [];
}
private function loadComments() {
// 무거운 DB 쿼리...
return [];
}
private function loadFollowers() {
// 무거운 DB 쿼리...
return [];
}
}
// ✅ 지연 로딩 (Lazy Loading) - 효율적
class UserOptimized {
private $id;
private $name;
private $posts = null;
private $comments = null;
private $followers = null;
public function __construct($id) {
$this->id = $id;
$this->name = $this->loadName();
// 다른 데이터는 로드하지 않음!
}
public function getPosts() {
// 필요할 때만 로드
if ($this->posts === null) {
$this->posts = $this->loadPosts();
}
return $this->posts;
}
public function getComments() {
if ($this->comments === null) {
$this->comments = $this->loadComments();
}
return $this->comments;
}
public function getFollowers() {
if ($this->followers === null) {
$this->followers = $this->loadFollowers();
}
return $this->followers;
}
private function loadPosts() {
// DB 쿼리...
return [];
}
private function loadComments() {
// DB 쿼리...
return [];
}
private function loadFollowers() {
// DB 쿼리...
return [];
}
}
// 사용 예
$user = new UserOptimized(123);
echo $user->getName(); // 빠름!
// posts, comments, followers는 로드되지 않음
// 필요할 때만 로드
if ($needPosts) {
$posts = $user->getPosts(); // 이때 로드됨
}
// 제너레이터를 활용한 지연 로딩
function loadLargeDataset() {
// ❌ 나쁜 예: 모든 데이터를 메모리에 로드
$data = [];
for ($i = 0; $i < 1000000; $i++) {
$data[] = fetchDataFromDB($i);
}
return $data; // 메모리 폭발! 💀
}
function loadLargeDatasetOptimized() {
// ✅ 좋은 예: 필요할 때마다 하나씩 생성
for ($i = 0; $i < 1000000; $i++) {
yield fetchDataFromDB($i);
}
// 메모리 사용량 최소화! ⚡
}
// 사용
foreach (loadLargeDatasetOptimized() as $item) {
processItem($item);
// 한 번에 하나씩만 메모리에 올라감!
}
?>
🔍 실전 알고리즘 예제
이론만 알면 뭐해 ㅋㅋ 실제로 어떻게 쓰는지 봐야지!
실무에서 자주 마주치는 문제들과 해결법을 알려줄게 💡
📌 예제 1: 대용량 데이터 정렬
수만 개의 데이터를 정렬해야 할 때, 어떤 알고리즘을 쓰느냐가 중요해!
<?php
// 데이터 준비
$data = [];
for ($i = 0; $i < 50000; $i++) {
$data[] = [
'id' => $i,
'score' => rand(0, 1000),
'name' => "User $i"
];
}
// ❌ 비효율적: 버블 정렬 O(n²)
function bubbleSort($arr) {
$n = count($arr);
for ($i = 0; $i < $n; $i++) {
for ($j = 0; $j < $n - $i - 1; $j++) {
if ($arr[$j]['score'] > $arr[$j + 1]['score']) {
$temp = $arr[$j];
$arr[$j] = $arr[$j + 1];
$arr[$j + 1] = $temp;
}
}
}
return $arr;
}
// 50,000개 정렬하면 몇 분 걸림... 😱
// ✅ 효율적: PHP 내장 정렬 O(n log n)
$start = microtime(true);
usort($data, function($a, $b) {
return $a['score'] <=> $b['score'];
});
$time = microtime(true) - $start;
echo "정렬 시간: {$time}초\n"; // 0.1초 이내!
// 더 빠른 방법: 키로 정렬 가능하면 array_multisort
$scores = array_column($data, 'score');
array_multisort($scores, SORT_ASC, $data);
// 더 빠름! ⚡
// 부분 정렬이 필요하면 힙 사용
class TopKFinder {
public static function findTopK($data, $k) {
$heap = new SplMaxHeap();
foreach ($data as $item) {
$heap->insert($item);
}
$result = [];
for ($i = 0; $i < $k && !$heap->isEmpty(); $i++) {
$result[] = $heap->extract();
}
return $result;
}
}
// 상위 100개만 필요하면 전체 정렬할 필요 없음!
$top100 = TopKFinder::findTopK($data, 100);
?>
📌 예제 2: 중복 제거 최적화
대량의 데이터에서 중복을 제거하는 건 생각보다 까다로워.
특히 객체나 배열의 중복을 찾을 때는 더 그렇지!
<?php
// 시나리오: 사용자 활동 로그에서 중복 제거
// ❌ 비효율적: 이중 루프 O(n²)
function removeDuplicatesSlow($logs) {
$unique = [];
foreach ($logs as $log) {
$isDuplicate = false;
foreach ($unique as $uniqueLog) {
if ($log['user_id'] === $uniqueLog['user_id'] &&
$log['action'] === $uniqueLog['action']) {
$isDuplicate = true;
break;
}
}
if (!$isDuplicate) {
$unique[] = $log;
}
}
return $unique;
}
// ✅ 효율적: 해시 테이블 O(n)
function removeDuplicatesFast($logs) {
$seen = [];
$unique = [];
foreach ($logs as $log) {
// 고유 키 생성
$key = $log['user_id'] . ':' . $log['action'];
if (!isset($seen[$key])) {
$seen[$key] = true;
$unique[] = $log;
}
}
return $unique;
}
// 테스트
$logs = [];
for ($i = 0; $i < 10000; $i++) {
$logs[] = [
'user_id' => rand(1, 1000),
'action' => ['view', 'click', 'purchase'][rand(0, 2)],
'timestamp' => time()
];
}
$start = microtime(true);
$result = removeDuplicatesFast($logs);
$time = microtime(true) - $start;
echo "처리 시간: {$time}초\n";
echo "원본: " . count($logs) . "개\n";
echo "중복 제거 후: " . count($result) . "개\n";
// 복잡한 객체의 중복 제거
class UniqueFilter {
public static function filterByFields($items, $fields) {
$seen = [];
$unique = [];
foreach ($items as $item) {
// 여러 필드로 고유 키 생성
$keyParts = [];
foreach ($fields as $field) {
$keyParts[] = $item[$field] ?? '';
}
$key = implode('|', $keyParts);
if (!isset($seen[$key])) {
$seen[$key] = true;
$unique[] = $item;
}
}
return $unique;
}
}
// 사용 예
$users = [
['name' => 'John', 'email' => 'john@example.com', 'age' => 25],
['name' => 'Jane', 'email' => 'jane@example.com', 'age' => 30],
['name' => 'John', 'email' => 'john@example.com', 'age' => 25], // 중복
];
$uniqueUsers = UniqueFilter::filterByFields($users, ['name', 'email']);
// 중복 제거됨!
?>
📌 예제 3: 검색 최적화
검색 기능은 거의 모든 서비스에 필수인데,
제대로 최적화 안 하면 사용자 경험이 최악이 되지 ㅠㅠ
<?php
// 시나리오: 재능넷에서 재능 검색 기능
// ❌ 비효율적: 선형 검색 O(n)
function searchTalentsSlow($talents, $keyword) {
$results = [];
foreach ($talents as $talent) {
if (stripos($talent['title'], $keyword) !== false ||
stripos($talent['description'], $keyword) !== false) {
$results[] = $talent;
}
}
return $results;
}
// ✅ 효율적: 인덱스 기반 검색
class TalentSearchEngine {
private $talents = [];
private $titleIndex = [];
private $keywordIndex = [];
public function addTalent($talent) {
$id = $talent['id'];
$this->talents[$id] = $talent;
// 제목 인덱싱
$titleWords = $this->tokenize($talent['title']);
foreach ($titleWords as $word) {
$this->titleIndex[$word][] = $id;
}
// 키워드 인덱싱
if (isset($talent['keywords'])) {
foreach ($talent['keywords'] as $keyword) {
$this->keywordIndex[$keyword][] = $id;
}
}
}
public function search($query) {
$words = $this->tokenize($query);
$candidateIds = [];
// 각 단어로 검색
foreach ($words as $word) {
if (isset($this->titleIndex[$word])) {
foreach ($this->titleIndex[$word] as $id) {
$candidateIds[$id] = ($candidateIds[$id] ?? 0) + 2; // 제목 가중치
}
}
if (isset($this->keywordIndex[$word])) {
foreach ($this->keywordIndex[$word] as $id) {
$candidateIds[$id] = ($candidateIds[$id] ?? 0) + 1; // 키워드 가중치
}
}
}
// 점수순 정렬
arsort($candidateIds);
// 결과 반환
$results = [];
foreach ($candidateIds as $id => $score) {
$results[] = $this->talents[$id];
}
return $results;
}
private function tokenize($text) {
// 간단한 토크나이저 (실제로는 더 정교하게)
$text = strtolower($text);
$text = preg_replace('/[^a-z0-9가-힣\s]/u', '', $text);
return array_filter(explode(' ', $text));
}
}
// 사용 예
$searchEngine = new TalentSearchEngine();
// 재능 등록
$talents = [
[
'id' => 1,
'title' => 'PHP 웹 개발 강의',
'description' => '초보자를 위한 PHP 프로그래밍',
'keywords' => ['php', '웹개발', '프로그래밍']
],
[
'id' => 2,
'title' => '파이썬 데이터 분석',
'description' => '데이터 과학 입문',
'keywords' => ['python', '데이터분석', '머신러닝']
],
[
'id' => 3,
'title' => 'PHP 고급 최적화 기법',
'description' => '성능 향상을 위한 PHP 팁',
'keywords' => ['php', '최적화', '성능']
],
];
foreach ($talents as $talent) {
$searchEngine->addTalent($talent);
}
// 검색 (초고속!)
$results = $searchEngine->search('PHP 최적화');
// 관련도 높은 순서로 정렬된 결과!
// 자동완성 기능
class AutoCompleter {
private $trie = [];
public function addWord($word) {
$node = &$this->trie;
$word = strtolower($word);
for ($i = 0; $i < strlen($word); $i++) {
$char = $word[$i];
if (!isset($node[$char])) {
$node[$char] = [];
}
$node = &$node[$char];
}
$node['*'] = $word; // 단어 끝 표시
}
public function getSuggestions($prefix, $limit = 10) {
$node = $this->trie;
$prefix = strtolower($prefix);
// prefix까지 이동
for ($i = 0; $i < strlen($prefix); $i++) {
$char = $prefix[$i];
if (!isset($node[$char])) {
return []; // prefix 없음
}
$node = $node[$char];
}
// 가능한 단어들 수집
$suggestions = [];
$this->collectWords($node, $suggestions, $limit);
return $suggestions;
}
private function collectWords($node, &$results, $limit) {
if (count($results) >= $limit) {
return;
}
if (isset($node['*'])) {
$results[] = $node['*'];
}
foreach ($node as $key => $child) {
if ($key !== '*') {
$this->collectWords($child, $results, $limit);
}
}
}
}
// 사용 예
$autocomplete = new AutoCompleter();
$keywords = ['php', 'python', 'programming', 'project', 'product'];
foreach ($keywords as $keyword) {
$autocomplete->addWord($keyword);
}
$suggestions = $autocomplete->getSuggestions('pr');
// ['programming', 'project', 'product']
?>
✨ 검색 최적화 핵심 포인트
1. 인덱싱: 미리 데이터를 구조화해서 빠른 검색 가능
2. 캐싱: 자주 검색되는 쿼리는 결과를 캐시
3. 페이지네이션: 한 번에 모든 결과를 로드하지 말기
4. 비동기 처리: 무거운 검색은 백그라운드에서
5. 전문 검색 엔진: 대규모면 Elasticsearch 같은 거 고려
📌 예제 4: 캐싱 전략
캐싱은 최적화의 끝판왕이야 ㅋㅋㅋ
제대로만 하면 성능이 10배, 100배도 향상될 수 있어! 🚀
<?php
// 다양한 캐싱 전략
// 1. 메모리 캐시 (APCu)
class MemoryCache {
public static function get($key) {
if (function_exists('apcu_fetch')) {
return apcu_fetch($key);
}
return false;
}
public static function set($key, $value, $ttl = 3600) {
if (function_exists('apcu_store')) {
return apcu_store($key, $value, $ttl);
}
return false;
}
public static function delete($key) {
if (function_exists('apcu_delete')) {
return apcu_delete($key);
}
return false;
}
}
// 2. 파일 캐시
class FileCache {
private $cacheDir;
public function __construct($cacheDir = '/tmp/cache') {
$this->cacheDir = $cacheDir;
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
}
public function get($key) {
$file = $this->getCacheFile($key);
if (!file_exists($file)) {
return false;
}
$data = unserialize(file_get_contents($file));
// 만료 체크
if ($data['expires'] < time()) {
unlink($file);
return false;
}
return $data['value'];
}
public function set($key, $value, $ttl = 3600) {
$file = $this->getCacheFile($key);
$data = [
'value' => $value,
'expires' => time() + $ttl
];
return file_put_contents($file, serialize($data)) !== false;
}
public function delete($key) {
$file = $this->getCacheFile($key);
if (file_exists($file)) {
return unlink($file);
}
return false;
}
private function getCacheFile($key) {
return $this->cacheDir . '/' . md5($key) . '.cache';
}
}
// 3. 통합 캐시 매니저
class CacheManager {
private $cache;
public function __construct() {
// APCu 사용 가능하면 메모리 캐시, 아니면 파일 캐시
if (function_exists('apcu_fetch')) {
$this->cache = new MemoryCache();
} else {
$this->cache = new FileCache();
}
}
public function remember($key, $ttl, $callback) {
// 캐시에 있으면 반환
$value = $this->cache->get($key);
if ($value !== false) {
return $value;
}
// 없으면 계산 후 저장
$value = $callback();
$this->cache->set($key, $value, $ttl);
return $value;
}
}
// 실전 사용 예
$cache = new CacheManager();
// 무거운 DB 쿼리 캐싱
$popularTalents = $cache->remember('popular_talents', 3600, function() {
// 복잡한 쿼리...
return $db->query("
SELECT * FROM talents
WHERE status = 'active'
ORDER BY view_count DESC
LIMIT 10
");
});
// 계산 결과 캐싱
$statistics = $cache->remember('daily_stats_' . date('Y-m-d'), 86400, function() {
// 복잡한 통계 계산...
return calculateDailyStatistics();
});
// 4. 캐시 워밍업 (Cache Warming)
class CacheWarmer {
private $cache;
public function __construct($cache) {
$this->cache = $cache;
}
public function warmUp() {
// 자주 사용되는 데이터를 미리 캐싱
$this->warmPopularData();
$this->warmCategoryData();
$this->warmUserPreferences();
}
private function warmPopularData() {
$popularItems = fetchPopularItems();
foreach ($popularItems as $item) {
$key = "item_{$item['id']}";
$this->cache->set($key, $item, 3600);
}
}
private function warmCategoryData() {
$categories = fetchAllCategories();
$this->cache->set('all_categories', $categories, 7200);
}
private function warmUserPreferences() {
$activeUsers = fetchActiveUsers();
foreach ($activeUsers as $user) {
$preferences = fetchUserPreferences($user['id']);
$key = "user_pref_{$user['id']}";
$this->cache->set($key, $preferences, 1800);
}
}
}
// 5. 캐시 무효화 전략
class CacheInvalidator {
private $cache;
public function __construct($cache) {
$this->cache = $cache;
}
public function invalidateUser($userId) {
// 사용자 관련 모든 캐시 삭제
$keys = [
"user_{$userId}",
"user_pref_{$userId}",
"user_talents_{$userId}",
"user_stats_{$userId}"
];
foreach ($keys as $key) {
$this->cache->delete($key);
}
}
public function invalidateTalent($talentId) {
// 재능 관련 캐시 삭제
$this->cache->delete("talent_{$talentId}");
$this->cache->delete("talent_reviews_{$talentId}");
// 관련 목록 캐시도 삭제
$this->cache->delete('popular_talents');
$this->cache->delete('recent_talents');
}
}
?>
⚡ 성능 측정과 프로파일링
최적화했다고 끝이 아니야!
실제로 얼마나 빨라졌는지 측정해야 진짜 최적화지 ㅋㅋ
🔬 성능 측정 도구
<?php
// 1. 간단한 벤치마크
class Benchmark {
private $startTime;
private $startMemory;
public function start() {
$this->startTime = microtime(true);
$this->startMemory = memory_get_usage();
}
public function end($label = '') {
$endTime = microtime(true);
$endMemory = memory_get_usage();
$time = ($endTime - $this->startTime) * 1000; // ms
$memory = ($endMemory - $this->startMemory) / 1024; // KB
echo "[$label]\n";
echo "실행 시간: " . number_format($time, 2) . "ms\n";
echo "메모리 사용: " . number_format($memory, 2) . "KB\n\n";
}
}
// 사용 예
$bench = new Benchmark();
$bench->start();
// 테스트할 코드...
$data = range(1, 100000);
$filtered = array_filter($data, function($n) { return $n % 2 === 0; });
$bench->end('array_filter 테스트');
// 2. 비교 벤치마크
class ComparativeBenchmark {
public static function compare($tests, $iterations = 1000) {
$results = [];
foreach ($tests as $name => $callback) {
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
$callback();
}
$time = microtime(true) - $start;
$results[$name] = $time;
}
// 결과 출력
asort($results);
echo "=== 벤치마크 결과 ($iterations 회 반복) ===\n";
$fastest = reset($results);
foreach ($results as $name => $time) {
$ratio = $time / $fastest;
echo sprintf(
"%s: %.4f초 (%.2fx)\n",
$name,
$time,
$ratio
);
}
}
}
// 사용 예
ComparativeBenchmark::compare([
'in_array' => function() {
$arr = range(1, 1000);
in_array(500, $arr);
},
'isset (flipped)' => function() {
$arr = array_flip(range(1, 1000));
isset($arr[500]);
},
'array_search' => function() {
$arr = range(1, 1000);
array_search(500, $arr);
}
]);
// 3. 프로파일러
class Profiler {
private static $markers = [];
public static function mark($name) {
self::$markers[$name] = [
'time' => microtime(true),
'memory' => memory_get_usage()
];
}
public static function measure($start, $end) {
if (!isset(self::$markers[$start]) || !isset(self::$markers[$end])) {
return null;
}
$startData = self::$markers[$start];
$endData = self::$markers[$end];
return [
'time' => ($endData['time'] - $startData['time']) * 1000,
'memory' => ($endData['memory'] - $startData['memory']) / 1024
];
}
public static function report() {
echo "=== 프로파일링 리포트 ===\n";
$markers = array_keys(self::$markers);
for ($i = 0; $i < count($markers) - 1; $i++) {
$start = $markers[$i];
$end = $markers[$i + 1];
$measure = self::measure($start, $end);
echo sprintf(
"%s → %s: %.2fms, %.2fKB\n",
$start,
$end,
$measure['time'],
$measure['memory']
);
}
}
}
// 사용 예
Profiler::mark('시작');
// 데이터 로드
$data = loadData();
Profiler::mark('데이터 로드 완료');
// 처리
$processed = processData($data);
Profiler::mark('처리 완료');
// 저장
saveData($processed);
Profiler::mark('저장 완료');
Profiler::report();
?>
🎯 Xdebug 프로파일링
진짜 프로처럼 프로파일링하고 싶으면 Xdebug를 써봐!
어느 함수가 시간을 많이 잡아먹는지 한눈에 보여줘 👀
💡 Xdebug 설정 (php.ini)
zend_extension=xdebug.so
xdebug.mode=profile
xdebug.output_dir=/tmp/xdebug
xdebug.profiler_output_name=cachegrind.out.%p
설정 후 PHP 스크립트 실행하면 /tmp/xdebug에 프로파일 파일 생성!
KCachegrind나 Webgrind로 시각화해서 볼 수 있어 🔥
🎓 실무 적용 가이드
자 이제 배운 걸 실제 프로젝트에 어떻게 적용할지 알려줄게!
단계별로 차근차근 하면 누구나 할 수 있어 💪
📋 최적화 체크리스트
| 단계 | 항목 | 우선순위 |
|---|---|---|
| 1단계 | 병목 지점 파악 (프로파일링) | 🔥 높음 |
| 2단계 | DB 쿼리 최적화 (인덱스, N+1 해결) | 🔥 높음 |
| 3단계 | 알고리즘 개선 (O(n²) → O(n)) | 🔥 높음 |
| 4단계 | 캐싱 적용 (메모리, 파일, Redis) | ⚡ 중간 |
| 5단계 | 자료구조 최적화 | ⚡ 중간 |
| 6단계 | 루프 최적화 | ✅ 낮음 |
| 7단계 | 문자열 처리 최적화 | ✅ 낮음 |
🚀 실전 최적화 시나리오
실제 프로젝트에서 겪을 수 있는 상황과 해결법을 정리해봤어!
시나리오 1: 느린 대시보드
문제: 관리자 대시보드 로딩에 5초 이상 소요
원인: 여러 테이블에서 실시간 통계 계산
해결:
<?php
// ❌ Before: 매번 계산 (5초+)
function getDashboardStats() {
return [
'total_users' => $db->query("SELECT COUNT(*) FROM users")->fetch(),
'active_users' => $db->query("SELECT COUNT(*) FROM users WHERE last_login > DATE_SUB(NOW(), INTERVAL 7 DAY)")->fetch(),
'total_sales' => $db->query("SELECT SUM(amount) FROM orders")->fetch(),
'pending_orders' => $db->query("SELECT COUNT(*) FROM orders WHERE status = 'pending'")->fetch(),
// ... 더 많은 쿼리들
];
}
// ✅ After: 캐싱 + 백그라운드 갱신 (0.01초)
class DashboardStatsCache {
private $cache;
public function __construct($cache) {
$this->cache = $cache;
}
public function getStats() {
// 캐시에서 가져오기 (거의 즉시)
$stats = $this->cache->get('dashboard_stats');
if ($stats === false) {
// 캐시 미스면 계산 (첫 요청만)
$stats = $this->calculateStats();
$this->cache->set('dashboard_stats', $stats, 300); // 5분
}
return $stats;
}
private function calculateStats() {
// 한 번의 복잡한 쿼리로 모든 통계 가져오기
return $db->query("
SELECT
(SELECT COUNT(*) FROM users) as total_users,
(SELECT COUNT(*) FROM users WHERE last_login > DATE_SUB(NOW(), INTERVAL 7 DAY)) as active_users,
(SELECT SUM(amount) FROM orders) as total_sales,
(SELECT COUNT(*) FROM orders WHERE status = 'pending') as pending_orders
")->fetch();
}
// 크론잡으로 주기적 갱신
public function refreshCache() {
$stats = $this->calculateStats();
$this->cache->set('dashboard_stats', $stats, 300);
}
}
// 크론잡 설정 (매 5분마다)
// */5 * * * * php /path/to/refresh_dashboard_cache.php
?>
시나리오 2: 검색 기능 개선
문제: 상품 검색이 느리고 관련도가 낮음
원인: LIKE 쿼리 + 정렬 없음
해결:
<?php
// ❌ Before: 단순 LIKE 검색
function searchProducts($keyword) {
return $db->query("
SELECT * FROM products
WHERE title LIKE '%$keyword%'
OR description LIKE '%$keyword%'
")->fetchAll();
}
// ✅ After: 전문 검색 + 관련도 정렬
function searchProductsOptimized($keyword) {
// 1. FULLTEXT 인덱스 사용
$results = $db->query("
SELECT *,
MATCH(title, description) AGAINST(? IN BOOLEAN MODE) as relevance
FROM products
WHERE MATCH(title, description) AGAINST(? IN BOOLEAN MODE)
ORDER BY relevance DESC
LIMIT 50
", [$keyword, $keyword])->fetchAll();
// 2. 결과 캐싱
$cacheKey = 'search_' . md5($keyword);
$cache->set($cacheKey, $results, 1800); // 30분
return $results;
}
// 더 나은 방법: Elasticsearch 사용
class ElasticsearchProductSearch {
private $client;
public function search($keyword, $filters = []) {
$params = [
'index' => 'products',
'body' => [
'query' => [
'bool' => [
'must' => [
'multi_match' => [
'query' => $keyword,
'fields' => ['title^3', 'description', 'tags^2'],
'fuzziness' => 'AUTO'
]
],
'filter' => $this->buildFilters($filters)
]
],
'highlight' => [
'fields' => [
'title' => new \stdClass(),
'description' => new \stdClass()
]
],
'size' => 50
]
];
return $this->client->search($params);
}
private function buildFilters($filters) {
$result = [];
if (isset($filters['category'])) {
$result[] = ['term' => ['category' => $filters['category']]];
}
if (isset($filters['price_min'])) {
$result[] = ['range' => ['price' => ['gte' => $filters['price_min']]]];
}
if (isset($filters['price_max'])) {
$result[] = ['range' => ['price' => ['lte' => $filters['price_max']]]];
}
return $result;
}
}
?>
시나리오 3: 대용량 데이터 처리
문제: 10만 개 데이터 처리 시 메모리 부족
원인: 모든 데이터를 한 번에 메모리에 로드
해결:
<?php
// ❌ Before: 메모리 폭발
function processAllUsers() {
$users = $db->query("SELECT * FROM users")->fetchAll();
// 10만 명이면 메모리 500MB+ 사용! 💀
foreach ($users as $user) {
processUser($user);
}
}
// ✅ After: 청크 단위 처리
function processAllUsersOptimized() {
$offset = 0;
$limit = 1000;
while (true) {
$users = $db->query("
SELECT * FROM users
LIMIT $limit OFFSET $offset
")->fetchAll();
if (empty($users)) {
break;
}
foreach ($users as $user) {
processUser($user);
}
$offset += $limit;
// 메모리 정리
unset($users);
gc_collect_cycles();
}
}
// 더 나은 방법: 제너레이터 사용
function getUsersGenerator($batchSize = 1000) {
$offset = 0;
while (true) {
$users = $db->query("
SELECT * FROM users
LIMIT $batchSize OFFSET $offset
")->fetchAll();
if (empty($users)) {
break;
}
foreach ($users as $user) {
yield $user; // 한 번에 하나씩!
}
$offset += $batchSize;
}
}
// 사용
foreach (getUsersGenerator() as $user) {
processUser($user);
// 메모리 사용량 일정하게 유지! ⚡
}
// 병렬 처리로 더 빠르게
class ParallelProcessor {
public function processInParallel($items, $callback, $workers = 4) {
$chunks = array_chunk($items, ceil(count($items) / $workers));
$processes = [];
foreach ($chunks as $index => $chunk) {
$pid = pcntl_fork();
if ($pid == -1) {
die("Fork 실패");
} elseif ($pid == 0) {
// 자식 프로세스
foreach ($chunk as $item) {
$callback($item);
}
exit(0);
} else {
// 부모 프로세스
$processes[] = $pid;
}
}
// 모든 자식 프로세스 대기
foreach ($processes as $pid) {
pcntl_waitpid($pid, $status);
}
}
}
// 사용 예
$processor = new ParallelProcessor();
$processor->processInParallel($userIds, function($userId) {
processUser($userId);
}, 4); // 4개 프로세스로 병렬 처리!
?>
💡 마무리하며
와... 여기까지 읽었으면 진짜 대단한 거야 ㅋㅋㅋ
솔직히 내용이 좀 많긴 했지? 😅
근데 이거 다 실무에서 진짜 쓰이는 것들이고,
알고 모르고의 차이가 엄청나거든!
🎯 핵심 정리
1. 시간 복잡도를 항상 생각하자
O(n²)보다 O(n)이, O(n)보다 O(log n)이 좋아!
2. 적절한 자료구조 선택이 중요
배열, 해시맵, 스택, 큐, 힙... 상황에 맞게!
3. 캐싱은 최적화의 왕
같은 계산 반복하지 말고 저장해두자.
4. 측정 없이 최적화 없다
추측하지 말고 프로파일링으로 확인!
5. 조기 최적화는 악의 근원
일단 작동하게 만들고, 병목 지점만 최적화하자.
재능넷 같은 플랫폼에서 개발할 때도 이런 최적화 기법들이 엄청 중요해.
사용자가 늘어날수록 성능 문제가 더 크게 느껴지거든!
처음엔 어려워 보여도 하나씩 적용하다 보면
어느새 최적화가 습관이 되어 있을 거야 💪
코드 짤 때 "이거 더 빠르게 할 수 없을까?" 하는 생각만 해도
벌써 절반은 성공한 거지 ㅋㅋㅋ
🚀 다음 단계로
이 글에서 다룬 내용들을 실제 프로젝트에 적용해보고,
성능이 얼마나 개선되는지 직접 측정해봐!
그리고 더 깊이 공부하고 싶다면:
• "Introduction to Algorithms" (알고리즘 바이블)
• PHP 공식 문서의 SPL 섹션
• Xdebug, Blackfire 같은 프로파일링 도구
• Redis, Memcached 같은 캐싱 시스템
화이팅! 🔥
✨ 최적화는 끝이 없는 여정이야!
항상 배우고, 측정하고, 개선하자! 💪
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.