캐시·Redis·성능 최적화
캐시·Redis·성능 최적화
요약: 느린 쿼리를 먼저 고친 뒤 Redis 캐시를 쓰고, 무효화 지점을 정해 두지 않으면 캐시는 낡은 화면이 된다.
1. 최적화 순서
캐시는 느린 증상을 가릴 수 있지만, N+1을 캐시하면 틀린 결과가 더 빨리 나간다. 순서를 지킨다.
- 느린 쿼리·N+1 제거 (3편)
- 메일·리사이즈는 큐로 (7편)
- 읽기 많은 결과를 캐시
- HTTP 캐시·CDN
- 필요할 때만 Octane·스케일 아웃
Cache::remember를 모든 메서드에 뿌리면 무효화(언제 지울지)를 아무도 모른다.
2. Redis와 드라이버
Redis는 메모리 키-값 저장소다. Laravel에서는 캐시, 세션, 큐, 속도 제한, 분산 락에 같은 인스턴스를 쓰는 경우가 많다. 용도별로 DB 번호를 나누거나, 트래픽이 커지면 인스턴스를 분리한다.
CACHE_STORE=redis
REDIS_CLIENT=phpredis
| 드라이버 | 언제 |
|---|---|
array |
요청 안만. 테스트 |
database / file |
작은 단일 서버 |
redis |
실무 기본 |
파일 캐시는 여러 웹 서버에서 공유되지 않는다. 스케일 아웃하면 Redis(또는 Memcached)가 맞다.
3. 기본 API
$post = Cache::remember('posts.'.$id, now()->addMinutes(10), function () use ($id) {
return Post::with('user')->findOrFail($id);
});
Cache::put('key', $value, 600);
Cache::forget('posts.'.$id);
Cache::flush(); // 운영에서 전부 삭제 — 거의 쓰지 않는다
remember는 키가 있으면 반환하고, 없으면 클로저를 실행해 저장한다. TTL(수명) 없는 forever는 무효화를 반드시 코드로 해야 한다.
태그(Redis/Memcached)로 게시글 묶음을 한 번에 지운다.
Cache::tags(['posts', 'user:'.$userId])->put($key, $value, 600);
Cache::tags(['posts'])->flush();
database 드라이버는 태그를 지원하지 않는다. 로컬은 database, 운영은 Redis면 태그 코드가 운영에서만 동작한다. 드라이버를 맞춘다.
갱신은 Observer 한곳에 모은다. 컨트롤러마다 forget을 흩뿌리면 빠뜨린다.
public function updated(Post $post): void
{
Cache::forget('posts.'.$post->id);
Cache::tags(['posts'])->flush();
}
목록을 posts.published.page.'.$page로 캐시하면 페이지마다 키가 는다. 글 하나 수정에 태그 flush가 없으면 목록이 하루 종일 옛날이다.
4. 프레임워크 캐시 (운영)
설정·라우트·뷰를 PHP 파일로 묶어 부팅을 줄인다. 배포 파이프라인에서 돌리고, 로컬에서 route:cache를 켜 두면 새 라우트가 안 보인다.
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
config:cache 뒤에 코드의 env()가 비는 문제는 1편과 같다.
5. HTTP 캐시
return response($content)
->header('Cache-Control', 'public, max-age=60')
->setEtag(md5($content));
public은 CDN·공유 캐시가 응답을 저장해도 된다는 뜻이다. 로그인 사용자마다 다른 JSON에 public을 붙이면 남의 이름이 캐시에 남는다. 개인화는 private 또는 캐시하지 않는다.
6. 락
같은 주문에 결제 요청이 두 번 들어오면 Redis 락으로 임계 구역을 좁힌다. 락은 DB 유니크 제약을 대체하지 않는다. 둘 다 쓰는 편이 안전하다.
$lock = Cache::lock('order:'.$orderId, 10);
if ($lock->get()) {
try {
// 한 번에 하나만
} finally {
$lock->release();
}
}
락을 못 받으면 빈 200을 주면 클라이언트가 성공으로 오해한다. 409나 재시도 안내를 정한다.
7. 쿼리가 여전히 먼저다
preventLazyLoading으로 N+1을 로컬에서 터뜨린다.select로 필요한 컬럼만.with(['user:id,name'])처럼 제한한다.where/orderBy에 인덱스, 의심되면EXPLAIN.- 대량은
chunkById/lazyById.
Post::with([
'user:id,name',
'tags:id,name',
'comments' => fn ($q) => $q->latest()->limit(5),
])->get();
Debugbar·Telescope는 로컬/스테이징용이다. 요청당 쿼리 개수를 숫자로 본다. 감으로 “Redis 넣자”보다 쿼리 20번이 원인인 경우가 많다.
8. Octane은 선택
Octane은 Swoole/RoadRunner로 앱을 메모리에 상주시켜, 요청마다 프레임워크를 다시 켜는 비용을 줄인다. 트래픽이 크고 부트 비용이 CPU를 잡아먹을 때 이득이다.
주의: static이나 싱글톤에 요청 데이터를 남기면 다음 사용자에게 유출된다. 일반 PHP-FPM에서는 요청이 끝나면 사라지던 습관이 Octane에서는 남는다. 도입 전에 상태 오염을 점검하고, 배포 시 워커 리로드 전략이 필요하다.
composer require laravel/octane
php artisan octane:install
9. 흔한 실수
- 측정 없이 전부
remember한다. Cache::flush()로 세션·다른 앱 키까지 지운다.- 개인화 응답에
Cache-Control: public. - 수정 시
forget을 빼먹어 관리자가 “저장했는데 안 바뀐다”고 한다. - Redis 없이 태그 캐시를 전제한다.
측정은 p95 지연, 요청당 쿼리 수, Redis hit 비율, 큐 대기 시간을 같이 본다. Telescope는 운영 전면 공개 금지.
연습
- 게시글 상세를
remember하고, 수정 Observer에서forget한다. - Redis lock으로 중복 결제 요청을 거절한다.
- 스테이징에서
config:cache/route:cache후 앱이 뜨는지 확인한다. - 목록 API 쿼리 수를
with전후로 비교한다.
댓글
0아직 댓글이 없습니다. 첫 댓글을 남겨보세요.