Laravel 데이터 caching 처리 하기

많은 트래픽을 보유한 사이트에서 db에서 데이터를 읽어올때 cache 처리해서 읽어오는 이유가 뭘까?

많은 트래픽이 발생하는 사이트에서는 대량의 요청을 동시에 처리해야 한다. 이런 요청들이 모두 데이터베이스에서 직접 데이터를 읽게 되면, 데이터베이스에 엄청난 부하가 발생하게 되고, 부하가 지나치게 증가하면 성능 저하 또는 서비스 중단이 발생할 수 있다.

그래서 이런 문제를 해결하기 위해 캐시를 사용하는데, 캐시는 빠르게 접근 가능한 저장소로, 한 번 읽은 데이터를 임시로 저장해 두는 것이다. 이후 동일한 데이터를 요청할 경우, 데이터베이스에 다시 접근하는 것이 아니라 캐시에서 데이터를 가져옴으로써, 데이터베이스의 부하를 줄이고 응답 시간을 향상시킬 수 있는 것이다.

특히, 변경이 자주 일어나지 않는 데이터나 반복적으로 읽는 데이터에 대해서는 캐시를 통해 큰 성능 향상을 기대할 수 있다.

 

라라벨에서 진행해주는 캐싱

 

Laravel에서는 캐싱을 처리하기 위해 다양한 도구와 기능을 제공한다. Laravel의 캐싱 메커니즘은 아래와 같은 종류의 캐시를 제공하는데 크게 아래 다섯가지 분류로 나눌 수 있다.

  1. Route Caching: 웹 응용 프로그램의 라우트를 캐시하는 데 사용되며, 라우트 구성이 변경되지 않는 경우 유용하다. php artisan route:cache 명령어를 사용해 라우트를 캐시할 수 있다.
  2. Config Caching: Laravel의 설정 파일들은 php artisan config:cache 명령어를 이용하여 캐시될 수 있는데, 이렇게 함으로써 애플리케이션의 부팅 시간을 단축시킬 수 있다.
  3. View Caching: 뷰 캐싱은 컴파일된 뷰 파일들을 캐시한다. Laravel은 자동으로 뷰를 컴파일하고 캐시하여 성능을 향상시키고 있다.
  4. Data Caching: 데이터 캐싱은 DB 쿼리 결과, 계산된 값 등과 같은 데이터를 캐시하는데 사용된다. 내가 이 포스팅에서 주로 다룰 캐싱 이야기도 여기에 속한다. Laravel의 캐시 파사드와 함께 Cache::put(), Cache::get(), Cache::remember() 등의 메서드를 사용해 데이터를 캐시하고 조회할 수 있다.
  5. Full Page Caching: Laravel에서는 HTTP 응답 자체를 캐시하여 전체 페이지 로드 시간을 줄일 수 있다. 이를 위해 spatie/laravel-responsecache와 같은 패키지를 사용할 수 있지만 캐시 갱신 정책과 관련된 로직을 잘 설계해야 할 필요가 있다.

캐싱은 애플리케이션의 성능을 향상시키지만, 데이터의 동기화나 캐시 관리 전략 등 주의해야 할 사항이 있으므로 적절히 사용해야 한다.

라라벨 DB 캐싱 구현하기

구현하기 전에 캐시를 사용할 때에도 주의할 점이 있다. 캐시와 데이터베이스 간의 동기화 문제가 발생할 수 있기 때문에 캐시 갱신 정책과 관련된 로직을 잘 설계해야 한다.

우리는 라라벨에서 데이터를 가져올 때 ORM인 Eloquent를 사용한다.

use App\Models\Flight;

$flights = Flight::where('active', 1)
               ->orderBy('name')
               ->get();

하지만 10000명의 사용자가 동시에 같은 조회를 한다면, 위에서 설명한 것과 같이 데이터베이스에 엄청난 부하가 걸릴 것이다. 그렇기 때문에 나는 아래와 같은 캐싱 전용 함수를 만들어 캐싱 처리를 진행한다.

function remember(string $key, $ttl, \Closure $callback) {
    $value = Cache::get($key);

    if (! is_null($value)) {
        return $value;
    }

    try {
        // 실행
        $value = $callback();

        // 캐싱
        Cache::set($key, $value, $ttl);

        return $value;
    } catch (\Exception $e) {
        Log::error('DB ERROR - '.$key.' - '.$e->getMessage());
    }

    return null;
}

위 함수를 아래와 같이 실행하면, key가 있을 경우 캐싱되어있는 value값을 반환하고, 아니라면 callback Closure의 return값을 value에 담아서 key로 캐싱해준다.

$flight = new Flight();

$value = $this->remember('findByFlightWhereActive1OrderByName', $code), 250, function () use ($flight) {
        $query = $flight->getQuery();
        $query->where('active', 1);
        $query->orderBy('name');

        return $query->get();
    });
}

하지만 아무리 이렇게 캐싱처리를 진행해주는 함수를 만들어도 단점이 있었다.

위 코드의 단점 및 보안

번잡히 CRUD가 이뤄지는 서비스의 경우, 캐싱된 캐시 데이터와 데이터베이스의 데이터 간의 동기화 문제가 발생할 수 있기 때문에 캐시 갱신 정책 혹은 예외처리를 잘 처리해야 했다. 그렇기 때문에 아래와 같이 보안해주었다.

function remember(string $key, $ttl, \Closure $callback) {

    // cache 불러오기
    try {
        $value = Cache::get($key);
    } catch (\Exception $e) {
        Log::error('Cache ERROR - '.$key.' - '.$e->getMessage());
        $value = Cache::driver('file')->get($key);
    }

    if (! is_null($value)) {
        return $value;
    }

    // 쿼리 실행
    $isError = false;
    try {
        $value = $callback();
    } catch (\Exception $e) {
        $isError = true;
        Log::error('DB ERROR - '.$key.' - '.$e->getMessage());
    }

    // cache 저장
    if (! is_null($value)) {
        try {
            Cache::set($key, $value, $ttl);
        } catch (\Exception $e) {
            Cache::driver('file')->set($key, $value, $ttl);
        }
        Cache::driver('file')->set($key.'_file', $value);
        return $value;
    }

    // DB 오류인 경우에 파일 캐시에서 가져온다
    if($isError) {
        return Cache::driver('file')->get($key.'_file', $value);
    }

    return null;
}

이렇게 함으로써 cache 혹은 db의 에러가 발생할 경우 어떤 에러인지 확인하여 재빠른 트러블 슈팅이 가능해졌다.