Laravel에서 N+1 문제 추적 및 불필요한 쿼리 제거하기

성능 최적화 작업은 종종 지루합니다.

항상 지연 시간을 80%나 줄이는 영웅적인 이야기만 있는 것은 아닙니다. 대부분의 날은 정리하는 작업입니다. 자신도 모르게 실행되고 있던 쿼리를 찾아내고, 그것을 삭제하는 일이죠.

Laravel 앱이 성장하고 있다면, 아마도 다음과 같은 세 가지 문제에 직면해 있을 것입니다.

  1. 숨겨진 N+1 쿼리

보이지 않는 것은 고칠 수 없습니다. 로컬 환경에서 beyondcode/laravel-query-detector와 같은 도구를 사용하세요. 이 도구는 루프 내에서 지연 로딩(lazy-load) 패턴을 사용할 때 이를 알려줍니다.

프로덕션 환경에서는 실행하지 마세요. 오버헤드가 발생합니다. 앱이 로컬 또는 테스트 모드일 때만 등록하여 사용하세요.

  1. 과도한 Eager Loading

사람들은 흔히 with()를 추가하여 N+1 문제를 해결하려 합니다. 하지만 이는 새로운 문제를 야기합니다. 뷰(view)에서 더 이상 사용하지 않는 관계를 eager-load 할 수도 있기 때문입니다.

리디자인으로 인해 테이블에서 특정 컬럼이 제거되었다면, 컨트롤러에서도 그에 해당하는 with() 호출을 제거해야 합니다. 버려지는 데이터를 위해 비용을 지불하고 있는 셈이니까요.

모든 eager load를 하나의 요청(claim)으로 간주하세요. 데이터를 사용하지 않는다면, 그 요청을 제거해야 합니다.

  1. 동일한 데이터의 반복 계산

어떤 값들은 요청 전체 과정 동안 변하지 않습니다. 이런 값을 여러 번 계산하면 CPU를 낭비하게 됩니다. 이를 해결하기 위해 메모이제이션(memoization)을 사용하세요.

예시:

protected ?string $defaultConnection = null;

public function getDefaultConnectionName(): string
{
    return $this->defaultConnection ??= $this->resolveDefaultConnection();
}

??= 연산자는 값을 한 번만 계산하고 나머지 요청 과정 동안 재사용합니다.

  1. 대시보드 쿼리 함정

대시보드는 종분 각 카드마다 별도의 count() 쿼리를 실행합니다. 카드가 6개라면 6개의 쿼리가 실행되는 것이죠.

대신 다음 두 가지를 수행하세요:

  • 카운트를 그룹화하세요. 하나의 테이블에 대한 모든 합계를 단일 쿼리로 가져오세요.
  • 짧은 캐시를 사용하세요. 대시보드 통계가 초 단위로 실시간일 필요는 없습니다. 5분 정도 캐싱하세요.

한 명의 방문자가 쿼리 비용을 지불하면, 나머지 모든 사용자는 캐시된 결과를 받게 됩니다.

마지막 팁: 회귀(Regression) 방지

개발자가 나중에 나쁜 코드를 다시 추가한다면 정리 작업은 실패한 것입니다. Pest를 사용하여 쿼리 수를 검증(assert)하세요. 페이지에서 실행할 수 있는 최대 쿼리 수에 상한선을 설정하세요. 개발자가 N+1을 추가하면 테스트가 실패할 것입니다.

최적화는 대부분 '제거'에 관한 것입니다. 사용하지 않는 로드를 제거하세요. 반복 계산되는 값을 제거하세요. 불필요한 쿼리를 제거하세요.

Source: https://dev.to/nasrulhazim/a-day-of-performance-hardening-hunting-n1s-and-killing-wasted-queries-in-laravel-568p