라라벨 성능 최적화: 속도 향상을 위한 실전 가이드
라라벨은 강력한 프레임워크이지만, 속도 면에서는 아쉬움을 남길 때가 있습니다. 하지만 걱정 마세요! 몇 가지 핵심적인 최적화 기법을 통해 라라벨 애플리케이션의 성능을 극적으로 향상시킬 수 있습니다. 이 글에서는 라라벨 앱의 속도를 높이는 데 도움이 되는 실질적인 방법들을 자세히 살펴보겠습니다.
최근 PHP 개발자들 사이에서 라라벨은 매우 인기 있는 프레임워크입니다. 라라벨의 빠른 개발 속도와 편리한 기능 덕분에 많은 개발자들이 라라벨을 선호하고 있습니다. 주니어 개발자는 물론 경험 많은 시니어 개발자까지 라라벨에 매력을 느끼고 있습니다. 라라벨은 PHP 생태계에 혁신적인 변화를 가져왔다는 사실은 누구도 부정할 수 없을 것입니다.
라라벨은 개발자들에게 편안한 개발 경험을 제공하기 위해 다양한 기능을 제공합니다. 하지만 이러한 편리함은 때때로 성능 저하로 이어지기도 합니다. 라라벨의 “마법 같은” 기능들은 실제로는 복잡한 코드 레이어들을 거치게 되며, 이로 인해 애플리케이션의 속도가 느려질 수 있습니다. 간단한 예외 추적만으로도 깊은 코드 구조를 확인할 수 있습니다.
예를 들어, 라라벨 애플리케이션에서 컴파일 오류가 발생했을 때, 실제 오류 원인을 찾기 위해 18개 이상의 함수 호출을 추적해야 할 수도 있습니다. 때로는 40개 이상의 함수 호출을 추적해야 할 때도 있습니다. 복잡한 라이브러리나 플러그인을 사용하면 이러한 함수 호출은 더욱 늘어날 수 있습니다. 이러한 코드 레이어는 라라벨을 느리게 만드는 주요 원인 중 하나입니다.
라라벨은 얼마나 느린가?
라라벨의 속도를 정확하게 측정하는 것은 쉽지 않습니다. 첫째, 웹 애플리케이션의 속도를 평가하는 데 있어 객관적이고 보편적으로 받아들여지는 기준이 없습니다. ‘느리다’ 혹은 ‘빠르다’는 상대적인 개념이며, 어떤 조건에서 측정하느냐에 따라 결과가 달라질 수 있습니다. 둘째, 웹 애플리케이션은 데이터베이스, 파일 시스템, 네트워크, 캐시 등 여러 요소에 의존하므로 속도에 대한 논의는 복잡해질 수밖에 없습니다. 예를 들어, 데이터베이스 속도가 느리다면 아무리 빠른 웹 앱이라도 전체 성능이 저하될 수밖에 없습니다.
이러한 불확실성 때문에 벤치마크가 자주 사용됩니다. 벤치마크는 의미가 없을 수도 있지만(참고: 이 글 과 이 글), 비교의 틀을 제공하고 상황을 이해하는 데 도움을 줄 수 있습니다. 따라서 약간의 주의를 기울여 PHP 프레임워크 간의 속도 차이에 대한 개략적인 아이디어를 얻어 봅시다.
GitHub의 벤치마크 프로젝트를 참조하면 PHP 프레임워크들의 성능 순위를 확인할 수 있습니다.
위 결과에서 라라벨은 최하위권에 속해 있는 것을 볼 수 있습니다. 물론, 여기에 나열된 프레임워크 중 일부는 실용적이지 않거나 유용하지 않을 수 있지만, 라라벨이 다른 인기 있는 프레임워크에 비해 상대적으로 느리다는 것을 알 수 있습니다. 하지만 걱정하지 마세요! 이 글은 불가능한 것에 대한 이야기가 아니라, 개선할 수 있는 것들에 대한 이야기입니다. 라라벨 앱의 성능을 향상시킬 수 있는 다양한 방법들을 알아볼 것입니다.
다행히도 라라벨 앱의 성능을 크게 향상시킬 수 있는 방법들이 많이 있습니다. 몇 배나 더 빠르게 만들 수 있다는 점을 잊지 마십시오. 동일한 코드베이스를 유지하면서 안정성을 높이고, 매달 인프라 비용을 절약할 수 있습니다. 지금부터 그 방법을 알아보겠습니다.
네 가지 유형의 최적화
저는 최적화를 네 가지 수준으로 분류하여 설명하겠습니다.
- 언어 수준: 더 빠른 버전의 PHP를 사용하고, 코드 속도를 저해하는 특정 언어 기능이나 코딩 스타일을 피하는 것을 의미합니다.
- 프레임워크 수준: 이 글에서 집중적으로 다룰 부분입니다. 라라벨 프레임워크 자체를 최적화하는 방법에 대한 내용입니다.
- 인프라 수준: PHP 프로세스 관리자, 웹 서버, 데이터베이스 등을 조정하여 성능을 향상시키는 것을 의미합니다.
- 하드웨어 수준: 더 빠르고 강력한 하드웨어 호스팅 환경으로 이동하여 성능을 개선하는 것을 의미합니다.
이러한 모든 유형의 최적화는 각각 중요한 역할을 합니다. 하지만 이 글에서는 특히 ‘프레임워크 수준’의 최적화에 초점을 맞출 것입니다. 라라벨 프레임워크 자체를 개선하여 성능을 높이는 방법에 대해 자세히 알아보겠습니다.
참고로, 위에서 언급한 최적화 수준에 대한 번호 매기기에는 특별한 근거가 없습니다. 제가 임의로 분류한 것입니다. 그러므로 “우리 서버에는 3단계 최적화가 필요하다”와 같은 표현은 사용하지 않도록 주의하세요! 혹시라도 그렇게 말하면 팀 리더에게 혼나게 될지도 모릅니다. 😉
이제부터 본격적으로 라라벨 성능 최적화의 세계로 들어가 보겠습니다.
n+1 데이터베이스 쿼리 문제에 주의하세요
n+1 쿼리 문제는 ORM(Object-Relational Mapping)을 사용할 때 흔히 발생하는 문제입니다. 라라벨은 Eloquent라는 강력한 ORM을 제공하지만, 사용이 너무 편리한 나머지 실제 내부에서 어떤 일이 일어나고 있는지 간과하기 쉽습니다.
일반적인 시나리오를 생각해 봅시다. 특정 고객 목록에 대한 주문 목록을 표시해야 하는 경우입니다. 전자상거래 시스템이나 엔티티 간의 관계를 보여줘야 하는 리포트 인터페이스에서 흔히 볼 수 있는 상황입니다.
다음은 라라벨에서 이를 처리하기 위한 컨트롤러 코드 예시입니다.
class OrdersController extends Controller { // ... public function getAllByCustomers(Request $request, array $ids) { $customers = Customer::findMany($ids); $orders = collect(); // 새로운 컬렉션 생성 foreach ($customers as $customer) { $orders = $orders->merge($customer->orders); } return view('admin.reports.orders', ['orders' => $orders]); } }
이 코드는 매우 우아해 보이지만, 라라벨에서 성능을 저해하는 방식입니다. ORM을 사용하여 고객 목록을 가져오는 코드는 다음과 같은 SQL 쿼리를 생성합니다.
SELECT * FROM customers WHERE id IN (22, 45, 34, ...);
이 쿼리 결과는 컨트롤러 내부의 `$customers` 컬렉션에 저장됩니다. 다음으로 각 고객을 반복하면서 해당 고객의 주문을 가져옵니다. 이 과정에서 다음과 같은 쿼리가 실행됩니다.
SELECT * FROM orders WHERE customer_id = 22;
이 쿼리는 고객 수만큼 반복 실행됩니다. 즉, 1000명의 고객에 대한 주문 데이터를 가져오려면 총 1001개의 쿼리(1개의 고객 쿼리 + 1000개의 주문 쿼리)가 실행됩니다. 이것이 바로 n+1 문제의 핵심입니다.
이보다 더 효율적으로 처리할 수 있을까요? 물론입니다! Eloquent의 Eager Loading 기능을 사용하면 ORM이 JOIN 쿼리를 생성하여 필요한 모든 데이터를 한 번의 쿼리로 가져올 수 있습니다. 다음 코드를 살펴보세요.
$orders = Customer::findMany($ids)->with('orders')->get();
이 코드는 다음과 같은 단일 SQL 쿼리를 생성합니다.
SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (22, 45, ...);
단 하나의 쿼리로 모든 필요한 데이터를 가져올 수 있으므로 수천 개의 쿼리를 실행하는 것보다 훨씬 효율적입니다. 처리해야 할 고객 수가 10,000명이라면 n+1 쿼리 문제가 얼마나 심각한지 쉽게 짐작할 수 있을 것입니다. Eager Loading은 거의 항상 현명한 선택입니다.
구성을 캐시하세요!
라라벨의 유연성은 수많은 설정 파일에서 비롯됩니다. 파일 저장 방식이나 큐 드라이버 설정을 변경하려면 `config/filesystems.php` 또는 `config/queue.php`와 같은 설정 파일을 수정하면 됩니다. 라라벨에는 13개가 넘는 다양한 설정 파일이 제공되므로 원하는 대로 시스템을 사용자 정의할 수 있습니다.
하지만 PHP의 특성상 새로운 웹 요청이 들어올 때마다 라라벨은 모든 설정 파일을 다시 읽고 해석합니다. 만약 최근에 설정이 변경되지 않았다면, 이러한 과정은 불필요한 낭비일 것입니다. 모든 요청마다 설정을 다시 로드하는 것은 피해야 할 작업입니다. 라라벨은 이를 해결하기 위해 간단한 명령어를 제공합니다.
php artisan config:cache
이 명령어는 모든 설정 파일을 하나의 캐시 파일로 결합하고, 다음에 요청이 들어오면 이 단일 파일을 읽어서 사용합니다. 이렇게 하면 불필요한 파일 읽기 및 해석 작업을 줄여 성능을 향상시킬 수 있습니다.
하지만 설정 캐싱에는 몇 가지 주의 사항이 있습니다. 가장 큰 문제는 설정 캐시를 활성화하면 설정 파일 외부에서 `env()` 함수 호출이 `null`을 반환한다는 것입니다. 왜냐하면 설정 캐싱은 “설정이 완료되었고 더 이상 변경하지 않을 것이다”라는 의미이기 때문입니다. 즉, `.env` 파일에 정의된 환경 변수는 설정 캐싱 후에는 더 이상 사용할 수 없습니다.
따라서 설정 캐싱을 적용할 때 다음 규칙을 엄격하게 지켜야 합니다.
- 프로덕션 환경에서만 사용하십시오.
- 설정을 변경하지 않을 것이 확실할 때만 사용하십시오.
- 문제가 발생하면 `php artisan cache:clear` 명령어를 사용하여 설정 캐시를 삭제하십시오.
- 설정 캐시를 사용하기 전에 반드시 개발 환경에서 충분히 테스트하십시오.
자동 로드 서비스 줄이기
라라벨은 부팅 시 여러 서비스를 자동으로 로드합니다. 이러한 서비스는 `config/app.php` 파일의 `providers` 배열에 정의되어 있습니다. 다음은 서비스 제공자 목록의 예시입니다.
/* |-------------------------------------------------------------------------- | Autoloaded Service Providers |-------------------------------------------------------------------------- | | The service providers listed here will be automatically loaded on the | request to your application. Feel free to add your own services to | this array to grant expanded functionality to your applications. | */ 'providers' => [ /* * Laravel Framework Service Providers... */ IlluminateAuthAuthServiceProvider::class, IlluminateBroadcastingBroadcastServiceProvider::class, IlluminateBusBusServiceProvider::class, IlluminateCacheCacheServiceProvider::class, IlluminateFoundationProvidersConsoleSupportServiceProvider::class, IlluminateCookieCookieServiceProvider::class, IlluminateDatabaseDatabaseServiceProvider::class, IlluminateEncryptionEncryptionServiceProvider::class, IlluminateFilesystemFilesystemServiceProvider::class, IlluminateFoundationProvidersFoundationServiceProvider::class, IlluminateHashingHashServiceProvider::class, IlluminateMailMailServiceProvider::class, IlluminateNotificationsNotificationServiceProvider::class, IlluminatePaginationPaginationServiceProvider::class, IlluminatePipelinePipelineServiceProvider::class, IlluminateQueueQueueServiceProvider::class, IlluminateRedisRedisServiceProvider::class, IlluminateAuthPasswordsPasswordResetServiceProvider::class, IlluminateSessionSessionServiceProvider::class, IlluminateTranslationTranslationServiceProvider::class, IlluminateValidationValidationServiceProvider::class, IlluminateViewViewServiceProvider::class, /* * Package Service Providers... */ /* * Application Service Providers... */ AppProvidersAppServiceProvider::class, AppProvidersAuthServiceProvider::class, // AppProvidersBroadcastServiceProvider::class, AppProvidersEventServiceProvider::class, AppProvidersRouteServiceProvider::class, ],
위 예시에서는 27개의 서비스가 로드되고 있습니다. 애플리케이션에 따라 이러한 서비스가 모두 필요할 수도 있지만, 그렇지 않은 경우도 많습니다. 예를 들어, REST API를 구축하는 경우 세션 서비스, 뷰 서비스, 인증 서비스, 페이지네이션 서비스, 번역 서비스 등은 필요하지 않을 수 있습니다. 필요하지 않은 서비스 제공자를 비활성화하면 애플리케이션 부팅 시간을 단축할 수 있습니다.
하지만 서비스 제공자를 비활성화하기 전에 반드시 해당 서비스가 애플리케이션에서 사용되는지 여부를 확인해야 합니다. 서비스 제공자를 잘못 비활성화하면 애플리케이션 오류가 발생할 수 있습니다. 프로덕션 환경에 변경 사항을 적용하기 전에 개발 환경과 스테이징 환경에서 철저히 테스트해야 합니다.
미들웨어 스택을 현명하게 사용하세요
들어오는 웹 요청에 대한 사용자 정의 처리가 필요한 경우 미들웨어를 사용하는 것이 일반적입니다. `app/Http/Kernel.php` 파일에서 웹 또는 API 스택에 미들웨어를 추가할 수 있습니다. 미들웨어를 전역적으로 추가하면 애플리케이션 전체에서 쉽게 사용할 수 있지만, 지나치게 많은 미들웨어를 전역적으로 사용하는 것은 성능 저하의 원인이 될 수 있습니다.
애플리케이션이 커질수록 모든 요청에 불필요한 미들웨어가 실행될 수 있습니다. 따라서 미들웨어를 추가할 때는 필요한 곳에만 적용하는 것이 중요합니다. 미들웨어를 선택적으로 적용하는 것이 번거로울 수 있지만, 장기적으로는 성능 향상에 큰 도움이 될 것입니다.
ORM을 피하세요 (때로는)
Eloquent는 데이터베이스 작업을 간편하게 만들어주지만, 속도 측면에서는 단점이 있을 수 있습니다. ORM은 단순히 데이터베이스에서 레코드를 가져오는 것뿐만 아니라 모델 객체를 인스턴스화하고 데이터로 채우는 작업도 수행합니다. 만약 10,000명의 사용자를 가져온다면 데이터베이스에서 10,000개의 행을 가져오고, 내부적으로 10,000개의 새로운 `User()` 객체를 생성하고 각 객체의 속성을 데이터로 채우게 됩니다.
따라서 데이터베이스가 애플리케이션의 병목 현상인 경우 ORM을 우회하는 것이 좋은 선택일 수 있습니다. 복잡한 SQL 쿼리를 실행해야 하는 경우, Eloquent를 사용하는 대신 `DB::raw()`를 사용하여 직접 쿼리를 작성하는 것이 더 효율적일 수 있습니다. 연구 결과에 따르면, 단순 삽입 작업에서도 Eloquent는 데이터가 증가할수록 성능이 크게 저하되는 것을 확인할 수 있습니다.
가능한 한 캐싱을 활용하세요
웹 애플리케이션 성능 최적화의 핵심은 캐싱입니다. 캐싱은 계산 비용이 많이 드는 결과를 미리 계산하고 저장하여 동일한 쿼리가 반복될 때 저장된 결과를 즉시 반환하는 것을 의미합니다. 예를 들어, 전자 상거래 사이트에서 특정 가격 범위와 연령 그룹에 대한 제품을 자주 검색하는 경우, 데이터베이스를 계속 쿼리하는 대신 검색 결과를 캐싱하는 것이 좋습니다.
라라벨은 다양한 유형의 캐싱을 내장하고 있습니다. 캐싱 드라이버를 사용하거나, 모델 캐싱, 쿼리 캐싱 등을 지원하는 패키지를 사용할 수 있습니다. 하지만 특정 단순화된 사용 사례를 넘어 미리 빌드된 캐싱 패키지는 해결하는 것보다 더 많은 문제를 일으킬 수 있습니다.
메모리 내 캐싱을 선호하세요
라라벨에서 캐싱을 할 때 캐시 데이터를 저장할 위치를 선택할 수 있습니다. 캐시 드라이버를 통해 파일 시스템, 데이터베이스, 메모리 등 다양한 저장소를 선택할 수 있습니다. 파일 시스템에 캐시 데이터를 저장하는 것도 가능하지만, 캐싱의 본질적인 목적을 고려할 때 메모리 내 캐싱을 사용하는 것이 이상적입니다.
Redis, Memcached, MongoDB와 같은 메모리 내 캐시 시스템을 사용하면 캐시 자체가 병목 현상이 되는 것을 방지할 수 있습니다. SSD 디스크를 사용하면 RAM과 거의 동일한 성능을 낼 수 있다고 생각할 수도 있지만, 실제로는 큰 차이가 있습니다. 벤치마크 결과에 따르면, RAM은 SSD보다 10~20배 더 빠른 것으로 나타났습니다.
캐싱 시스템으로 Redis를 추천합니다. Redis는 매우 빠르고(초당 10만 회 이상의 읽기 작업 가능) 대규모 캐싱 시스템을 쉽게 클러스터링 할 수 있습니다.
경로 캐시를 활용하세요
애플리케이션 구성과 마찬가지로 라우트도 자주 변경되지 않으므로 캐싱을 위한 이상적인 대상입니다. 특히 `web.php`와 `api.php` 파일을 여러 파일로 분할하는 경우, 경로 캐싱을 사용하면 성능 향상에 큰 도움이 됩니다. 다음 명령어를 사용하여 라우트를 캐싱할 수 있습니다.
php artisan route:cache
라우트를 변경한 후에는 다음 명령어를 사용하여 캐시를 지워야 합니다.
php artisan route:clear
이미지 최적화 및 CDN 활용
이미지는 대부분의 웹 애플리케이션에서 중요한 요소이지만, 대역폭 소모가 크고 웹사이트 속도 저하의 주요 원인이기도 합니다. 업로드된 이미지를 서버에 그대로 저장하고 HTTP 응답으로 전송하는 것은 많은 최적화 기회를 놓치는 것입니다.
이미지를 로컬 서버에 저장하는 대신 Cloudinary와 같은 서비스를 사용하는 것을 추천합니다. Cloudinary는 즉석에서 이미지 크기를 자동으로 조정하고 최적화합니다. Cloudinary와 같은 서비스가 불가능하다면, Cloudflare와 같은 CDN을 사용하여 이미지를 캐싱하고 전송할 수 있습니다. 또한 웹 서버 설정을 조정하여 자산을 압축하고 브라우저가 항목을 캐싱하도록 지시하면 성능을 크게 향상시킬 수 있습니다. 다음은 Nginx 설정 예시입니다.
server { # file truncated # gzip compression settings gzip on; gzip_comp_level 5; gzip_min_length 256; gzip_proxied any; gzip_vary on; # browser cache control location ~* .(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ { expires 1d; access_log off; add_header Pragma public; add_header Cache-Control "public, max-age=86400"; } }
이미지 최적화는 라라벨 자체와 직접적인 관련은 없지만, 매우 간단하고 강력한 최적화 방법이므로 반드시 고려해야 합니다.
오토로더 최적화
자동 로딩은 PHP의 중요한 기능이지만, 고성능 프로덕션 환경에서는 클래스를 찾고 로드하는 데 시간이 걸릴 수 있습니다. 라라벨은 오토로더 최적화를 위한 간단한 명령어를 제공합니다.
composer install --optimize-autoloader --no-dev
대기열을 활용하세요
대기열은 시간이 오래 걸리는 작업을 비동기적으로 처리하는 데 유용한 기능입니다. 이메일 전송은 대기열을 사용하기 좋은 예시입니다. 웹 애플리케이션에서 사용자가 특정 작업을 수행할 때 여러 개의 알림 이메일을 보내야 하는 경우가 있습니다. 만약 이메일 게이트웨이가 500ms 내에 SMTP 요청에 응답한다고 가정하면, 6-7개의 이메일을 보내는 데 3-4초가 걸릴 수 있습니다. 이로 인해 사용자 경험이 저해될 수 있습니다.
이러한 문제를 해결하기 위해 대기열을 사용하면 들어오는 작업을 저장하고, 사용자에게 작업이 완료되었다는 메시지를 즉시 표시하고, 나중에 작업을 처리할 수 있습니다. 만약 오류가 발생하면 작업이 실패하기 전에 몇 번 다시 시도할 수도 있습니다.
출처: Microsoft.com
대기열 시스템은 설정을 약간 복잡하게 만들지만, 현대 웹 애플리케이션에서는 필수적인 요소입니다.
자산 최적화 (Laravel Mix)
라라벨 애플리케이션의 프런트엔드 자산을 처리하기 위해 컴파일 및 축소 파이프라인을 설정해야 합니다. Webpack, Gulp, Parcel과 같은 번들러 시스템을 사용하는 데 익숙한 사람들은 별도의 설정이 필요 없지만, 그렇지 않은 경우에는 Laravel Mix를 사용하는 것을 추천합니다.
Mix는 Webpack을 감싸는 가볍고 사용하기 쉬운 래퍼입니다. Mix는 CSS, SASS, JS 파일을 쉽게 처리할 수 있으며, Vue 및 React 구성 요소도 처리할 수 있습니다. 다음은 간단한 `mix.js` 파일 예시입니다.
const mix = require('laravel-mix'); mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css');
이 설정 파일을 사용하면 `npm run production` 명령어를 실행할 때 자동으로 번들링, 축소, 최적화 작업을 수행할 수 있습니다. 자세한 내용은 여기를 참조하십시오.
결론
성능 최적화는 과학보다는 예술에 가깝습니다. 무엇을 해야 하는지뿐만 아니라 어떻게, 얼마나 해야 하는지를 아는 것이 중요합니다. 라라벨 애플리케이션의 성능을 최적화할 수 있는 방법은 무궁무진합니다. 하지만 어떤 방법을 선택하든 다음 조언을 기억하십시오. 최적화는 분명한 이유가 있을 때 수행되어야 하며, 단지 그럴듯하게 들린다는 이유나 10만 명 이상의 사용자를 위한 앱 성능을 과도하게 걱정해서는 안 됩니다.
애플리케이션 최적화가 필요한지 확신이 서지 않는다면, 굳이 서두를 필요는 없습니다. 최적화되지 않았지만, 요구 사항에 맞게 제대로 작동하는 애플리케이션은 최적화되었지만 때때로 실패하는 애플리케이션보다 훨씬 바람직합니다.
라라벨을 배우고 싶다면 온라인 강좌를 확인해보십시오.
라라벨 애플리케이션의 속도 향상에 이 글이 도움이 되었기를 바랍니다! 🙂