간단한 캐싱 라켓 매크로

맞춤형 웹 사이트를 구축하기 위해 작업 중인 프로젝트에는 많은 정보를 수신하기 위해 타사 API와 작업하는 작업이 포함됩니다. API는 사용자를 계정에 연결된 제품과 연결하고 API는 별도의 API 호출에서 각 제품에 대한 자세한 정보를 제공할 수 있습니다.

이 프로젝트를 완전히 구축하려면 모든 사용자, 그들이 소유한 제품에 대한 정보가 필요하고 유익한 웹 사이트를 생성하기 위해 각 제품에 대한 추가 정보가 필요합니다. N 사용자와 M 제품이 있으며 사용자와 제품 사이에는 많은 연결이 있습니다.

이 프로젝트의 부트스트랩을 시작하기 위해 사용자를 반복한 다음 사용자가 소유한 모든 제품을 반복합니다.

(for ([user all-users])
  (define users-products ($fetch-products (user-id user)))
  (for ([pid users-products])
    (define product-info ($fetch-product pid))
    (do-something-with product-info)))


500명의 사용자가 있고 각 사용자가 1000개의 제품을 가지고 있다면 약 500,000번의 네트워크 호출을 할 것이기 때문에 이것은 좋지 않습니다. 나는 API 제공자가 그것에 대해 별로 신경쓰지 않을 것이라고 생각한다. 대신 사용자 정보는 정기적으로 변경될 수 있지만 제품 정보는 그렇지 않은 경우가 많다는 점에 유의해야 합니다.

대신 파일 시스템에 플랫 파일로 제품 정보를 저장해 보겠습니다. 이렇게 하면 웹 사이트 게시의 새 빌드를 수행할 때 제품이 이미 따로 설정되어 있고 자주 변경될 수 있으므로 API에서 사용자 라이브 데이터를 계속 가져올 수 있습니다.

(for ([user all-users])
  (define users-products ($fetch-products (user-id user)))
  (for ([pid users-products])
    (define cached (build-path "cache" (format "~a.txt" pid)))
    (define product-info
      (if (file-exists? cached)
          (file->string cached)
          (let ([resp ($fetch-product pid)])
            (call-with-output-file cached #:exists 'replace
              (lambda (out)
                (write resp out)))
             resp)))
    (do-something-with product-info)))


이것은 일종의 한 입입니다. 캐싱이 이제 우리의 기본 프로그램 논리와 연결되어 있기 때문에 이 프로그램은 그다지 모듈화되어 있지 않습니다. 기본 프로그램을 변경하는 것은 캐싱 코드 안팎으로 피하는 것을 의미하며 우리의 진정한 의도를 잘 반영하지 않습니다. 모듈성과 가독성을 위해 캐싱 로직을 별도의 함수로 옮겨야 합니다.

(define (Cachify path-check code-to-run)
  (if (file-exists? path-check)
      (file->string path-check)
      (let ([resp code-to-run])
        (call-with-output-file cached
          #:exists 'replace
          (lambda (out)
            (write resp out)))
         resp)))


이것은 나에게 맞는 것 같습니다. 파일 경로를 확인하고 파일이 없으면 코드 페이로드를 실행합니다. 지금까지는 맞는 것 같습니다.

그러나 여기에 우리의 다음 문제가 있습니다. Racket이 코드를 평가하는 방법 때문에 이것을 사용하려고 하면 파일에 저장되었는지 여부에 관계없이 매번 네트워크 요청을 실행하게 됩니다.

> (define product-info (Cachify "product.txt" ($fetch-product 5))
; ... still runs the network request ...
> "{\"pid\":\"5\"}"


이것은 분명히 우리에게 효과가 없습니다. 이를 언어 수준의 매크로로 전환해야 합니다. 여기서 Racket은 일반적인 Racket 함수인 것처럼 매크로를 평가하지만 평가할 하위 표현식 대신 인수를 리터럴 값으로 취급합니다. 다행히도 크게 다르지 않아 보입니다.

(define-syntax-rule (Cachify path-check code-to-run)
  (if (file-exists? path-check)
      (file->string path-check)
      (let ([resp code-to-run])
        (call-with-output-file path-check
          #:exists 'replace
          (lambda (out)
            (write resp out)))
         resp)))


이 매크로는 수신한 코드에서 리터럴 변환을 수행하고 변수를 적절하게 대체하여 작동합니다. 이 매크로를 실행하려고 하면 네트워크 요청이 파일 시스템에 먼저 있지 않은 경우에만 네트워크 요청을 실행하는 방식으로 코드를 대체하고 확인할 기회가 생기기 전이 아님을 알 수 있습니다. 더 나은 그림을 얻기 위해 expandsyntax->datum를 사용하여 매크로 변환을 볼 수 있습니다.

> (syntax->datum (expand '(Cachify "1.txt" (println "Hello"))))
'(if (#%app file-exists? '"1.txt")
   (let-values (((temp16) '"1.txt"))
     (if (#%app
          variable-reference-constant?
          (#%variable-reference file->string101))
       (#%app file->string 'binary temp16)
       (#%app file->string101 temp16)))
   (let-values (((resp) (#%app println '"Hello")))
     (let-values (((string:5:8) call-with-output-file36)
                  ((temp17) '"1.txt")
                  ((temp18) 'replace)
                  ((temp19) (lambda (out) (#%app write resp out))))
; ...


약간 왜곡되어 보이지만 이것이 Racket VM 내부에서 보이는 방식입니다. 먼저 (if (#%app file-exists? '"1.txt") 가 포함된 파일이 있는지 확인하고 실패하면 아래로 이동하여 (let-values (((resp) (#%app println '"Hello"))) 줄에 지정한 표현식을 실행합니다.

이것은 네트워크 요청에 대해 작동할 뿐만 아니라 솔직히 수행하는 모든 계산에서 이 매크로와 함께 사용할 수 있습니다. 좋은 점은 데이터베이스 또는 redis와 같은 것을 메모리 저장소로 사용하려는 경우와 같이 더 나은 최신 기술을 지원하도록 매크로 자체를 조정할 수 있다는 것입니다. 프로그램 전체에서 캐싱 구현을 변경하지 않고도 매크로를 수정하고 계속 사용할 수 있습니다. 이제 웹사이트를 구축해 보겠습니다.

(for ([user all-users])
  (define users-products ($fetch-products (user-id user)))
  (for ([pid users-products])
    (define product-info
      (Cachify (build-path "cache" (format "~a.txt" pid))
               ($fetch-product pid)))
    (do-something-with product-info)))


모든 증분 빌드에서 제품 정보가 변경되지 않는 한 API 호스트 수천 건의 호출을 저장하게 됩니다. 그렇다면 오래된 파일 등을 정리하거나 캐시가 특정 날짜 또는 간격에만 유효성을 검사하는 업데이트 주기를 생성하여 캐시를 무효화해야 할 수 있습니다. 그러나 그것은 물론 당신에게 달려 있습니다.

읽어 주셔서 감사합니다!

좋은 웹페이지 즐겨찾기