참고: 다른 앱이 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) { Futureresult = 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개의 코드 참조). 이 경우 컴파일러 오류가 발생합니다.