Async Datastore API

Async Datastore API를 사용하면 Datastore를 병렬 비차단 방식으로 호출한 후 요청을 처리하는 과정에서 이후 시점에 호출 결과를 검색할 수 있습니다. 이 문서에서는 Async Datastore API의 다음과 같은 측면에 대해 설명합니다.

비동기 데이터 저장소 서비스로 작업

Async Datastore API에서는 AsyncDatastoreService 인터페이스의 메소드를 사용하여 데이터 저장소를 호출합니다. DatastoreServiceFactory 클래스의 getAsyncDatastoreService() 클래스 메서드를 호출하여 이 객체를 가져올 수 있습니다.

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;

// ...
AsyncDatastoreService datastore = DatastoreServiceFactory.getAsyncDatastoreService();

AsyncDatastoreServiceDatastoreService와 같은 작업을 지원하지만 대부분의 메서드가 이후 시점에 결과가 차단될 수 있는 Future를 즉시 반환한다는 점이 다릅니다. 예를 들어 DatastoreService.get()Entity를 반환하지만 AsyncDatastoreService.get()Future<Entity>를 반환합니다.

// ...

Key key = KeyFactory.createKey("Employee", "Max");
// Async call returns immediately
Future<Entity> entityFuture = datastore.get(key);

// Do other stuff while the get operation runs in the background...

// Blocks if the get operation has not finished, otherwise returns instantly
Entity entity = entityFuture.get();

참고: get() 메서드를 호출할 때까지 예외가 발생하지 않습니다. 이 메서드를 호출하면 비동기 작업의 성공 여부를 확인할 수 있습니다.

AsyncDatastoreService를 사용하지만 작업을 동기식으로 실행해야 하는 경우 해당 AsyncDatastoreService 메서드를 호출한 후 즉시 결과를 차단합니다.

// ...

Entity entity = new Employee("Employee", "Alfred");
// ... populate entity properties

// Make a sync call via the async interface
Key key = datastore.put(key).get();

비동기 트랜잭션으로 작업

Async Datastore API 호출은 동기식 호출과 마찬가지로 트랜잭션에 참여할 수 있습니다. 다음은 단일 트랜잭션 내에서 Employee의 급여를 조정하고 Employee와 동일한 항목 그룹에 SalaryAdjustment 항목을 추가로 작성하는 함수입니다.

void giveRaise(AsyncDatastoreService datastore, Key employeeKey, long raiseAmount)
        throws Exception {
    Future<Transaction> txn = datastore.beginTransaction();

    // Async call to lookup the Employee entity
    Future<Entity> employeeEntityFuture = datastore.get(employeeKey);

    // Create and put a SalaryAdjustment entity in parallel with the lookup
    Entity adjustmentEntity = new Entity("SalaryAdjustment", employeeKey);
    adjustmentEntity.setProperty("adjustment", raiseAmount);
    adjustmentEntity.setProperty("adjustmentDate", new Date());
    datastore.put(adjustmentEntity);

    // Fetch the result of our lookup to make the salary adjustment
    Entity employeeEntity = employeeEntityFuture.get();
    long salary = (Long) employeeEntity.getProperty("salary");
    employeeEntity.setProperty("salary", salary + raiseAmount);

    // Re-put the Employee entity with the adjusted salary.
    datastore.put(employeeEntity);
    txn.get().commit(); // could also call txn.get().commitAsync() here
}

이 샘플에서는 트랜잭션 없는 비동기 호출과 트랜잭션 있는 비동기 호출 사이의 중요한 차이점을 보여줍니다. 트랜잭션을 사용하지 않는 경우 개별 비동기 호출이 완료되었는지 확인하는 유일한 방법은 호출 시점에 반환된 Future의 반환값을 가져오는 것입니다. 트랜잭션을 사용하는 경우 Transaction.commit()을 호출하면 커밋 전에 트랜잭션이 시작되었으므로 모든 비동기 호출의 결과를 차단합니다.

따라서 위의 예시에서는 commit()을 호출할 때 SalaryAdjustment 항목을 삽입하는 비동기 호출이 여전히 미해결 상태일 수 있어도 삽입이 완료될 때까지 커밋이 수행되지 않습니다. 마찬가지로 commit() 대신 commitAsync()를 호출하도록 선택하면 commitAsync()에서 반환된 Future에 대해 get()을 호출하면 모든 미해결 비동기 호출이 완료될 때까지 차단됩니다.

참고: 트랜잭션에는 DatastoreService 또는 AsyncDatastoreService의 특정 인스턴스가 아닌 특정 스레드가 연결됩니다. 즉, DatastoreService로 트랜잭션을 시작하고 AsyncDatastoreService로 비동기 호출을 수행하면 비동기 호출이 트랜잭션에 참여합니다. 또는 간단히 말해서 DatastoreService.getCurrentTransaction()AsyncDatastoreService.getCurrentTransaction()은 항상 동일한 Transaction을 반환합니다.

Future 작업

Future Javadoc에서는 Async Datastore API가 반환하는 Future를 적절히 활용하기 위해 알아야 할 대부분의 정보를 설명하지만 다음과 같이 App Engine에만 해당하는 몇 가지 사항도 숙지해야 합니다.

비동기 쿼리

명시적 비동기 API는 현재 쿼리용으로 노출되지 않습니다. 그러나 PreparedQuery.asIterable(), PreparedQuery.asIterator() 또는 PreparedQuery.asList(FetchOptions fetchOptions)를 호출하면 DatastoreService와 AsyncDatastoreService 모두 즉시 반환되며 비동기식으로 결과를 미리 가져옵니다. 따라서 쿼리 결과를 가져오는 동안 애플리케이션에서 병렬로 작업을 수행할 수 있습니다.

// ...

Query q1 = new Query("Salesperson");
q1.setFilter(new FilterPredicate("dateOfHire", FilterOperator.LESS_THAN, oneMonthAgo));

// Returns instantly, query is executing in the background.
Iterable<Entity> recentHires = datastore.prepare(q1).asIterable();

Query q2 = new Query("Customer");
q2.setFilter(new FilterPredicate("lastContact", FilterOperator.GREATER_THAN, oneYearAgo));

// Also returns instantly, query is executing in the background.
Iterable<Entity> needsFollowup = datastore.prepare(q2).asIterable();

schedulePhoneCall(recentHires, needsFollowUp);

비동기 데이터 저장소 호출을 사용해야 하는 경우

DatastoreService 인터페이스가 노출하는 작업은 동기식입니다. 예를 들어 DatastoreService.get()을 호출하면 Datastore 호출이 완료될 때까지 코드가 차단됩니다. 애플리케이션이 HTML에서 get()의 결과를 렌더링하기만 하면 되는 경우 호출이 완료될 때까지 차단할 수 있습니다. 그러나 애플리케이션이 응답을 렌더링하기 위해 get()Query 결과가 필요하고 get()Query에 데이터 종속 항목이 없으면 get()이 완료될 때까지 기다렸다가 Query를 시작하는 것은 시간 낭비입니다. 다음은 비동기 API를 사용하여 개선할 수 있는 코드의 예입니다.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Key empKey = KeyFactory.createKey("Employee", "Max");

// Read employee data from the Datastore
Entity employee = datastore.get(empKey); // Blocking for no good reason!

// Fetch payment history
Query query = new Query("PaymentHistory");
PreparedQuery pq = datastore.prepare(query);
List<Entity> result = pq.asList(FetchOptions.Builder.withLimit(10));
renderHtml(employee, result);

get()이 완료되기를 기다리는 대신 AsyncDatastoreService의 인스턴스를 사용하여 다음과 같이 비동기식으로 호출을 실행합니다.

AsyncDatastoreService datastore = DatastoreServiceFactory.getAsyncDatastoreService();
Key empKey = KeyFactory.createKey("Employee", "Max");

// Read employee data from the Datastore
Future<Entity> employeeFuture = datastore.get(empKey); // Returns immediately!

// Fetch payment history for the employee
Query query = new Query("PaymentHistory", empKey);
PreparedQuery pq = datastore.prepare(query);

// Run the query while the employee is being fetched
List<Entity> result = pq.asList(FetchOptions.Builder.withLimit(10));
// Implicitly performs query asynchronously
Entity employee = employeeFuture.get(); // Blocking!
renderHtml(employee, result); 

이 코드의 동기식 버전과 비동기식 버전은 같은 양의 작업을 수행하므로 CPU 사용량이 비슷합니다. 그러나 비동기 버전에서는 두 Datastore 작업이 병렬로 실행되므로 지연 시간이 더 짧습니다. 일반적으로, 데이터 종속 항목이 없는 여러 Datastore 작업을 수행해야 하는 경우 AsyncDatastoreService로 지연 시간을 크게 개선할 수 있습니다.