perf: 회원가입 쿼리 최적화 (268개 → 58개, 78% 감소)

- MenuObserver: Bulk insert + 지연 캐시 삭제로 메뉴당 28개 → 3개 쿼리
- RegisterService: 중복 권한 생성 로직 제거 (27개 쿼리 감소)
- 캐시 삭제: 126개 → 11개 (91% 감소)
- 확장성 유지: 관리자 메뉴 추가 시에도 최적화 적용
This commit is contained in:
2025-11-10 10:30:35 +09:00
parent c558606954
commit 657623fef5
3 changed files with 293 additions and 19 deletions

View File

@@ -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 (일) - 회원가입 메뉴 생성 오류 수정 및 검증 에러 처리 개선
### 주요 작업