Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

클라이언트 측 캐싱으로 Rails 성능 개선하기: 조건부 GET 요청 완벽 가이드

러시안 돌 캐싱(Russian doll caching) 외에도 Rails 애플리케이션의 성능을 높일 수 있는 방법은 다양합니다. 이번 글에서는 Rails에 내장된 조건부 GET(Conditional GET) 지원 기능을 살펴보겠습니다. 이 기능을 활용하면 렌더링된 페이지를 사용자의 브라우저 캐시에 저장할 수 있습니다.

👋 캐싱 외적인 성능 최적화에 관심이 있다면, 저희가 작성한 Ruby(on Rails) 성능 관련 글들도 확인해 보세요. Ruby 성능 모니터링 체크리스트도 함께 준비되어 있습니다.

ETag와 Last-Modified 헤더

브라우저가 Rails 애플리케이션의 특정 페이지에 대해 HTTP GET 요청을 보내면, 라우터는 해당 요청을 컨트롤러 액션과 연결합니다. 이후 컨트롤러가 데이터베이스에서 필요한 데이터를 조회하고 뷰를 렌더링합니다. 그런 다음 200 OK 응답 코드와 함께 렌더링된 HTML을 응답 본문에 담아 브라우저로 전송하면, 브라우저가 이를 파싱해 화면에 표시합니다.

동일한 리소스가 다시 요청되면 같은 파이프라인을 반복하게 됩니다. 하지만 그 사이에 페이지가 변경되지 않았다면 이 과정은 불필요합니다. 바로 이런 경우를 위해 HTTP는 ETagLast-Modified 헤더를 제공합니다. 이 헤더들을 활용하면 브라우저가 응답 본문을 저장해 두었다가, 내용이 오래되었을 때 헤더 정보를 기준으로 무효화할 수 있습니다.

ETag(Entity Tag)는 클라이언트 측 캐시 유효성 검증에 사용되는 값으로, HTTP 응답의 캐시 키라고 생각하면 이해하기 쉽습니다. 매 요청마다 HTTP 응답 헤더를 통해 브라우저로 전달됩니다.

~ $ curl -I https://localhost:3000/products/1
HTTP/1.1 200 OK
...
ETag: W/"9462d76cc55aeb6249fa990e39231c7c"
Last-Modified: Wed, 25 Apr 2018 08:27:04 GMT
...

나중에 동일한 응답이 다시 요청되면, 브라우저는 자신의 캐시에서 해당 응답을 찾고 이전 요청에서 저장해 둔 ETag를 If-None-Match 헤더로 전달합니다. 이 헤더는 Rails 애플리케이션에게 "캐시에 이미 이 버전이 있다"는 사실을 알려주는 역할을 합니다.

요청에 포함된 ETag가 현재 ETag와 일치하면, Rails는 응답 본문 없이 304 Not Modified 응답만 전송합니다. 이 응답을 받은 브라우저는 서버에서 데이터를 다시 받는 대신 로컬 캐시에 저장된 버전을 사용하게 됩니다.

~ $ curl -i -H 'If-None-Match: W/"9462d76cc55aeb6249fa990e39231c7c"' https://localhost:3000/products/1
HTTP/1.1 304 Not Modified
...
ETag: W/"9462d76cc55aeb6249fa990e39231c7c"
Last-Modified: Wed, 25 Apr 2018 08:27:04 GMT
...

Rails의 조건부 GET 요청

로컬 Rails 애플리케이션에서 페이지를 요청해 보면, Rails가 매 요청마다 자동으로 ETag를 추가하는 것을 확인할 수 있습니다. 같은 페이지를 연속해서 여러 번 요청하면 매번 ETag 값이 달라지는 것도 볼 수 있습니다.

Rails는 기본적으로 매 요청마다 ETag를 생성하지만, 이때 전체 응답 본문의 다이제스트(digest)를 사용합니다. 즉, 레이아웃의 <%= csrf_meta_tags %> 때문에 ETag가 무효화됩니다. CSRF 토큰 메타 태그는 요청마다 값이 바뀌기 때문입니다. 이로 인해 매 요청마다 응답 본문이 달라지고, ETag는 무효화되며, 로컬 캐시는 항상 오래된(stale) 상태로 표시됩니다.

게다가 Rails는 기본적으로 컨트롤러에서 로컬 캐시를 명시적으로 신선한(fresh) 상태로 표시하지 않기 때문에, 아무 설정 없이는 절대 304 Not Modified를 반환하지 않습니다.

fresh_whenstale?

요청 헤더의 ETag를 조건부 GET에 활용하려면, 로컬 캐시의 객체를 명시적으로 "신선한(fresh)" 상태로 표시해야 합니다. 예를 들어 상품을 보여주는 페이지라면, 상품 데이터와 뷰 템플릿이 변경되지 않는 한 캐시를 유효하게 유지할 수 있습니다. 이를 구현하려면 두 가지 작업이 필요합니다.

  1. ETag를 구성할 값을 명시적으로 설정합니다. 전체 응답 본문을 기준으로 삼으면 캐시된 응답의 유효성을 확인하기 위해 본문 전체를 렌더링해야 하므로, 로컬 캐싱으로 얻는 속도 향상 효과가 사라집니다.
  2. 뷰를 렌더링하기 전에 요청 헤더의 ETag와 미리 계산한 ETag를 비교하고, 두 값이 일치하면 렌더링을 생략합니다.

다행히 Rails에는 이 모든 작업을 대신 처리해 주는 헬퍼가 내장되어 있습니다. fresh_when을 사용하면 ETag와 Last-Modified 날짜를 상품 객체 기준으로 명시적으로 지정할 수 있습니다.

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when(@product)
  end
end

respond_to 블록을 직접 작성하는 경우에는 fresh_when 대신 stale?를 사용하세요.

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    respond_to do |format|
      format.html { render :show } if stale?(@product)
      format.json { render json: @product if stale?(@product) }
    end
  end
end

이렇게 설정하면 상품 페이지를 처음 요청할 때 응답이 브라우저에 로컬로 캐시됩니다. 이후 동일한 페이지에 대한 요청에는 ETag가 포함되어 Rails에 캐시된 응답이 있다는 사실을 알려주고, Rails는 새로 계산한 ETag와 비교합니다. 두 값이 일치하면 Rails는 페이지 렌더링을 건너뛰고 즉시 304 Not Modified를 반환합니다.

클라이언트 측 캐싱으로 Rails 성능 개선하기: 조건부 GET 요청 완벽 가이드

참고: 페이지를 새로 고침(F5)하면 항상 캐시되지 않은 버전을 요청하게 됩니다. 조건부 GET이 제대로 작동하는지 테스트하려면 링크를 통해 다른 페이지로 이동했다가 돌아오거나, 브라우저의 뒤로 가기 버튼을 사용하세요.

이 글과 AppSignal Academy 시리즈의 다른 글들은 어떠셨나요? Rails 캐싱에 관한 글을 더 준비하고 있으니, 다음에 어떤 주제(캐싱 관련이든 아니든)를 다뤄줬으면 하는지 편하게 의견을 남겨주세요!

클라이언트 측 캐싱으로 Rails 성능 개선하기: 조건부 GET 요청 완벽 가이드

Jeff Kreeftmeijer