개발자들은 종종 제게 똑같은 질문을 합니다. "제 API가 느려요. 어디서부터 시작해야 하죠?" 보통은 서버를 업그레이드하거나 RAM을 두 배로 늘리는 것이 반사적인 대응입니다. 하지만 이는 비용만 발생시킬 뿐 근본적인 원인을 해결하는 경우는 드뭅니다. 대부분의 Laravel 애플리케이션에서 병목 현상은 데이터베이스 계층에서 발생합니다. 프레임워크의 우아한 문법 때문에 각 Eloquent 호출이 결국 SQL로 변환된다는 사실과, 그 SQL이 종종 문제의 시작점이라는 사실을 잊기 쉽습니다.
서버 설정을 건드리기 전에, 쿼리를 체계적으로 검토하십시오.
올바른 진단부터 시작하세요
막연하게 최적화하지 마세요. 무작위로 쿼리를 다시 작성하는 것은 추측일 뿐이며, 추측은 시간을 낭비하게 만듭니다.
가장 많은 총 시간을 소비하는 문장을 찾아야 합니다. 실제 부하가 걸리는 상황에서 애플리케이션을 관찰하세요. Laravel Telescope를 사용하면 요청 중에 실행된 모든 쿼리를 실행 시간과 함께 깔끔하게 볼 수 있습니다. Laravel Debugbar는 로컬 개발 중에 브라우저에 쿼리를 표시하여 이상 징후를 즉시 파악할 수 있게 해줍니다. 프로덕션 환경에서 문제를 포착해야 할 때는 MySQL Slow Query Log를 활성화하세요. 이는 정의한 임계값을 초과하는 문장을 기록하므로, 작은 데이터셋에서는 나타나지 않는 예상치 못한 문제를 찾는 데 이상적입니다. 더 큰 규모의 서비스를 운영 중이라면, Application Performance Monitoring(APM) 도구를 사용하여 느린 HTTP 엔드포인트와 특정 데이터베이스 호출 간의 상관관계를 파악할 수 있습니다.
데이터를 검토할 때 두 가지를 확인하세요: 절대적인 실행 시간과 호출 빈도입니다. 40밀리초가 걸리는 쿼리는 별것 아닌 것처럼 들리지만, 분당 2,000번 실행된다는 사실을 알게 되면 이야기가 달라집니다. 한 시간에 한 번 실행되는 3초짜리 보고서보다, 모든 페이지에서 실행되는 0.5초짜리 조회 작업이 더 중요할 수 있습니다. 영향력이 큰 문제부터 먼저 해결하세요.
모든 것을 요청하지 마세요
SELECT *은 편리합니다. 하지만 비용이 많이 듭니다. Model::all()을 작성하거나 컬럼명을 지정하지 않고 결과 세트를 가져오면, MySQL은 일치하는 모든 행에 대해 모든 필드를 끌어옵니다. 여기에는 대용량 텍스트 필드, JSON 블롭(blob), 그리고 테이블에 있는 다른 모든 데이터가 포함됩니다. 결과 세트가 커지면 메모리 사용량이 증가하고, 응답을 직렬화(serializing)하는 데 걸리는 시간도 늘어납니다.
명시적으로 작성하세요. 컨트롤러에 id, name, email 필드만 필요하다면, 정확히 그 필드들만 요청하세요.
User::select('id', 'name', 'email')->get();
쿼리 빌더에서도 동일한 원칙이 적용됩니다. 페이로드가 작을수록 네트워크를 통해 더 빠르게 이동하고 애플리케이션 서버의 RAM을 적게 사용합니다. 이는 가장 적은 비용으로 얻을 수 있는 이점 중 하나이지만, Laravel이 SELECT *을 기본 동작으로 만들기 때문에 쉽게 간과되곤 합니다.
EXPLAIN을 통해 변경 사항을 가이드하세요
EXPLAIN을 먼저 실행해 보지 않고 느린 쿼리를 리팩터링하지 마세요. MySQL에서 EXPLAIN 키워드는 쿼리 실행 계획을 보여줍니다. 옵티마이저가 데이터를 찾는 의도를 정확히 드러냅니다.
type 컬럼에 주의를 기울이세요. 만약 ALL이 보인다면, MySQL이 전체 테이블 스캔(full table scan)을 수행하고 있다는 뜻입니다. 이는 WHERE 절을 충족하기 위해 모든 행을 읽고 있음을 의미합니다. key 컬럼을 확인하여 옵티마이저가 인덱스를 사용하고 있는지 확인하세요. 그런 다음 Extra 컬럼을 확인합니다. 만약 Using temporary나 Using filesort가 발견된다면, 현재 구조가 쿼리를 깔끔하게 처리할 수 없어서 MySQL이 중간 테이블을 생성하거나 메모리에서 정렬을 수행하고 있다는 뜻입니다.
MySQL 클라이언트에서 EXPLAIN을 실행하거나 출력을 보기 좋게 포맷팅해 주는 도구를 사용하세요. 실행 계획을 확인하고 나면, 문제가 누락된 인덱스 때문인지, 잘못된 조인(join) 때문인지, 아니면 엔진이 최적화할 수 없는 조건(predicate) 때문인지 알 수 있습니다. 더 이상 추측할 필요가 없습니다.
의도를 가지고 인덱스를 생성하세요
인덱스는 조회를 빠르게 만드는 가장 강력한 도구이지만, 쿼리 방식과 일치할 때만 작동합니다. 적절한 인덱스가 없으면 MySQL은 행을 하나씩 스캔합니다. 데이터가 천 개뿐인 개발 환경에서는 괜찮아 보일 수 있지만, 데이터가 천만 개인 프로덕션 환경에서는 시스템이 무너질 수 있습니다.
WHERE 절에 자주 등장하는 필드에 대해 단일 컬럼 인덱스부터 시작하세요. status로 지속적으로 필터링한다면, status에 인덱스를 추가하세요.
쿼리가 여러 컬럼을 함께 필터링하는 경우, 복합 인덱스(composite index)로 넘어가세요. MySQL은 복합 인덱스를 왼쪽에서 오른쪽 방향으로 읽기 때문에 인덱스 내 컬럼의 순서가 중요합니다. 이를 '가장 왼쪽 접두사 규칙(leftmost prefix rule)'이라고 합니다. 만약 쿼리가 user_id로 검색한 다음 created_at으로 정렬한다면, (user_id, created_at)에 대한 복합 인덱스가 큰 도움이 됩니다. 순서를 바꾸면 옵티마이저가 필터링을 위해 인덱스를 전혀 사용하지 않을 수도 있습니다.
모든 컬럼에 인덱스를 걸지 마세요. MySQL이 구조를 유지해야 하므로 각 인덱스는 삽입(insert), 수정(update), 삭제(delete) 시 오버헤드를 추가합니다. 측정 과정에서 발견한 패턴을 바탕으로 신중하게 인덱스를 추가하세요.
컬럼에 함수를 사용하지 마세요
이 실수는 인덱스를 조용히 비활성화합니다. WHERE 절 내부에서 컬럼을 함수로 감싸면, MySQL은
