> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 캐시

> Laravel의 캐시 시스템을 사용해 애플리케이션의 성능을 향상시키는 방법을 배웁니다.

## 캐시란

데이터베이스에의 질의나 외부 API의 호출은 CPU나 네트워크의 비용이 높아 처리에 수 초가 걸리는 경우가 있습니다.
같은 데이터를 몇 번이고 취득하는 경우, 결과를 **캐시**에 보존해 둠으로써 다음 번 이후의 요청을 고속으로 처리할 수 있습니다.

Laravel은 Memcached, Redis, DynamoDB, 데이터베이스 등, 다양한 캐시 백엔드에 대응한 통합 API를 제공하고 있습니다.

<Info>
  기본적으로는 `database` 드라이버가 설정되어 있습니다. Redis나 Memcached로 전환함으로써 더욱 고속의 캐시를 실현할 수 있습니다.
</Info>

## 캐시 설정

### config/cache.php

캐시 설정은 `config/cache.php`에 집약되어 있습니다.
`CACHE_STORE` 환경 변수로 기본 드라이버를 전환합니다.

```php theme={null}
// config/cache.php
'default' => env('CACHE_STORE', 'database'),
```

### 사용 가능한 드라이버

<AccordionGroup>
  <Accordion title="database(기본값)">
    직렬화된 캐시 데이터를 데이터베이스 테이블에 저장합니다.
    Laravel 11 이후의 신규 프로젝트에는 처음부터 마이그레이션이 포함되어 있습니다.

    ```ini theme={null}
    CACHE_STORE=database
    ```

    마이그레이션이 포함되어 있지 않은 경우 Artisan 명령어로 작성합니다.

    ```shell theme={null}
    php artisan make:cache-table
    php artisan migrate
    ```
  </Accordion>

  <Accordion title="file">
    캐시 데이터를 파일시스템에 저장합니다.
    추가의 셋업은 불필요하며, 소규모 애플리케이션에 적합합니다.

    ```ini theme={null}
    CACHE_STORE=file
    ```
  </Accordion>

  <Accordion title="redis">
    인메모리로 동작하는 고속의 캐시 드라이버입니다.
    운영 환경에서 가장 자주 사용됩니다. PhpRedis PHP 확장 또는 `predis/predis` 패키지가 필요합니다.

    ```ini theme={null}
    CACHE_STORE=redis
    REDIS_HOST=127.0.0.1
    REDIS_PORT=6379
    ```
  </Accordion>

  <Accordion title="memcached">
    Memcached PECL 패키지가 필요합니다.
    `config/cache.php`에서 서버를 설정합니다.

    ```php theme={null}
    'memcached' => [
        'servers' => [
            [
                'host' => env('MEMCACHED_HOST', '127.0.0.1'),
                'port' => env('MEMCACHED_PORT', 11211),
                'weight' => 100,
            ],
        ],
    ],
    ```
  </Accordion>

  <Accordion title="dynamodb">
    AWS DynamoDB를 캐시 스토어로 사용합니다.
    사전에 DynamoDB 테이블을 작성하고 AWS SDK를 설치합니다.

    ```shell theme={null}
    composer require aws/aws-sdk-php
    ```

    ```ini theme={null}
    CACHE_STORE=dynamodb
    DYNAMODB_CACHE_TABLE=cache
    AWS_DEFAULT_REGION=us-east-1
    AWS_ACCESS_KEY_ID=your-key-id
    AWS_SECRET_ACCESS_KEY=your-secret-key
    ```
  </Accordion>

  <Accordion title="storage">
    임의의 파일시스템 디스크를 키/값의 캐시 스토어로 사용합니다.
    기존의 S3 디스크를 그대로 캐시로 사용하고 싶은 경우에 편리합니다.

    ```php theme={null}
    'storage' => [
        'driver' => 'storage',
        'disk' => env('CACHE_STORAGE_DISK'),
        'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
    ],
    ```
  </Accordion>

  <Accordion title="array / null(테스트용)">
    `array`는 요청 내에서만 유효한 인메모리 캐시입니다.
    `null`은 모든 조작을 무시합니다. 둘 다 자동 테스트에서 편리합니다.

    ```ini theme={null}
    CACHE_STORE=array
    ```
  </Accordion>
</AccordionGroup>

### 캐시 드라이버의 계층 구조

용도나 속도에 따라 드라이버를 구분해서 사용합니다.

```mermaid theme={null}
flowchart LR
    A["애플리케이션"] --> B["Cache 파사드"]

    subgraph L1 ["L1: 요청 내(최속)"]
        C["array<br>테스트·개발용"]
        D["memo(래퍼)<br>중복 접근 삭감"]
    end

    subgraph L2 ["L2: 인메모리(고속)"]
        E["Redis<br>운영 환경 권장"]
        F["Memcached<br>분산 캐시"]
    end

    subgraph L3 ["L3: 영속화 스토리지(표준)"]
        G["database(기본값)"]
        H["file"]
        I["DynamoDB(AWS)"]
        J["storage<br>S3 등의 디스크"]
    end

    B --> C
    B --> D
    B --> E
    B --> F
    B --> G
    B --> H
    B --> I
    B --> J
```

## 기본적인 조작

### Cache 파사드 취득

`Cache` 파사드를 사용해 캐시를 조작합니다.

```php theme={null}
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    public function index(): array
    {
        $value = Cache::get('key');

        return [
            // ...
        ];
    }
}
```

여러 캐시 스토어를 구분해서 사용하려면 `store()` 메서드를 사용합니다.

```php theme={null}
$value = Cache::store('file')->get('foo');

Cache::store('redis')->put('bar', 'baz', 600); // 10분간 저장
```

### 데이터 취득: `Cache::get()`

`get()` 메서드로 캐시에서 데이터를 취득합니다.
캐시가 존재하지 않는 경우 `null`을 반환합니다. 기본값을 지정할 수도 있습니다.

```php theme={null}
$value = Cache::get('key');

// 기본값 지정
$value = Cache::get('key', 'default');

// 클로저로 기본값을 지연 취득
$value = Cache::get('key', function () {
    return DB::table('settings')->get();
});
```

### 데이터 저장: `Cache::put()`

`put()` 메서드로 데이터를 캐시에 저장합니다. 세 번째 인수에 유효 기간(초)을 지정합니다.

```php theme={null}
// 10초간 저장
Cache::put('key', 'value', 10);

// Carbon 인스턴스로 유효 기간을 지정
Cache::put('key', 'value', now()->plus(minutes: 10));

// 유효 기간 없음(영구 저장)
Cache::put('key', 'value');
```

캐시가 존재하지 않는 경우에만 저장하는 `add()` 메서드도 있습니다.

```php theme={null}
// 존재하지 않는 경우에만 추가(원자 조작)
Cache::add('key', 'value', $seconds);
```

영구적으로 저장하려면 `forever()`를 사용합니다.

```php theme={null}
Cache::forever('key', 'value');
```

### 취득 또는 저장: `Cache::remember()`

**가장 자주 사용되는 조작**입니다. 캐시에 데이터가 존재하면 그것을 반환하고, 없으면 클로저를 실행하여 그 결과를 저장합니다.

```php theme={null}
$users = Cache::remember('users', 3600, function () {
    return DB::table('users')->get();
});
```

<Tip>
  `Cache::remember()`를 사용함으로써 "캐시 확인 → 없으면 취득 → 캐시에 저장"이라는 3단계를 한 줄로 쓸 수 있습니다. 데이터베이스 쿼리 결과나 외부 API 응답의 캐시에 최적입니다.
</Tip>

### remember() 의 플로우

```mermaid theme={null}
flowchart TD
    A["Cache::remember('key', $ttl, fn)"] --> B{"캐시 스토어에<br>'key' 가 존재하는가?"}
    B -->|"히트 (Hit)"| C["캐시에서 데이터를 취득"]
    B -->|"미스 (Miss)"| D["클로저 fn 을 실행<br>예: DB 쿼리 / API 호출"]
    D --> E["결과를 캐시에 저장<br>유효 기간 = $ttl 초"]
    E --> F["데이터를 반환"]
    C --> F
```

영구 저장 버전의 `rememberForever()`도 있습니다.

```php theme={null}
$value = Cache::rememberForever('users', function () {
    return DB::table('users')->get();
});
```

캐시가 "히트"했는지, 클로저에서 새로 취득되었는지를 알고 싶은 경우 `rememberWithWarmth()` 메서드를 사용합니다. 이 메서드는 캐시 값과 그것이 신선한지(캐시에서 취득)를 나타내는 진위값을 배열로 반환합니다.

```php theme={null}
[$value, $warm] = Cache::rememberWithWarmth('users', 3600, function () {
    return DB::table('users')->get();
});

if ($warm) {
    // 캐시로부터의 데이터
} else {
    // 클로저에서 새로 취득한 데이터
}
```

<Tip>
  `rememberWithWarmth()`는 캐시의 효율성을 감시하고 싶은 경우에 편리합니다. \$warm이 `false`인 경우가 많다면 캐시의 유효 기간(TTL)을 조정함으로써 개선할 수 있을지도 모릅니다.
</Tip>

#### Stale While Revalidate(유연한 캐시 업데이트)

`Cache::flexible()`은 캐시가 "신선한 기간"과 "오래되어도 사용할 수 있는 기간"을 배열로 지정합니다.
오래된 데이터를 사용자에게 반환하면서 백그라운드에서 캐시를 업데이트하는 패턴입니다.

```php theme={null}
// [신선한 기간(초), 오래되어도 사용할 수 있는 기간(초)]
$value = Cache::flexible('users', [5, 10], function () {
    return DB::table('users')->get();
});
```

### 존재 확인: `Cache::has()`

```php theme={null}
if (Cache::has('key')) {
    // 캐시가 존재하는 경우의 처리
}
```

### 값의 인크리먼트 / 디크리먼트

정수값의 카운터를 조작할 수 있습니다.

```php theme={null}
// 값이 존재하지 않는 경우는 초기화
Cache::add('key', 0, now()->addHours(4));

// 인크리먼트 / 디크리먼트
Cache::increment('key');
Cache::increment('key', $amount);
Cache::decrement('key');
Cache::decrement('key', $amount);
```

### 취득과 삭제: `Cache::pull()`

취득 후에 캐시에서 삭제합니다. 원타임 데이터의 관리에 편리합니다.

```php theme={null}
$value = Cache::pull('key');
```

### 데이터 삭제: `Cache::forget()`

```php theme={null}
// 특정 키 삭제
Cache::forget('key');

// 캐시 전체 클리어
Cache::flush();
```

<Warning>
  `Cache::flush()`는 캐시의 "프리픽스" 설정과 관계없이 모든 엔트리를 삭제합니다. 여러 애플리케이션이 공유하고 있는 캐시의 경우 주의해 주십시오.
</Warning>

`Cache::flushLocks()`를 사용하면 캐시 내의 모든 원자 락을 클리어할 수 있습니다.

```php theme={null}
Cache::flushLocks();
```

### TTL 의 연장: `Cache::touch()`

기존 캐시 아이템의 유효 기간을 연장합니다.

```php theme={null}
// 초 수로 지정
Cache::touch('key', 3600);

// Carbon 인스턴스로 지정
Cache::touch('key', now()->addHours(2));
```

## 캐시 메모화

`memo` 드라이버를 사용하면 동일 요청 내에서의 캐시 접근을 메모리에 캐시합니다.
같은 키에의 반복 접근이 많은 경우, 캐시 스토어에의 라운드트립을 삭감할 수 있습니다.

```php theme={null}
use Illuminate\Support\Facades\Cache;

// 기본 스토어를 메모화
$value = Cache::memo()->get('key');

// Redis 스토어를 메모화
$value = Cache::memo('redis')->get('key');
```

```php theme={null}
// 최초의 접근은 캐시 스토어에 접근
$value = Cache::memo()->get('key');

// 두 번째 이후는 메모리에서 반환(스토어에 접근하지 않음)
$value = Cache::memo()->get('key');
```

## 캐시 태그

관련된 캐시 아이템을 태그로 그룹화하여 함께 삭제할 수 있습니다.

<Warning>
  캐시 태그는 `file`, `dynamodb`, `database`, `storage` 드라이버에서는 사용할 수 없습니다. `redis`나 `memcached` 드라이버가 필요합니다.
</Warning>

### 태그 부착 캐시의 구조

여러 태그를 붙인 캐시 아이템은 어느 태그로든 함께 삭제할 수 있습니다.

```mermaid theme={null}
flowchart TD
    A["Cache::tags(['people', 'artists'])<br>.put('John', $data)"] --> B["John 의 캐시"]
    C["Cache::tags(['people', 'authors'])<br>.put('Anne', $data)"] --> D["Anne 의 캐시"]

    B --> T1["태그: people"]
    B --> T2["태그: artists"]
    D --> T1
    D --> T3["태그: authors"]

    T1 -->|"flush()"| E["John 과 Anne 을 삭제"]
    T2 -->|"flush()"| F["John 만 삭제"]
    T3 -->|"flush()"| G["Anne 만 삭제"]
```

### 태그 부착 캐시의 저장과 취득

```php theme={null}
use Illuminate\Support\Facades\Cache;

// 태그를 붙여 저장
Cache::tags(['people', 'artists'])->put('John', $john, $seconds);
Cache::tags(['people', 'authors'])->put('Anne', $anne, $seconds);

// 태그를 지정해 취득
$john = Cache::tags(['people', 'artists'])->get('John');
$anne = Cache::tags(['people', 'authors'])->get('Anne');
```

### 태그 부착 캐시의 삭제

```php theme={null}
// 'people' 과 'authors' 태그를 가지는 모든 캐시를 삭제(John도 Anne도 삭제)
Cache::tags(['people', 'authors'])->flush();

// 'authors' 태그만 삭제(Anne 만 삭제. John 은 남음)
Cache::tags('authors')->flush();
```

<Tip>
  사용자별이나 기사별 등, 그룹 단위로 캐시를 무효화하고 싶은 경우에 태그가 도움이 됩니다.
  예: `Cache::tags(['user', "user:{$userId}"])->flush()`로 그 사용자에 관한 모든 캐시를 클리어할 수 있습니다.
</Tip>

## 원자 조작(락)

`Cache::lock()`을 사용하면 분산 락을 구현할 수 있으며, 여러 프로세스나 병렬 요청으로부터의 경합 상태를 방지합니다.

<Info>
  이 기능은 `memcached`, `redis`, `dynamodb`, `database`, `file`, `array` 캐시 드라이버에서 이용할 수 있습니다. 모든 서버가 같은 중앙 캐시 서버와 통신하고 있어야 합니다.
</Info>

### 기본적인 락

```php theme={null}
use Illuminate\Support\Facades\Cache;

$lock = Cache::lock('foo', 10);

if ($lock->get()) {
    // 락 취득 성공(10초간 유효)

    // 무언가의 배타 처리를 수행

    $lock->release();
}
```

클로저를 전달하면 처리 완료 후에 자동으로 락을 해방합니다.

```php theme={null}
Cache::lock('foo', 10)->get(function () {
    // 락을 자동 해방
});
```

### 락 대기

락을 취득할 수 있을 때까지 최대 지정 초 수 대기합니다. 타임아웃되면 `LockTimeoutException`이 던져집니다.

```php theme={null}
use Illuminate\Contracts\Cache\LockTimeoutException;

$lock = Cache::lock('foo', 10);

try {
    $lock->block(5); // 최대 5초 대기

    // 락 취득 후의 처리

} catch (LockTimeoutException $e) {
    // 락 취득 실패
} finally {
    $lock->release();
}
```

클로저를 사용하면 단순하게 쓸 수 있습니다.

```php theme={null}
Cache::lock('foo', 10)->block(5, function () {
    // 최대 5초 대기해서 락을 취득하고, 완료 후에 자동 해방
});
```

### 중복 실행 방지: `withoutOverlapping()`

동일 처리의 다중 실행을 방지하는 단순한 방법입니다.

```php theme={null}
Cache::withoutOverlapping('foo', function () {
    // 동시에 1개만 실행됨
});

// 대기 시간과 락 보유 시간을 커스터마이즈
Cache::withoutOverlapping('foo', function () {
    // ...
}, lockFor: 120, waitFor: 5);
```

### 동시 실행 수 제한: `funnel()`

동시에 실행할 수 있는 처리 수를 제한합니다.

```php theme={null}
Cache::funnel('foo')
    ->limit(3)          // 최대 3개의 병렬 실행을 허가
    ->releaseAfter(60)  // 60초 후에 자동 해방
    ->block(10)         // 최대 10초 대기
    ->then(function () {
        // 동시 실행 슬롯을 확보할 수 있었던 경우의 처리
    }, function () {
        // 슬롯을 확보할 수 없었던 경우의 처리
    });
```

### 프로세스 간에서의 락 이양

Web 요청에서 락을 취득하고 큐 잡에서 해방하는 케이스입니다.

```php theme={null}
// 요청에서 락을 취득해 잡을 디스패치
$lock = Cache::lock('processing', 120);

if ($lock->get()) {
    ProcessPodcast::dispatch($podcast, $lock->owner());
}

// 큐 잡 내에서 락을 해방
Cache::restoreLock('processing', $this->owner)->release();
```

현재의 오너에 관계없이 락을 강제 해방하려면 `forceRelease` 메서드를 사용합니다.

```php theme={null}
Cache::lock('processing')->forceRelease();
```

### 락 리프레시

현재 보유하고 있는 락의 유효 기간을 연장하려면 `refresh` 메서드를 사용합니다. 초 수를 지정하지 않는 경우는 락 취득 시의 원래 유효 기간이 사용됩니다. 장시간 처리에서 짧은 락을 정기적으로 연장하고 싶은 경우에 편리하며, 처음부터 매우 긴 유효 기간을 설정할 필요가 없어집니다.

```php theme={null}
$lock = Cache::lock('generate-reports', 60);

if ($lock->get()) {
    foreach ($reports as $report) {
        $report->generate();

        // 락을 추가로 60초 연장
        $lock->refresh();
    }

    $lock->release();
}
```

## 캐시 헬퍼

`cache()` 헬퍼 함수를 사용하면 `Cache` 파사드와 같은 조작을 보다 단순하게 쓸 수 있습니다.

```php theme={null}
// 값을 취득
$value = cache('key');

// 값을 저장(유효 기간 포함)
cache(['key' => 'value'], $seconds);
cache(['key' => 'value'], now()->addMinutes(10));

// 인수 없이 파사드와 같은 인스턴스를 취득
cache()->remember('users', $seconds, function () {
    return DB::table('users')->get();
});
```

## 실전 예시

### 데이터베이스 쿼리 결과의 캐시

<Steps>
  <Step title="컨트롤러에서 쿼리 결과를 캐시">
    ```php theme={null}
    use Illuminate\Support\Facades\Cache;
    use Illuminate\Support\Facades\DB;

    public function index(): array
    {
        $users = Cache::remember('all-users', 3600, function () {
            return DB::table('users')->orderBy('name')->get();
        });

        return compact('users');
    }
    ```
  </Step>

  <Step title="데이터가 업데이트되면 캐시를 삭제">
    ```php theme={null}
    public function store(Request $request): RedirectResponse
    {
        User::create($request->validated());

        // 캐시를 삭제해 다음 접근 시에 재취득
        Cache::forget('all-users');

        return redirect()->route('users.index');
    }
    ```
  </Step>
</Steps>

### Eloquent 모델의 캐시

```php theme={null}
use App\Models\Product;
use Illuminate\Support\Facades\Cache;

// 카테고리별 상품 목록을 1시간 캐시
public function byCategory(int $categoryId): array
{
    $products = Cache::remember(
        "products:category:{$categoryId}",
        3600,
        fn () => Product::where('category_id', $categoryId)
            ->where('is_active', true)
            ->orderBy('name')
            ->get()
    );

    return compact('products');
}
```

### API 응답의 캐시

```php theme={null}
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

public function getWeather(string $city): array
{
    return Cache::remember(
        "weather:{$city}",
        1800, // 30분간 캐시
        function () use ($city) {
            $response = Http::get('https://api.weather.example.com/current', [
                'city' => $city,
            ]);

            return $response->json();
        }
    );
}
```

### 캐시 태그를 사용한 그룹 관리

```php theme={null}
use Illuminate\Support\Facades\Cache;

class ArticleController extends Controller
{
    public function show(Article $article): array
    {
        $data = Cache::tags(['articles', "article:{$article->id}"])
            ->remember("article:{$article->id}:detail", 3600, function () use ($article) {
                return $article->load(['author', 'tags', 'comments']);
            });

        return compact('data');
    }

    public function update(Request $request, Article $article): RedirectResponse
    {
        $article->update($request->validated());

        // 이 기사에 관한 모든 캐시를 클리어
        Cache::tags(["article:{$article->id}"])->flush();

        return redirect()->route('articles.show', $article);
    }
}
```

## 정리

<AccordionGroup>
  <Accordion title="자주 사용하는 메서드 정리">
    | 메서드                                | 설명              |
    | ---------------------------------- | --------------- |
    | `Cache::get('key')`                | 캐시를 취득          |
    | `Cache::put('key', $value, $ttl)`  | 캐시를 저장          |
    | `Cache::remember('key', $ttl, fn)` | 취득 또는 저장(가장 중요) |
    | `Cache::forget('key')`             | 캐시를 삭제          |
    | `Cache::has('key')`                | 캐시의 존재 확인       |
    | `Cache::flush()`                   | 모든 캐시를 클리어      |
    | `Cache::forever('key', $value)`    | 영구 저장           |
    | `Cache::pull('key')`               | 취득한 후 삭제        |
    | `Cache::increment('key')`          | 숫자를 인크리먼트       |
    | `Cache::tags([...])->flush()`      | 태그 부착 캐시를 클리어   |
  </Accordion>

  <Accordion title="드라이버 선택 방법">
    * **개발·소규모**: `file` 또는 `database`
    * **운영·고트래픽**: `redis`(Laravel Horizon과 조합하면 감시도 용이)
    * **AWS 환경**: `dynamodb` 또는 `storage`(S3 디스크를 유용하는 경우)
    * **테스트**: `array` 또는 `null`
  </Accordion>

  <Accordion title="캐시의 모범 사례">
    * 캐시 키는 앱 전체에서 유니크해지도록 명명한다(예: `users:1:profile`)
    * 데이터가 업데이트되면 `Cache::forget()` 또는 `Cache::tags()->flush()`로 캐시를 무효화한다
    * 캐시의 TTL은 너무 짧지도 너무 길지도 않도록, 데이터의 업데이트 빈도에 맞춰 설정한다
    * `Cache::remember()`를 사용함으로써 캐시 미스 시의 처리를 간결하게 쓸 수 있다
    * 운영 환경에서는 Redis를 사용하고 페일오버 설정도 검토한다
  </Accordion>
</AccordionGroup>


## Related topics

- [Laravel에서 MCP 서버 만들기](/ko/advanced/mcp-server.md)
- [Laravel AI SDK](/ko/ai-sdk.md)
- [Laravel 12에서 13으로의 업그레이드 가이드](/ko/blog/upgrade-12-to-13.md)
- [MongoDB](/ko/mongodb.md)
- [Eloquent 액세서·뮤테이터·캐스트](/ko/eloquent-mutators.md)
