실행된 SQL 목록 */ protected function captureQueries(\Closure $callback): array { $queries = []; DB::listen(function (QueryExecuted $query) use (&$queries) { $queries[] = $query->sql; }); try { $callback(); } finally { // 리스너는 커넥션에 누적되므로, 다음 측정이 이전 기록을 물려받지 않도록 끊는다. DB::getEventDispatcher()?->forget(QueryExecuted::class); } return $queries; } /** * 클로저를 실행하며 발생한 쿼리 수를 셉니다. * * @param \Closure $callback 측정 대상 * @return int 실행된 쿼리 수 */ protected function countQueries(\Closure $callback): int { return count($this->captureQueries($callback)); } /** * 데이터 규모를 바꿔도 쿼리 수가 늘지 않는지 단언합니다. * * 행을 늘리는 일(`$grow`)은 측정 밖에서 수행하고, 측정은 조회(`$measure`)만 감쌉니다. * * @param \Closure $measure 측정할 조회 (쿼리 수를 재는 대상) * @param \Closure $grow 데이터를 늘리는 작업 (측정에 포함되지 않음) * @param string $context 실패 메시지에 붙일 설명 */ protected function assertQueryCountStableAsDataGrows(\Closure $measure, \Closure $grow, string $context = ''): void { $before = $this->countQueries($measure); $grow(); $after = $this->countQueries($measure); $label = $context !== '' ? $context.': ' : ''; $this->assertLessThanOrEqual( $before, $after, $label."행 수를 늘렸더니 쿼리 수가 {$before} → {$after} 로 늘었다. " .'조회가 행마다 쿼리를 내고 있다(N+1) — 관계를 eager load 하거나 일괄 조회로 바꾼다' ); } }