# 템플릿 캐싱 전략 > **관련 문서**: [cache-driver.md](./cache-driver.md) (공통 캐시 인터페이스 · 권장) | [template-basics.md](./template-basics.md) | [template-security.md](./template-security.md) | [index.md](./index.md) > **⚠️ 중요**: 본 문서는 템플릿 캐싱의 **개념적 계층**을 설명합니다. > 실제 구현은 [`CacheInterface`](./cache-driver.md) 를 사용해야 하며, `Cache::` 파사드 직접 호출은 금지되었습니다. > 모든 캐시 키는 드라이버 접두사(`g7:core:` / `g7:module.{id}:` / `g7:plugin.{id}:`)가 자동 적용됩니다. > TTL 은 `g7_core_settings('cache.*_ttl')` 중앙 관리를 따릅니다. --- ## 목차 1. [핵심 원칙](#핵심-원칙) 2. [캐싱 계층](#캐싱-계층) 3. [확장 기능 캐시 버전 (Cache Busting)](#확장-기능-캐시-버전-cache-busting) 4. [캐시 무효화 구현](#캐시-무효화-구현) 5. [서비스에서 캐시 무효화 호출](#서비스에서-캐시-무효화-호출) 6. [캐싱 모범 사례](#캐싱-모범-사례) 7. [성능 벤치마크](#성능-벤치마크) --- ## 핵심 원칙 ``` 중요: 다층 캐싱으로 성능 최적화 ✅ 필수: 캐시 무효화 규칙 준수 ``` 템플릿 시스템은 여러 계층의 캐싱을 통해 성능을 최적화합니다. 각 캐시는 적절한 TTL(Time To Live)을 가지며, 데이터 변경 시 정확한 무효화가 필수입니다. --- ## 캐싱 계층 ### 1. 활성 템플릿 상태 캐시 ```php // 키: ext.templates.active_identifiers / ext.templates.active_identifiers_{type} / ext.templates.installed_identifiers // TTL: extension_status_ttl 설정값 (기본 86400초) // 값: 템플릿 식별자 목록 (CachesTemplateStatus 트레이트가 관리) $activeIdentifiers = $cache->remember('ext.templates.active_identifiers_admin', function () { return Template::where('type', 'admin') ->where('is_active', true) ->pluck('identifier') ->all(); }, $ttl); ``` **무효화 시점**: - 템플릿 설치/활성화/비활성화/삭제/업데이트 시 (`TemplateManager::invalidateTemplateStatusCache()` — 상태 키 4종 일괄 삭제) - `template:cache-clear` 실행 시 --- ### 2. 레이아웃 JSON 캐시 ```php // 키: template.layout.{layout_name}.{locale} // TTL: 1800초 (30분) // 값: 병합된 레이아웃 JSON 배열 Cache::remember("template.layout.dashboard.ko", 1800, function () use ($layoutName, $locale) { return $this->layoutService->getLayout($layoutName, $locale); }); ``` **무효화 시점**: - 레이아웃 생성/수정/삭제 시 - 상위 레이아웃(extends) 변경 시 - 로케일 변경 시 --- ### 3. 병합된 레이아웃 캐시 상속 체인 처리 결과를 캐싱합니다. ```php // 키: template.layout.merged.{layout_name}.{locale} // TTL: 3600초 (1시간) // 값: extends/slots 병합 완료된 최종 레이아웃 Cache::remember("template.layout.merged.dashboard.ko", 3600, function () use ($layoutName, $locale) { return $this->layoutService->mergeLayouts($layoutName, [], 0, $locale); }); ``` **무효화 시점**: - 레이아웃 JSON 캐시 무효화 시 함께 무효화 - 상속 체인의 어느 레이아웃이든 변경 시 --- ### 4. 컴포넌트 매니페스트 캐시 ```php // 키: template.{templateId}.components_manifest (숫자 id / identifier 두 변종) // TTL: 3600초 (1시간) // 값: components.json 파싱 결과 (레이아웃 컴포넌트 검증용) $manifest = $cache->remember("template.{$templateId}.components_manifest", function () use ($identifier) { $componentsPath = base_path("templates/{$identifier}/components.json"); return json_decode(file_get_contents($componentsPath), true); }); ``` **무효화 시점**: - 템플릿 라이프사이클 전 경로 (`TemplateManager::clearTemplateCache()` — update/deactivate/uninstall/`template:cache-clear`) - components.json 이 바뀌는 템플릿 업데이트 시 (위 경로에 포함) --- ## 확장 기능 캐시 버전 (Cache Busting) ### 개요 모듈/플러그인/템플릿 활성화/비활성화 시 프론트엔드가 최신 데이터를 즉시 반영하도록 버전 기반 캐시 버스팅을 구현합니다. ``` 문제 상황: 브라우저 HTTP 캐시로 인해 모듈 활성화 후 새로고침(Ctrl+F5) 전까지 다국어/라우트 데이터가 반영되지 않음 ✅ 해결책: 캐시 버전(타임스탬프) 기반 쿼리 파라미터로 캐시 무효화 ``` ### 백엔드 구현 #### ClearsTemplateCaches Trait 모듈/플러그인/템플릿 매니저에서 사용하는 공통 trait입니다. 내부적으로 `CacheInterface` 를 lazy resolve 해서 사용합니다. ```php put(self::$extensionCacheVersionKey, time()); } /** * 현재 확장 기능 캐시 버전을 반환합니다. */ public static function getExtensionCacheVersion(): int { return (int) self::resolveExtensionCache()->get(self::$extensionCacheVersionKey, 0); } private static function resolveExtensionCache(): CacheInterface { try { return app(CacheInterface::class); } catch (\Throwable $e) { return new CoreCacheDriver(config('cache.default', 'array')); } } /** * 모든 활성 템플릿의 언어 캐시를 무효화합니다. */ protected function clearAllTemplateLanguageCaches(): void { $locales = config('app.supported_locales', ['ko', 'en']); $templates = Template::where('status', ExtensionStatus::Active->value)->get(); foreach ($templates as $template) { foreach ($locales as $locale) { Cache::forget("template.language.{$template->identifier}.{$locale}"); } } } /** * 모든 활성 템플릿의 routes 캐시를 무효화합니다. */ protected function clearAllTemplateRoutesCaches(): void { $templates = Template::where('status', ExtensionStatus::Active->value)->get(); foreach ($templates as $template) { Cache::forget("template.routes.{$template->identifier}"); } } } ``` #### 캐시 버전 증가 트리거 시점 | 매니저 | 메서드 | 트리거 상황 | |--------|--------|------------| | ModuleManager | `activate()` | 모듈 활성화 | | ModuleManager | `deactivate()` | 모듈 비활성화 | | ModuleManager | `refreshModuleLayouts()` | 레이아웃 갱신 시 변경 발생 | | PluginManager | `activate()` | 플러그인 활성화 | | PluginManager | `deactivate()` | 플러그인 비활성화 | | TemplateManager | `activate()` | 템플릿 활성화 | | TemplateManager | `refreshTemplateLayouts()` | 레이아웃 갱신 시 변경 발생 | ### 프론트엔드 구현 #### 캐시 버전 저장 및 관리 ```typescript // LayoutLoader.ts private cacheVersion: number = 0; constructor() { // localStorage에서 캐시 버전 복원 const stored = localStorage.getItem('g7_extension_cache_version'); this.cacheVersion = stored ? parseInt(stored, 10) : 0; } setCacheVersion(version: number): void { const oldVersion = this.cacheVersion; this.cacheVersion = version; localStorage.setItem('g7_extension_cache_version', String(version)); // 버전 변경 시 캐시 무효화 if (oldVersion !== 0 && oldVersion !== version) { this.clearCache(); } } ``` #### API 요청 시 버전 파라미터 추가 ```typescript // LayoutLoader.ts async loadLayout(layoutName: string): Promise { let url = `/api/layout/${layoutName}`; if (this.cacheVersion > 0) { url += `?v=${this.cacheVersion}`; } // ... } // TranslationEngine.ts async loadTranslations(identifier: string, locale: string): Promise { let url = `/api/template/${identifier}/language/${locale}`; const params: string[] = []; if (this.cacheVersion > 0) { params.push(`v=${this.cacheVersion}`); } if (params.length > 0) { url += `?${params.join('&')}`; } // ... } ``` #### 앱 초기화 시 캐시 버전 설정 ```typescript // TemplateApp.ts async init(): Promise { // 서버에서 캐시 버전 조회 const response = await fetch('/api/extension/cache-version'); const { version } = await response.json(); // 각 엔진에 버전 설정 this.layoutLoader.setCacheVersion(version); this.translationEngine.setCacheVersion(version); } ``` ### 캐시 버전 API 엔드포인트 ```php // routes/api.php Route::get('/extension/cache-version', function () { return response()->json([ 'version' => ClearsTemplateCaches::getExtensionCacheVersion(), ]); }); ``` ### 동작 흐름 ``` 1. 사용자가 모듈 활성화 버튼 클릭 2. 백엔드: ModuleManager->activate() 실행 3. 백엔드: incrementExtensionCacheVersion() 호출 → 캐시 버전 = 현재 타임스탬프 4. 백엔드: clearAllTemplateLanguageCaches() 호출 → 서버 캐시 무효화 5. 프론트엔드: 페이지 새로고침 또는 SPA 라우트 이동 6. 프론트엔드: /api/extension/cache-version 조회 7. 프론트엔드: 버전 변경 감지 → localStorage 업데이트 + 내부 캐시 클리어 8. 프론트엔드: API 요청 시 ?v={version} 파라미터 추가 9. 브라우저: 새로운 URL로 인식하여 캐시 우회 → 최신 데이터 수신 ``` ### 캐시 버전 키 요약 | 저장소 | 키 | 값 | 용도 | |--------|-----|-----|------| | Laravel Cache | `g7:core:ext.cache_version` | 타임스탬프 | 서버 측 버전 관리 (CacheInterface 경유) | | localStorage | `g7_extension_cache_version` | 타임스탬프 | 클라이언트 측 버전 유지 | --- ## 캐시 무효화 구현 ### TemplateCacheService ```php invalidateLayout($layoutName); // 이 레이아웃을 extends하는 모든 자식 레이아웃 무효화 $children = TemplateLayout::where('extends', $layoutName)->get(); foreach ($children as $child) { $this->invalidateLayoutChain($child->layout_name); } } /** * 컴포넌트 매니페스트 캐시 무효화 (숫자 id / identifier 두 변종) */ public function invalidateComponentRegistry(int $templateId, string $identifier): void { Cache::forget("template.{$templateId}.components_manifest"); Cache::forget("template.{$identifier}.components_manifest"); } /** * 템플릿 전체 캐시 무효화 */ public function invalidateAllTemplateCache(string $identifier): void { // 활성 템플릿 캐시 무효화 $template = Template::where('identifier', $identifier)->first(); if ($template) { $this->invalidateActiveTemplate($template->type); } // 모든 레이아웃 캐시 무효화 $layouts = TemplateLayout::where('template_id', $template->id)->get(); foreach ($layouts as $layout) { $this->invalidateLayoutChain($layout->layout_name); } // 컴포넌트 매니페스트 캐시 무효화 $this->invalidateComponentRegistry($template->id, $identifier); } } ``` --- ## 서비스에서 캐시 무효화 호출 ### LayoutService 예시 ```php extends; $layout->update($data); // 캐시 무효화 $this->cacheService->invalidateLayoutChain($layout->layout_name); // extends 변경 시 이전 부모의 자식들도 무효화 if ($oldExtends && $oldExtends !== $layout->extends) { $this->cacheService->invalidateLayoutChain($oldExtends); } return $layout->fresh(); } /** * 템플릿 활성화 */ public function activateTemplate(string $identifier): Template { $template = Template::where('identifier', $identifier)->firstOrFail(); // 동일 타입의 기존 활성 템플릿 비활성화 Template::where('type', $template->type) ->where('is_active', true) ->update(['is_active' => false]); $template->update(['is_active' => true]); // 캐시 무효화 $this->cacheService->invalidateActiveTemplate($template->type); return $template->fresh(); } } ``` ### 레이아웃 편집(관리자 커스터마이징) 시 캐시 무효화 관리자 UI에서 레이아웃을 직접 편집/복원할 때도 캐시 무효화가 필수입니다. `LayoutService`의 저장(`updateLayoutContent`) 및 버전 복원(`restoreVersion`) 메서드에서: 1. **`clearDependentLayoutsCache()`** 호출 — 해당 레이아웃 + 이를 extends하는 자식 레이아웃의 병합 캐시 + `PublicLayoutController` 서빙 캐시(`layout.{identifier}.{name}.v{version}`)를 재귀적으로 무효화 2. **`ext.cache_version` 증가** (`$this->incrementExtensionCacheVersion()` — `ClearsTemplateCaches` 트레이트) — CDN/브라우저 HTTP 캐시 busting. `PublicLayoutController`가 `?v={version}` 기반 캐싱을 사용하므로, 버전 변경 시 브라우저가 새 URL로 인식하여 캐시를 우회합니다. 버전 키 쓰기는 반드시 트레이트를 경유합니다 — 캐시 드라이버에 직접 `put` 하면 스토어/네임스페이스가 어긋날 때 bump 가 읽기 경로에서 보이지 않습니다. ```php // LayoutService::updateLayoutContent() 내부 $layout = $this->layoutRepository->update($layout->id, $updateData); // 캐시 무효화 (현재 레이아웃 + 자식 레이아웃 + PublicLayoutController 서빙 캐시) $this->clearDependentLayoutsCache($templateId, $name); // 프론트엔드 브라우저 캐시 무효화 (ClearsTemplateCaches 트레이트) $this->incrementExtensionCacheVersion(); ``` ```php // LayoutService::restoreVersion() 내부 $newVersion = $this->versionRepository->restoreVersion($layout->id, $versionId); // 동일하게 캐시 무효화 $this->clearDependentLayoutsCache($templateId, $name); $this->incrementExtensionCacheVersion(); ``` 이 과정이 누락되면 편집/복원 내용이 실제 사이트에 반영되지 않습니다. > 참고: `clearDependentLayoutsCache()`는 내부적으로 `clearLayoutCache()`를 호출하며, 이 메서드는 병합 캐시 삭제(일반 + `.with_source_meta` 편집기 변종) + `clearPublicServingCache()`(현재 `ext.cache_version` 버전 키 삭제) + 모듈 레이아웃인 경우 `LayoutResolverService` 캐시도 무효화합니다. --- ## 캐싱 모범 사례 ### DO (권장) - ✅ 읽기 빈도가 높은 데이터는 캐싱 필수 (활성 템플릿, 레이아웃 JSON) - ✅ TTL은 데이터 변경 빈도에 따라 조정 - ✅ 상속 체인 무효화는 재귀적으로 처리 - ✅ 로케일별 캐싱으로 다국어 지원 - ✅ 캐시 키에 의미 있는 이름 사용 ### DON'T (금지) - ❌ 캐시 무효화 누락 금지 (데이터 불일치 발생) - ❌ 과도한 캐싱으로 메모리 낭비 금지 - ❌ TTL 없이 무제한 캐싱 남용 금지 (컴포넌트 레지스트리 제외) --- ## 성능 벤치마크 참고용 성능 비교 데이터입니다. | 상태 | 응답 시간 | 개선율 | |------|----------|--------| | 캐싱 없음 | 120ms | - | | 캐싱 적용 | 5ms | 95% ↓ | --- ## 캐시 키 요약 서버 사이드 캐시는 모두 `CacheInterface`(`g7:core:` 네임스페이스) 에 저장됩니다. 무효화 방식은 두 갈래입니다 — **버전 키**(`.v{v}` 접미사)는 `ext.cache_version` bump 로 다음 요청부터 새 키를 쓰고 이전 키는 TTL 로 자연 만료되며, **고정 키**(버전 접미사 없음)는 라이프사이클에서 능동 `forget` 해야 합니다. | 캐시 타입 | 키 패턴 | TTL | 무효화 | |----------|---------|-----|--------| | 공개 템플릿 설정 (config.json) | `template.config.{identifier}` | 3600초 | 고정 키 — 라이프사이클 능동 삭제 | | 공개 라우트 (routes.json) | `template.routes.{identifier}.v{v}` | 3600초 | 버전 키 + 현재 버전 키 능동 삭제 | | 공개 다국어 (lang/{locale}.json) | `template.language.{identifier}.{locale}.v{v}` | 3600초 | 버전 키 + 현재 버전 키 능동 삭제 | | 공개 레이아웃 서빙 | `layout.{identifier}.{name}.v{v}` (+ 편집기 `.meta` 변종) | layout_ttl (기본 3600초) | 버전 키 + 현재 버전 키 능동 삭제 | | 병합 레이아웃 (내부) | `template.{templateId}.layout.{name}` (+ `.{sourceHash}` / `.with_source_meta` 변종) | layout_ttl | 고정 키 — 능동 삭제 | | 컴포넌트 매니페스트 (검증) | `template.{templateId}.components_manifest` (identifier 변종 포함) | 3600초 | 고정 키 — 능동 삭제 | | 템플릿 상태 | `ext.templates.active_identifiers*` / `ext.templates.installed_identifiers` | 86400초 | 라이프사이클 능동 삭제 | | 확장 캐시 버전 좌표 | `ext.cache_version` | 없음 | bump 전용 (`ClearsTemplateCaches`) | ### `template:cache-clear` 가 무효화하는 대상 `template:cache-clear {identifier?}` 는 템플릿 라이프사이클(update/deactivate/uninstall)과 동일한 무효화 단일 지점을 경유합니다. | 대상 | 단일 모드 | 전체 모드 | |------|-----------|-----------| | `template.config.{identifier}` | ✅ | ✅ (설치된 전 템플릿) | | `template.{id}.components_manifest` (identifier 변종 포함) | ✅ | ✅ | | 현재 버전 routes/language 키 | ✅ | ✅ | | 병합 레이아웃 캐시 (변종 포함) | ✅ | ✅ | | 템플릿 상태 키 + `ext.cache_version` bump | ✅ | ✅ (1회) | | 확장 프론트엔드 병합 번들 파일 | — | ✅ | --- ## 관련 문서 - [템플릿 기초](./template-basics.md) - 템플릿 타입, 메타데이터 - [템플릿 보안](./template-security.md) - API 서빙, 화이트리스트 - [템플릿 커맨드](./template-commands.md) - Artisan 커맨드 - [확장 시스템 인덱스](./index.md) - 전체 문서 목록