Memcache 사용

이 페이지에서는 Google Cloud Console을 사용하여 애플리케이션에 Memcache 서비스를 구성 및 모니터링하는 방법을 설명합니다. 또한 JCache 인터페이스를 사용하여 일반적인 작업을 수행하는 방법과 자바용 하위 수준 App Engine memcache API를 사용하여 동시 쓰기를 처리하는 방법도 설명합니다. Memcache에 대한 자세한 내용은 Memcache 개요를 참조하세요.

Memcache 구성

  1. Google Cloud Console에서 Memcache 페이지로 이동합니다.
    Memcache 페이지로 이동
  2. 사용하려는 Memcache 서비스 수준을 선택합니다.

    • 공유(기본값) - 가급적 최대한의 캐시 용량을 확보하여 제공합니다.
    • 전용 - 캐시 크기의 GB 시간 단위로 요금이 청구되며, 애플리케이션 전용으로 할당되는 고정 캐시 용량을 제공합니다.

    사용 가능한 서비스 등급에 대한 자세한 내용은 Memcache 개요를 참조하세요.

JCache 사용

App Engine 자바 SDK는 Memcache에 액세스하기 위한 JCache 인터페이스(JSR 107)를 지원합니다. 이 인터페이스는 javax.cache 패키지에 포함되어 있습니다.

JCache를 사용하면 값을 설정하거나 가져오고, 캐시에서 값이 만료되는 방식을 제어하고, 캐시의 내용을 검사하고, 캐시에 대한 통계를 확인할 수 있습니다. 값을 설정하거나 삭제할 때 '리스너'를 사용하여 커스텀 동작을 추가할 수도 있습니다.

App Engine 구현에서는 JCache API 표준의 일부를 충실히 구현하려고 합니다. JCache에 대한 자세한 내용은 JSR 107를 참조하세요. 하지만 JCache를 사용하는 대신 하위 수준 Memcache API를 사용하여 기본 서비스의 기능을 더 많이 이용할 수도 있습니다.

캐시 인스턴스 가져오기

캐시와 상호작용하려면 javax.cache.Cache 인터페이스 구현을 사용합니다. CacheManager의 정적 메서드에서 가져온 CacheFactory를 사용하여 Cache 인스턴스를 가져옵니다. 다음 코드에서는 기본 구성으로 Cache 인스턴스를 가져옵니다.

import java.util.Collections;
import javax.cache.Cache;
import javax.cache.CacheException;
import javax.cache.CacheFactory;
import javax.cache.CacheManager;

// ...
        Cache cache;
        try {
            CacheFactory cacheFactory = CacheManager.getInstance().getCacheFactory();
            cache = cacheFactory.createCache(Collections.emptyMap());
        } catch (CacheException e) {
            // ...
        }

CacheFactory의 createCache() 메서드에는 구성 속성의 맵이 사용됩니다. 이러한 속성은 아래에서 설명합니다. 기본값을 사용하려면 메소드에 빈 맵을 지정합니다.

값 저장 및 가져오기

Cache는 맵처럼 작동합니다. 즉, put() 메서드를 사용하여 키와 값을 저장하고 get() 메서드를 사용하여 값을 검색할 수 있습니다. 키 또는 값 중 하나에 Serializable 객체를 사용할 수 있습니다.

        String key;      // ...
        byte[] value;    // ...

        // Put the value into the cache.
        cache.put(key, value);

        // Get the value from the cache.
        value = (byte[]) cache.get(key);

여러 값을 저장하려면 맵을 인수로 사용하여 putAll() 메서드를 호출하면 됩니다.

캐시에서 값을 삭제(즉시 삭제)하려면 키를 인수로 사용하여 remove() 메서드를 호출합니다. 애플리케이션의 캐시에서 모든 값을 제거하려면 clear() 메서드를 호출합니다.

containsKey() 메서드는 키를 인수로 받고, 해당 키를 가진 값이 캐시에 있는지 여부를 나타내는 boolean(true 또는 false)을 반환합니다. isEmpty() 메서드는 캐시가 비어 있는지 여부를 테스트합니다. size() 메서드는 현재 캐시에 있는 값의 개수를 반환합니다.

만료 구성

기본적으로 모든 값은 메모리 부족으로 인해 제거되거나 앱에서 명시적으로 제거되거나 서비스 중단 등의 다른 이유로 사용할 수 없게 될 때까지 가능한 한 오래 캐시에 유지됩니다. 앱에서는 값의 만료 시간, 즉 값을 사용할 수 있는 최대 시간을 지정할 수 있습니다. 만료 시간은 값이 설정된 시점을 기준으로 한 상대적 시간이나 절대 날짜 및 시간으로 설정될 수 있습니다.

Cache 인스턴스를 만들 경우 구성 속성을 사용하여 만료 정책을 지정합니다. 해당 인스턴스와 함께 저장되는 모든 값은 동일한 만료 정책을 사용합니다. 예를 들어, Cache 인스턴스가 설정된 지 1시간(3,600초) 후에 만료되도록 구성하는 방법은 다음과 같습니다.

import java.util.HashMap;
import java.util.Map;
import javax.cache.Cache;
import javax.cache.CacheException;
import javax.cache.CacheFactory;
import javax.cache.CacheManager;
import javax.concurrent.TimeUnit;
import com.google.appengine.api.memcache.jsr107cache.GCacheFactory;

// ...
        Cache cache;
        try {
            CacheFactory cacheFactory = CacheManager.getInstance().getCacheFactory();
            Map<Object, Object> properties = new HashMap<>();
            properties.put(GCacheFactory.EXPIRATION_DELTA, TimeUnit.HOURS.toSeconds(1));
            cache = cacheFactory.createCache(properties);
        } catch (CacheException e) {
            // ...
        }

다음 속성은 값 만료를 제어합니다.

  • GCacheFactory.EXPIRATION_DELTA: 값이 저장된 후부터 정수로 지정된 시간(초)이 지나면 값을 만료 처리합니다.
  • GCacheFactory.EXPIRATION_DELTA_MILLIS: 값이 저장된 후부터 정수로 지정된 시간(밀리초)이 지나면 값을 만료 처리합니다.
  • GCacheFactory.EXPIRATION: java.util.Date로 지정된 날짜 및 시간에 값을 만료 처리합니다.

설정 정책 구성

기본적으로는 캐시에 값을 설정할 경우 지정된 키를 가진 값이 없으면 새 값이 추가되고 지정된 키를 가진 값이 있으면 기존 값이 대체됩니다. 캐시에 값을 추가하기만 하거나(기존 값 보호) 값을 대체하기만 하도록(추가 안 함) 구성할 수도 있습니다.

import java.util.HashMap;
import java.util.Map;
import com.google.appengine.api.memcache.MemcacheService;

// ...
        Map<Object, Object> properties = new HashMap<>();
        properties.put(MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT, true);

다음 속성은 설정 정책을 제어합니다.

  • MemcacheService.SetPolicy.SET_ALWAYS: 지정된 키를 가진 값이 없으면 새 값을 추가하고, 지정된 키를 가진 값이 있으면 기존 값을 대체합니다. 이것이 기본값입니다.
  • MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT: 지정된 키를 가진 값이 없으면 새 값을 추가하고, 지정된 키를 가진 값이 있으면 아무것도 수행하지 않습니다.
  • MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT: 지정된 키를 가진 값이 없으면 아무것도 수행하지 않고, 지정된 키를 가진 값이 있으면 기존 값을 대체합니다.

캐시 통계 검색

앱에서는 해당 앱의 캐시 사용에 대한 통계를 검색할 수 있습니다. 이러한 통계는 캐시 동작을 모니터링하고 조정하는 데 유용합니다. 통계에 액세스하려면 CacheStatistics 객체를 사용하며 이 객체는 Cache의 getCacheStatistics() 메서드를 호출하여 가져옵니다.

사용 가능한 통계로는 캐시 적중 횟수(존재하는 키에 대한 가져오기), 캐시 누락 횟수(존재하지 않는 키에 대한 가져오기), 캐시 내의 값 수가 포함됩니다.

import javax.cache.CacheStatistics;

        CacheStatistics stats = cache.getCacheStatistics();
        int hits = stats.getCacheHits();
        int misses = stats.getCacheMisses();

App Engine 구현에서는 hitmiss 횟수의 재설정이 지원되지 않습니다. 적중 및 누락 횟수가 무한정 유지되지만 memcache 서버의 일시적 상태로 인해 재설정되는 경우도 있습니다.

Google Cloud Console에서 Memcache 모니터링

  1. Google Cloud Console에서 Memcache 페이지로 이동합니다.
    Memcache 페이지로 이동
  2. 다음 보고서를 확인합니다.
    • Memcache 서비스 수준: 애플리케이션이 공유 서비스 수준을 사용하고 있는지 아니면 전용 서비스 수준을 사용하고 있는지를 표시합니다. 프로젝트 소유자는 두 수준 간에 전환할 수 있습니다. 자세한 내용은 서비스 수준을 참조하세요.
    • 적중률: 캐시에서 제공된 데이터 요청의 비율과 더불어 캐시에서 제공된 데이터 요청의 총 개수를 보여줍니다.
    • 캐시의 항목 수
    • 가장 오래된 항목의 유지 기간: 가장 오래된 캐시된 항목의 유지 기간입니다. 항목의 유지 기간은 항목이 사용(읽기 또는 쓰기)될 때마다 재설정됩니다.
    • 총 캐시 크기
  3. 다음과 같은 작업을 수행할 수 있습니다.

    • 새 키: 캐시에 새 키를 추가합니다.
    • 키 찾기: 기존 키를 검색합니다.
    • 캐시 삭제: 캐시에서 모든 키-값 쌍을 삭제합니다.
  4. (전용 Memcache에만 해당) 핫 키 목록을 살펴봅니다.

    • '핫 키'는 Memcache에서 수신 QPS(초당 쿼리 수)가 100을 초과하는 키입니다.
    • 이 목록에는 최대 100개의 핫 키가 QPS가 많은 순서대로 포함됩니다.

동시 쓰기 처리

다른 동시 쓰기 요청을 수신할 수 있는 Memcache 키의 값을 업데이트하는 경우에는 putget 대신 하위 수준 Memcache 메서드인 putIfUntouchedgetIdentifiable을 사용해야 합니다. putIfUntouchedgetIdentifiable 메서드는 동시에 처리되는 여러 요청이 동일한 Memcache 키 값을 원자적으로 업데이트할 수 있도록 하여 경합 상태를 방지합니다.

아래 코드 스니펫에서는 다른 클라이언트로부터의 동시 업데이트 요청이 있을 수 있는 키 값을 안전하게 업데이트하는 한 가지 방법을 보여줍니다.

@SuppressWarnings("serial")
public class MemcacheConcurrentServlet extends HttpServlet {

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException,
      ServletException {
    String path = req.getRequestURI();
    if (path.startsWith("/favicon.ico")) {
      return; // ignore the request for favicon.ico
    }

    String key = "count-concurrent";
    // Using the synchronous cache.
    MemcacheService syncCache = MemcacheServiceFactory.getMemcacheService();

    // Write this value to cache using getIdentifiable and putIfUntouched.
    for (long delayMs = 1; delayMs < 1000; delayMs *= 2) {
      IdentifiableValue oldValue = syncCache.getIdentifiable(key);
      byte[] newValue = oldValue == null
          ? BigInteger.valueOf(0).toByteArray()
              : increment((byte[]) oldValue.getValue()); // newValue depends on old value
      resp.setContentType("text/plain");
      resp.getWriter().print("Value is " + new BigInteger(newValue).intValue() + "\n");
      if (oldValue == null) {
        // Key doesn't exist. We can safely put it in cache.
        syncCache.put(key, newValue);
        break;
      } else if (syncCache.putIfUntouched(key, oldValue, newValue)) {
        // newValue has been successfully put into cache.
        break;
      } else {
        // Some other client changed the value since oldValue was retrieved.
        // Wait a while before trying again, waiting longer on successive loops.
        try {
          Thread.sleep(delayMs);
        } catch (InterruptedException e) {
          throw new ServletException("Error when sleeping", e);
        }
      }
    }
  }

  /**
   * Increments an integer stored as a byte array by one.
   * @param oldValue a byte array with the old value
   * @return         a byte array as the old value increased by one
   */
  private byte[] increment(byte[] oldValue) {
    long val = new BigInteger(oldValue).intValue();
    val++;
    return BigInteger.valueOf(val).toByteArray();
  }
}

재시도 횟수에 대한 제한을 설정하여 너무 오랜 차단으로 인해 App Engine 요청이 타임아웃되지 않도록 하기 위해 이 샘플 코드를 조정할 수도 있습니다.

다음 단계