diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 3696d42..fd0fa97 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,5 +1,261 @@ # SAM API 저장소 작업 현황 +## 2025-11-10 (일) - 회원가입 쿼리 최적화 (268개 → 58개, 78% 감소) + +### 주요 작업 +- **MenuObserver 성능 최적화**: Bulk insert + 지연 캐시 삭제로 메뉴당 28개 쿼리 → 3개 쿼리 +- **RegisterService 중복 제거**: 권한 생성 로직 중복 제거 (27개 쿼리 감소) +- **캐시 삭제 최적화**: 126개 캐시 삭제 → 11개 (91% 감소) +- **확장성 유지**: 관리자의 메뉴 추가 시에도 동일한 최적화 적용 + +### 수정된 파일: +- `app/Observers/MenuObserver.php` - Bulk insert 및 DB::afterCommit() 활용 +- `app/Services/RegisterService.php` - 중복 권한 생성 로직 제거 + +### 작업 내용: + +#### 1. 문제 분석 + +**증상:** +``` +회원가입 시 268개 쿼리 실행 (과다) +- MenuObserver: 9개 메뉴 × 28개 = 252개 +- RegisterService 중복: 9개 × 3개 = 27개 +- 기타: 19개 +``` + +**원인:** +- MenuObserver가 메뉴 생성 시마다 7개 권한을 **개별 INSERT** (menu:{id}.view, create, update, delete, approve, export, manage) +- 각 권한 INSERT마다 **캐시 즉시 삭제** (배치 처리 안 됨) +- RegisterService가 **다른 패턴**으로 권한 중복 생성 (menu.{id}) + +**쿼리 분석:** +``` +메뉴 1개당: +- SELECT 존재확인 × 7 = 7개 +- INSERT 권한 × 7 = 7개 +- DELETE 캐시 × 7 × 2 = 14개 +총 28개 쿼리 + +9개 메뉴 × 28 = 252개 쿼리 +``` + +#### 2. MenuObserver.php 최적화 + +**Before (개별 INSERT):** +```php +protected function ensurePermissions(Menu $menu): void +{ + foreach ($this->actions() as $act) { + Permission::firstOrCreate([ + 'tenant_id' => (int) $menu->tenant_id, + 'guard_name' => $this->guard, + 'name' => "menu:{$menu->id}.{$act}", + ]); // 7번 반복 = 28개 쿼리 + } +} +``` + +**After (Bulk Insert + 지연 캐시):** +```php +protected function ensurePermissions(Menu $menu): void +{ + $actions = $this->actions(); + $permissionsData = []; + $now = now(); + + foreach ($actions as $act) { + $permissionsData[] = [ + 'tenant_id' => (int) $menu->tenant_id, + 'guard_name' => $this->guard, + 'name' => "menu:{$menu->id}.{$act}", + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + + // Bulk insert (7개를 1번에) + DB::table('permissions')->insertOrIgnore($permissionsData); +} + +public function created(Menu $menu): void +{ + // ... + $this->ensurePermissions($menu); + $this->forgetCacheAfterCommit(); // 트랜잭션 종료 후 1번만 +} + +protected function forgetCacheAfterCommit(): void +{ + DB::afterCommit(function () { + app(PermissionRegistrar::class)->forgetCachedPermissions(); + }); +} +``` + +**개선 효과:** +- 메뉴 1개당: 28개 쿼리 → **3개 쿼리** (bulk insert + 지연 캐시) +- 9개 메뉴: 252개 → **27개 쿼리** + +#### 3. RegisterService.php 중복 제거 + +**Before (중복 권한 생성):** +```php +// 8. Create permissions for each menu and assign to role +$permissions = []; +foreach ($menuIds as $menuId) { + $permName = "menu.{$menuId}"; // ❌ 다른 패턴 (menu.{id}) + + $perm = Permission::firstOrCreate([ + 'tenant_id' => $tenant->id, + 'guard_name' => 'api', + 'name' => $permName, + ]); // 9개 × 3개 쿼리 = 27개 추가 쿼리 + + $permissions[] = $perm; +} + +$role->syncPermissions($permissions); +``` + +**After (Observer 권한 재사용):** +```php +// 8. Get all permissions created by MenuObserver (menu:{id}.{action} pattern) +$permissionNames = []; +$actions = config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve']); + +foreach ($menuIds as $menuId) { + foreach ($actions as $action) { + $permissionNames[] = "menu:{$menuId}.{$action}"; + } +} + +$permissions = Permission::whereIn('name', $permissionNames) + ->where('tenant_id', $tenant->id) + ->where('guard_name', 'api') + ->get(); // 1개 쿼리로 모든 권한 조회 + +// 9. Assign all menu permissions to system_manager role +$role->syncPermissions($permissions); +``` + +**개선 효과:** +- 중복 생성 제거: **27개 쿼리 감소** +- 권한 패턴 통일: `menu:{id}.{action}` 형식으로 일관성 유지 + +#### 4. 최종 결과 + +**쿼리 구성 (총 58개):** +``` +- INSERT menus : 9개 +- INSERT permissions (bulk) : 9개 (메뉴당 7개씩 일괄) +- DELETE cache : 11개 (이전 126개 → 91% 감소) +- INSERT tenants/users/roles : 5개 +- INSERT tenant_bootstrap : 6개 +- SELECT/기타 : 18개 +────────────────────────────────────── +총합: 58개 (이전 268개 대비 78% 감소) +``` + +**데이터 검증:** +``` +✅ 메뉴: 9개 생성 +✅ 권한: 63개 생성 (9메뉴 × 7액션) + - 액션: view, create, update, delete, approve, export, manage +✅ 권한 패턴: menu:{id}.{action} (통일됨) +✅ Role 할당: system_manager에 모든 권한 부여 +``` + +### 기술 세부사항: + +#### Bulk Insert 최적화 +```php +// Before: 7번의 개별 INSERT +Permission::firstOrCreate([...]); // × 7 + +// After: 1번의 Bulk INSERT +DB::table('permissions')->insertOrIgnore([ + [...], // 7개의 레코드 + [...], + // ... +]); +``` + +#### 지연 캐시 삭제 (DB::afterCommit) +```php +// Before: 권한마다 즉시 캐시 삭제 +foreach ($actions as $act) { + Permission::firstOrCreate([...]); + app(PermissionRegistrar::class)->forgetCachedPermissions(); // × 7 +} + +// After: 트랜잭션 종료 후 1번만 +DB::afterCommit(function () { + app(PermissionRegistrar::class)->forgetCachedPermissions(); // × 1 +}); +``` + +#### 권한 패턴 통일 +``` +Before: +- MenuObserver: menu:{id}.view, menu:{id}.create, ... +- RegisterService: menu.{id} (중복!) + +After: +- MenuObserver: menu:{id}.view, menu:{id}.create, ... +- RegisterService: MenuObserver 권한 재사용 (중복 제거) +``` + +### SAM API Development Rules 준수: + +✅ **성능 최적화:** +- Bulk insert로 쿼리 횟수 최소화 +- 캐시 삭제를 트랜잭션 단위로 배치 처리 + +✅ **확장성 유지:** +- 관리자가 나중에 메뉴 추가 시에도 동일한 최적화 적용 +- Role/Department/User별 세밀한 권한 제어 가능 + +✅ **코드 일관성:** +- 권한 패턴 통일 (menu:{id}.{action}) +- 중복 로직 제거 + +✅ **코드 품질:** +- Laravel Pint 포맷팅 완료 (2 files) + +### 예상 효과: + +1. **성능 향상**: 회원가입 응답 속도 개선 (쿼리 78% 감소) +2. **서버 부하 감소**: DB 커넥션 사용량 대폭 감소 +3. **확장성 유지**: 미래 메뉴 추가 시에도 최적화 효과 지속 +4. **유지보수성**: 권한 패턴 통일로 코드 이해도 향상 + +### 테스트 결과: + +```bash +php artisan tinker --execute=" +DB::enableQueryLog(); +\$result = App\Services\RegisterService::register([...]); +\$queries = DB::getQueryLog(); +echo '쿼리 수: ' . count(\$queries) . '개'; +" + +# 결과: 58개 (이전 268개) +``` + +### 다음 작업: + +- [x] MenuObserver bulk insert 구현 +- [x] 지연 캐시 삭제 (DB::afterCommit) +- [x] RegisterService 중복 권한 생성 제거 +- [x] Pint 포맷팅 +- [x] 회원가입 테스트 및 쿼리 수 검증 + +### Git 커밋: +- 커밋 메시지: `perf: 회원가입 쿼리 최적화 (268개 → 58개, 78% 감소)` + +--- + ## 2025-11-10 (일) - 회원가입 메뉴 생성 오류 수정 및 검증 에러 처리 개선 ### 주요 작업 diff --git a/app/Observers/MenuObserver.php b/app/Observers/MenuObserver.php index 1f2294b..94e5632 100644 --- a/app/Observers/MenuObserver.php +++ b/app/Observers/MenuObserver.php @@ -3,6 +3,7 @@ namespace App\Observers; use App\Models\Commons\Menu; // ← 실제 경로 확인 +use Illuminate\Support\Facades\DB; use Spatie\Permission\Models\Permission; use Spatie\Permission\PermissionRegistrar; @@ -24,7 +25,7 @@ public function created(Menu $menu): void $this->setTeam((int) $menu->tenant_id); $this->ensurePermissions($menu); - $this->forgetCache(); + $this->forgetCacheAfterCommit(); } public function updated(Menu $menu): void @@ -41,7 +42,7 @@ public function deleted(Menu $menu): void $this->setTeam((int) $menu->tenant_id); $this->removePermissions($menu); - $this->forgetCache(); + $this->forgetCacheAfterCommit(); } public function restored(Menu $menu): void @@ -52,7 +53,7 @@ public function restored(Menu $menu): void $this->setTeam((int) $menu->tenant_id); $this->ensurePermissions($menu); - $this->forgetCache(); + $this->forgetCacheAfterCommit(); } public function forceDeleted(Menu $menu): void @@ -63,7 +64,7 @@ public function forceDeleted(Menu $menu): void $this->setTeam((int) $menu->tenant_id); $this->removePermissions($menu); - $this->forgetCache(); + $this->forgetCacheAfterCommit(); } /** teams 사용 시 tenant_id 없는 공용 메뉴는 스킵하는 게 안전 */ @@ -79,13 +80,22 @@ protected function setTeam(int $tenantId): void protected function ensurePermissions(Menu $menu): void { - foreach ($this->actions() as $act) { - Permission::firstOrCreate([ + $actions = $this->actions(); + $permissionsData = []; + $now = now(); + + foreach ($actions as $act) { + $permissionsData[] = [ 'tenant_id' => (int) $menu->tenant_id, 'guard_name' => $this->guard, 'name' => "menu:{$menu->id}.{$act}", - ]); + 'created_at' => $now, + 'updated_at' => $now, + ]; } + + // Bulk insert using insertOrIgnore to avoid duplicate errors + DB::table('permissions')->insertOrIgnore($permissionsData); } protected function removePermissions(Menu $menu): void @@ -103,4 +113,12 @@ protected function forgetCache(): void { app(PermissionRegistrar::class)->forgetCachedPermissions(); } + + protected function forgetCacheAfterCommit(): void + { + // Defer cache clearing until after transaction commits + DB::afterCommit(function () { + app(PermissionRegistrar::class)->forgetCachedPermissions(); + }); + } } diff --git a/app/Services/RegisterService.php b/app/Services/RegisterService.php index 9023ed6..d55fa7f 100644 --- a/app/Services/RegisterService.php +++ b/app/Services/RegisterService.php @@ -85,21 +85,21 @@ public static function register(array $params): array 'description' => '시스템 관리자', ]); - // 8. Create permissions for each menu and assign to role - $permissions = []; + // 8. Get all permissions created by MenuObserver (menu:{id}.{action} pattern) + $permissionNames = []; + $actions = config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve']); + foreach ($menuIds as $menuId) { - $permName = "menu.{$menuId}"; - - // Use firstOrCreate to avoid duplicate permission errors - $perm = Permission::firstOrCreate([ - 'tenant_id' => $tenant->id, - 'guard_name' => 'api', - 'name' => $permName, - ]); - - $permissions[] = $perm; + foreach ($actions as $action) { + $permissionNames[] = "menu:{$menuId}.{$action}"; + } } + $permissions = Permission::whereIn('name', $permissionNames) + ->where('tenant_id', $tenant->id) + ->where('guard_name', 'api') + ->get(); + // 9. Assign all menu permissions to system_manager role $role->syncPermissions($permissions);