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

Rails의 클라이언트 측 캐싱: 조건부 GET 요청으로 성능 최적화하기

Rails 애플리케이션의 성능을 높이는 방법은 러시아 인형 캐싱(Russian doll caching)만 있는 것이 아닙니다. 이번 글에서는 Rails에 내장된 조건부 GET(Conditional GET) 지원 기능을 살펴보겠습니다. 이 기능을 활용하면 렌더링된 페이지를 사용자의 브라우저 캐시에 저장해 서버 부담과 응답 시간을 크게 줄일 수 있습니다.

👋 캐싱 외적인 성능 최적화 주제에 관심이 있다면, Ruby(on Rails) 성능에 관한 다른 글들도 확인해 보세요. 저희가 정리한 Ruby 성능 모니터링 체크리스트도 큰 도움이 될 것입니다.

ETag와 Last-Modified 헤더

브라우저가 Rails 앱의 특정 페이지에 대해 HTTP GET 요청을 실행하면, 라우터는 해당 요청을 컨트롤러 액션 중 하나와 연결합니다. 그러면 컨트롤러가 데이터베이스에서 필요한 데이터를 조회하고 뷰를 렌더링합니다. 이후 렌더링된 HTML이 담긴 HTTP 응답(200 OK 상태 코드)이 브라우저로 전송되고, 브라우저는 이를 파싱해 화면에 표시합니다.

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

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 %>가 문제가 됩니다. CSRF 토큰 메타 태그는 요청마다 값이 바뀌기 때문에 응답 본문도 매번 달라지고, 결국 ETag가 무효화되어 로컬 캐시가 stale 상태로 표시됩니다.

게다가 Rails는 기본적으로 304 Not Modified를 절대 반환하지 않습니다. 컨트롤러에서 로컬 캐시를 명시적으로 fresh 상태로 표시해 주지 않기 때문입니다.

fresh_whenstale?

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

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

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

# app/controllers/products_controller.rb
def show
  @product = Product.find(params[:id])
  fresh_when @product
end

명시적인 respond_to 블록이 있다면 fresh_when 대신 stale?을 사용하세요.

# app/controllers/products_controller.rb
def show
  @product = Product.find(params[:id])

  if stale?(@product)
    respond_to do |format|
      format.html
    end
  end
end

이제 제품 페이지를 요청하면 응답이 로컬에 캐시됩니다. 이후 같은 페이지에 대한 요청에는 ETag가 함께 전송되어 "캐시된 응답이 있다"고 Rails에 알리고, Rails는 이를 새로 계산한 ETag와 비교합니다. 두 값이 일치하면 Rails는 페이지 렌더링을 건너뛰고 즉시 304 Not Modified를 반환합니다.

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

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