Datastore 콜백

참고: 다른 앱이 Remote API를 사용하여 App Engine을 호출할 경우 콜백이 트리거되지 않습니다

콜백을 사용하면 지속성 프로세스의 여러 지점에서 코드를 실행할 수 있습니다. 이러한 콜백을 사용하여 특히 로깅, 샘플링, 장식, 감사, 유효성 검사와 같이 여러 함수를 사용하는 논리를 쉽게 구현할 수 있습니다. Datastore API는 현재 put()delete() 작업 전후에 실행되는 콜백을 지원합니다.

PrePut

특정 종류의 PrePut 콜백을 등록하면 해당 종류의 항목이 배치되기 전에(또는 종류가 지정되지 않은 경우 어떤 항목이든 배치되기 전에) 콜백이 호출됩니다.

import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PutContext;
import java.util.Date;
import java.util.logging.Logger;

class PrePutCallbacks {
    static Logger logger = Logger.getLogger("callbacks");

    @PrePut(kinds = {"Customer", "Order"})
    void log(PutContext context) {
        logger.fine("Putting " + context.getCurrentElement().getKey());
    }

    @PrePut // Applies to all kinds
    void updateTimestamp(PutContext context) {
        context.getCurrentElement().setProperty("last_updated", new Date());
    }
}

PrePut이라는 주석이 있는 메서드는 void를 반환하고, PutContext를 유일한 인수로 허용하고, 확인된 예외를 발생시키지 않고, 인수 없는 생성자가 있는 클래스에 속하는 인스턴스 메서드여야 합니다. PrePut 콜백이 예외를 발생시키면 추가 콜백이 더 이상 실행되지 않고 RPC가 Datastore에 실행되기 전에 put() 작업이 예외를 발생시킵니다.

PostPut

특정 종류의 PostPut 콜백을 등록하면 해당 종류의 항목이 배치된 후(종류를 지정하지 않은 경우 어느 항목이든 배치된 후)에 콜백이 호출됩니다.

import com.google.appengine.api.datastore.PostPut;
import com.google.appengine.api.datastore.PutContext;
import java.util.logging.Logger;

class PostPutCallbacks {
    static Logger logger = Logger.getLogger("logger");

    @PostPut(kinds = {"Customer", "Order"}) // Only applies to Customers and Orders
    void log(PutContext context) {
        logger.fine("Finished putting " + context.getCurrentElement().getKey());
    }

    @PostPut // Applies to all kinds
    void collectSample(PutContext context) {
        Sampler.getSampler().collectSample(
            "put", context.getCurrentElement().getKey());
    }
}

PostPut이라는 주석이 있는 메서드는 void를 반환하고, PutContext를 유일한 인수로 허용하고, 확인된 예외를 발생시키지 않고, 인수 없는 생성자가 있는 클래스에 속하는 인스턴스 메서드여야 합니다. PostPut 콜백이 예외를 발생시키면 추가 콜백이 더 이상 실행되지 않지만 put() 작업의 결과는 아무런 영향을 받지 않습니다. put() 작업 자체가 실패하면 PostPut 콜백이 전혀 호출되지 않는다는 점에 유의하세요. 또한 트랜잭션 put() 작업과 연결된 PostPut 콜백은 해당 트랜잭션이 성공적으로 커밋될 때까지 실행되지 않습니다.

PreDelete

특정 종류의 PreDelete 콜백을 등록하면 해당 종류의 항목이 삭제되기 전(종류를 지정하지 않은 경우 어느 항목이든 삭제되기 전)에 콜백이 호출됩니다.

import com.google.appengine.api.datastore.DeleteContext;
import com.google.appengine.api.datastore.PreDelete;
import java.util.logging.Logger;

class PreDeleteCallbacks {
    static Logger logger = Logger.getLogger("logger");

    @PreDelete(kinds = {"Customer", "Order"})
    void checkAccess(DeleteContext context) {
        if (!Auth.canDelete(context.getCurrentElement()) {
            throw new SecurityException();
        }
    }

    @PreDelete // Applies to all kinds
    void log(DeleteContext context) {
        logger.fine("Deleting " + context.getCurrentElement().getKey());
    }
}

PreDelete이라는 주석이 있는 메서드는 void를 반환하고, DeleteContext를 유일한 인수로 허용하고, 확인된 예외를 발생시키지 않고, 인수 없는 생성자가 있는 클래스에 속하는 인스턴스 메서드여야 합니다. PreDelete 콜백이 예외를 발생시키면 추가 콜백이 더 이상 실행되지 않고 RPC가 Datastore에 실행되기 전에 delete() 작업이 예외를 발생시킵니다.

PostDelete

특정 종류의 PostDelete 콜백을 등록하면 해당 종류의 항목이 삭제된 후(종류를 지정하지 않은 경우 어느 항목이든 삭제된 후) 콜백이 호출됩니다.

import com.google.appengine.api.datastore.DeleteContext;
import com.google.appengine.api.datastore.PostDelete;
import java.util.logging.Logger;

class PostDeleteCallbacks {
    static Logger logger = Logger.getLogger("logger");

    @PostDelete(kinds = {"Customer", "Order"})
    void log(DeleteContext context) {
        logger.fine(
            "Finished deleting " + context.getCurrentElement().getKey());
    }

    @PostDelete // Applies to all kinds
    void collectSample(DeleteContext context) {
        Sampler.getSampler().collectSample(
            "delete", context.getCurrentElement().getKey());
    }
}

PostDelete이라는 주석이 있는 메서드는 void를 반환하고, DeleteContext를 유일한 인수로 허용하고, 확인된 예외를 발생시키지 않고, 인수 없는 생성자가 있는 클래스에 속하는 인스턴스 메서드여야 합니다. PostDelete 콜백이 예외를 발생시키면 추가 콜백이 더 이상 실행되지 않지만 delete() 작업의 결과는 아무런 영향을 받지 않습니다. delete() 작업 자체가 실패하면 PostDelete 콜백이 전혀 호출되지 않는다는 점에 유의하세요. 또한 트랜잭션 delete() 작업과 연결된 PostDelete 콜백은 해당 트랜잭션이 성공적으로 커밋될 때까지 실행되지 않습니다.

PreGet

PreGet 콜백을 등록하여 특정 종류(또는 모든 종류)의 항목을 가져오기 전에 호출되도록 만들 수 있습니다. 예를 들어 일부 get 요청을 가로채서 Datastore가 아니라 캐시에서 데이터를 가져올 때 이 콜백을 사용할 수 있습니다.

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PreGetContext;
import com.google.appengine.api.datastore.PreGet;

class PreGetCallbacks {
  @PreGet(kinds = {"Customer", "Order"})
  public void preGet(PreGetContext context) {
    Entity e = MyCache.get(context.getCurrentElement());
    if (e != null) {
      context.setResultForCurrentElement(e);
    }
    // if not in cache, don't set result; let the normal datastore-fetch happen
  }
}

PreQuery

PreQuery 콜백을 등록하여 특정 종류(또는 모든 종류)에 쿼리를 실행하기 전에 호출되도록 만들 수 있습니다. 예를 들어 로그인한 사용자에 따라 일치 필터를 추가할 때 이 콜백을 사용할 수 있습니다. 이 콜백은 쿼리로 전달됩니다. 콜백을 수정하여 다른 쿼리가 대신 실행되도록 할 수 있습니다. 또는 콜백에서 확인되지 않은 예외를 생성하여 쿼리가 실행되지 못할 수 있습니다.

import com.google.appengine.api.datastore.PreQueryContext;
import com.google.appengine.api.datastore.PreQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

class PreQueryCallbacks {
  @PreQuery(kinds = {"Customer"})
  // Queries should only return data owned by the logged in user.
  public void preQuery(PreQueryContext context) {
    UserService users = UserServiceFactory.getUserService();
    context.getCurrentElement().setFilter(
        new FilterPredicate("owner", Query.FilterOperator.EQUAL, users.getCurrentUser()));
    }
}

PostLoad

PostLoad 콜백을 등록하여 특정 종류(또는 모든 종류)의 항목을 로드한 후에 호출되도록 만들 수 있습니다. 여기서, '로드'는 get 또는 쿼리의 결과를 의미합니다. 이 콜백은 로드된 항목의 데코레이션에 유용합니다.

import com.google.appengine.api.datastore.PostLoadContext;
import com.google.appengine.api.datastore.PostLoad;

class PostLoadCallbacks {
   @PostLoad(kinds = {"Order"})
   public void postLoad(PostLoadContext context) {
     context.getCurrentElement().setProperty("read_timestamp",
                                             System.currentTimeMillis());
   }
}

일괄 작업

일괄 작업을 실행하면(예: 여러 항목에 put() 적용) 콜백이 항목당 1회 호출됩니다. 콜백 메서드의 인수에서 CallbackContext.getElements()를 호출하여 전체 객체에 일괄적으로 액세스할 수 있습니다. 이렇게 하면 한 번에 하나의 요소가 아니라 전체 요소에 실행되는 콜백을 구현할 수 있습니다.

import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PutContext;

class Validation {
    @PrePut(kinds = "TicketOrder")
    void checkBatchSize(PutContext context) {
        if (context.getElements().size() > 5) {
            throw new IllegalArgumentException(
                "Cannot purchase more than 5 tickets at once.");
        }
    }
}

콜백이 배치당 한 번만 실행되도록 하려면 CallbackContext.getCurrentIndex()를 사용하여 배치의 첫 번째 요소를 대상으로 정할지 결정합니다.

import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PutContext;
import java.util.logging.Logger;

class Validation {
    static Logger logger = Logger.getLogger("logger");

    @PrePut
    void log(PutContext context) {
        if (context.getCurrentIndex() == 0) {
            logger.fine("Putting batch of size " + getElements().size());
        }
    }
}

비동기 작업

콜백이 비동기 Datastore 작업과 상호작용하는 방법과 관련하여 알아야 할 몇 가지 중요 사항이 있습니다. Async Datastore API를 사용하여 put() 또는 delete()를 실행하면 등록한 Pre* 콜백이 동기화되어 실행됩니다. Post* 콜백은 Future.get() 메서드 중 하나를 호출하여 작업의 결과를 검색해야만 동기화되어 실행됩니다.

App Engine 서비스에 콜백 사용

콜백은 애플리케이션의 다른 모든 코드와 마찬가지로 광범위한 App Engine 백엔드 서비스에 액세스할 수 있으며 단일 클래스 내에서 사용자가 원하는 만큼 정의할 수 있습니다.

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.DeleteContext;
import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PostPut;
import com.google.appengine.api.datastore.PostDelete;
import com.google.appengine.api.datastore.PutContext;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;

class ManyCallbacks {

    @PrePut(kinds = {"kind1", "kind2"})
    void foo(PutContext context) {
      MemcacheService ms = MemcacheServiceFactory.getMemcacheService();
      // ...
    }

    @PrePut
    void bar(PutContext context) {
      DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
      // ...
    }

    @PostPut(kinds = {"kind1", "kind2"})
    void baz(PutContext context) {
        Queue q = QueueFactory.getDefaultQueue();
        // ...
    }

    @PostDelete(kinds = {"kind2"})
    void yam(DeleteContext context) {
        URLFetchService us = URLFetchServiceFactory.getURLFetchService();
        // ...
    }
}

피해야 할 일반적인 오류

콜백을 구현할 때 알아야 할 일반적인 오류가 몇 가지 있습니다.

비정적 상태를 유지하지 않음

import java.util.logging.Logger;
import com.google.appengine.api.datastore.PrePut;

class MaintainsNonStaticState {
    static Logger logger = Logger.getLogger("logger");
    // ERROR!
    // should be static to avoid assumptions about lifecycle of the instance
    boolean alreadyLogged = false;

    @PrePut
    void log(PutContext context) {
        if (!alreadyLogged) {
            alreadyLogged = true;
            logger.fine(
                "Finished deleting " + context.getCurrentElement().getKey());
        }
    }
}

콜백 실행 순서에 가정을 하지 않음

Pre* 콜백은 항상 Post* 콜백 전에 실행되지만, 다른 Pre* 콜백을 기준으로 Pre* 콜백이 실행되는 순서에 대해 가정하거나 다른 Post* 콜백을 기준으로 Post* 콜백이 실행되는 순서를 가정하는 것은 위험합니다.

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PrePut;

import java.util.HashSet;
import java.util.Set;

class MakesAssumptionsAboutOrderOfCallbackExecution {
    static Set<Key> paymentKeys = new HashSet<Key>();

    @PrePut(kinds = "Order")
    void prePutOrder(PutContext context) {
        Entity order = context.getCurrentElement();
        paymentKeys.addAll((Collection<Key>) order.getProperty("payment_keys"));
    }

    @PrePut(kinds = "Payment")
    void prePutPayment(PutContext context) {
        // ERROR!
        // Not safe to assume prePutOrder() has already run!
        if (!paymentKeys.contains(context.getCurrentElement().getKey()) {
            throw new IllegalArgumentException("Unattached payment!");
        }
    }
}

메서드당 콜백 1개

한 클래스에 콜백 메소드를 무한대로 지정할 수 있지만 한 메소드는 오직 하나의 콜백과 연결될 수 있습니다.

import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PostPut;

class MultipleCallbacksOnAMethod {
    @PrePut
    @PostPut // Compiler error!
    void foo(PutContext context) { }
}

비동기 결과를 검색해야 함

Post* 콜백은 사용자가 Future.get()을 호출하여 작업의 결과를 검색해야만 실행됩니다. Future.get()을 호출하지 않고 HTTP 요청 처리를 완료하면 Post* 콜백이 실행되지 않습니다.

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PostPut;
import com.google.appengine.api.datastore.PutContext;

import java.util.concurrent.Future;

class IgnoresAsyncResult {
    AsyncDatastoreService ds = DatastoreServiceFactory.getAsyncDatastoreService();

    @PostPut
    void collectSample(PutContext context) {
        Sampler.getSampler().collectSample(
            "put", context.getCurrentElement().getKey());
    }

    void addOrder(Entity order) {
        Future result = ds.put(order);
        // ERROR! Never calls result.get() so collectSample() will not run.
    }
}

무한 루프 방지

콜백에서 Datastore 작업을 수행할 때는 무한 루프에 빠지지 않도록 주의하세요.

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PutContext;

class InfiniteLoop {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

    @PrePut
    void audit(PutContext context) {
        Entity original = ds.get(context.getCurrentElement().getKey());
        Entity auditEntity = new Entity(original.getKind() + "_audit");
        auditEntity.setPropertiesFrom(original);
        // INFINITE LOOP!
        // Restricting the callback to specific kinds would solve this.
        ds.put(auditEntity);
    }
}

Eclipse에서 콜백 사용

Eclipse로 앱을 개발한다면 Datastore 콜백을 사용하기 위한 몇 가지 간단한 구성 단계를 거쳐야 합니다. 아래의 단계는 Eclipse 3.7을 위한 구성 단계입니다. Eclipse용 Google 플러그인의 향후 출시 버전에서는 이러한 단계를 요구하지 않을 계획입니다.

  • 프로젝트에서 속성 대화상자를 엽니다(프로젝트 > 속성).
  • 주석 처리 대화상자를 엽니다(Java 컴파일러 > 주석 처리).
  • '주석 처리 사용 설정'에 체크합니다.
  • '편집기에서 처리 사용 설정'에 체크합니다.
  • 초기화 경로 대화상자를 엽니다(Java 컴파일러 > 주석 처리 > 초기화 경로).
  • '외부 JAR 추가'를 클릭합니다.
  • <SDK_ROOT>/lib/impl/appengine-api.jar를 선택합니다(여기서 SDK_ROOT는 SDK를 설치한 최상위 디렉토리).
  • '확인'을 클릭합니다.

여러 콜백에 메소드를 구현하여 콜백이 제대로 구성되었는지 확인할 수 있습니다(메소드당 콜백 1개의 코드 참조). 이 경우 컴파일러 오류가 발생합니다.