Cloud Spanner: 자바로 게임 리더보드 만들기

Google Cloud Spanner는 성능 및 고가용성을 희생하지 않고도 ACID 트랜잭션 및 SQL 시맨틱스를 제공하는 수평 확장이 가능하고, 전역으로 분산되었고, 완전히 관리되는 관계형 데이터베이스 서비스입니다.

이 실습에서는 Cloud Spanner 인스턴스를 설정하는 방법을 알아봅니다. 게임 리더보드에 사용할 수 있는 데이터베이스 및 스키마를 만드는 단계를 진행합니다. 먼저 플레이어 정보를 저장하기 위한 플레이어 테이블을 만들고 플레이어 점수를 저장하기 위한 점수 테이블을 만듭니다.

그런 후 테이블에 샘플 데이터를 채웁니다. 그러고 나서 몇 가지 상위 10개 샘플 쿼리를 실행하고 마지막으로 리소스를 비우기 위해 인스턴스를 삭제함으로써 이 실습을 마칩니다.

학습 내용

  • Cloud Spanner 인스턴스 설정 방법
  • 데이터베이스 및 테이블을 만드는 방법
  • 커밋 타임스탬프 열 사용 방법
  • 타임스탬프가 있는 Cloud Spanner 데이터베이스 테이블에 데이터를 로드하는 방법
  • Cloud Spanner 데이터베이스를 쿼리하는 방법
  • Cloud Spanner 인스턴스를 삭제하는 방법

준비물

본 가이드를 어떻게 사용하실 계획인가요?

읽기만 할 계획입니다. 읽은 다음 연습 활동을 완료할 계획입니다.

귀하의 Google Cloud Platform 사용 경험을 평가해 주세요.

초급 중급 고급

자습형 환경 설정

아직 Google 계정(Gmail 또는 Google Apps)이 없으면 계정을 만들어야 합니다. Google Cloud Platform Console(console.cloud.google.com)에 로그인하고 새 프로젝트를 만듭니다.

프로젝트가 이미 있으면 Console 왼쪽 위에서 프로젝트 선택 풀다운 메뉴를 클릭합니다.

6c9406d9b014760.png

그리고 표시된 대화상자에서 '새 프로젝트' 버튼을 클릭하여 새 프로젝트를 만듭니다.

f708315ae07353d0.png

아직 프로젝트가 없으면 첫 번째 프로젝트를 만들기 위해 다음과 비슷한 대화상자가 표시됩니다.

870a3cbd6541ee86.png

이후의 프로젝트 만들기 대화상자에서 새 프로젝트의 세부정보를 입력할 수 있습니다.

6a92c57d3250a4b3.png

모든 Google Cloud 프로젝트에서 고유한 이름인 프로젝트 ID를 기억하세요(위의 이름은 이미 사용되었으므로 사용할 수 없습니다). 이 ID는 나중에 이 Codelab에서 PROJECT_ID라고 부릅니다.

그런 다음 Google Cloud 리소스를 사용하고 Cloud Spanner API를 사용 설정하기 위해서는 아직 완료하지 않은 경우 Developers Console에서 결제를 사용 설정해야 합니다.

15d0ef27a8fbab27.png

이 codelab을 실행하는 과정에는 많은 비용이 들지 않지만 더 많은 리소스를 사용하려고 하거나 실행 중일 경우 비용이 더 들 수 있습니다(이 문서 마지막의 '삭제' 섹션 참조). Google Cloud Spanner 가격 책정은 여기를 참조하세요.

Google Cloud Platform 신규 사용자는 $300 상당의 무료 체험판을 사용할 수 있으므로, 이 Codelab을 완전히 무료로 사용할 수 있습니다.

Google Cloud Shell 설정

Google Cloud 및 Spanner를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 Cloud에서 실행되는 명령줄 환경인 Google Cloud Shell을 사용합니다.

이 Debian 기반 가상 머신에는 필요한 모든 개발 도구가 로드되어 있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 즉, 이 Codelab에 필요한 것은 브라우저뿐입니다(Chromebook에서도 작동 가능).

  1. Cloud Console에서 Cloud Shell을 활성화하려면 단순히 Cloud Shell 활성화gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A를 클릭합니다. 환경을 프로비저닝하고 연결하는 데 몇 정도만 소요됩니다.

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSrDc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjvIEx9pIkE-246DomWuCfiGHK78DgoeWkHRw

Screen Shot 2017-06-14 at 10.13.43 PM.png

Cloud Shell에 연결되면 사용자 인증이 이미 완료되었고 프로젝트가 내 PROJECT_ID에 설정되어 있음을 확인할 수 있습니다.

gcloud auth list

명령어 결과

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

명령어 결과

[core]
project = <PROJECT_ID>

어떤 이유로든 프로젝트가 설정되지 않았으면 다음 명령어를 실행하면 됩니다.

gcloud config set project <PROJECT_ID>

PROJECT_ID를 찾고 계신가요? 설정 단계에서 사용한 ID를 확인하거나 Cloud Console 대시보드에서 확인하세요.

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

또한 Cloud Shell은 기본적으로 이후 명령어를 실행할 때 유용할 수 있는 몇 가지 환경 변수를 설정합니다.

echo $GOOGLE_CLOUD_PROJECT

명령어 결과

<PROJECT_ID>
  1. 마지막으로 기본 영역 및 프로젝트 구성을 설정합니다.
gcloud config set compute/zone us-central1-f

다양한 영역을 선택할 수 있습니다. 자세한 내용은 리전 및 영역을 참조하세요.

요약

이 단계에서는 환경을 설정합니다.

다음 항목

이제 Cloud Spanner 인스턴스를 설정합니다.

이 단계에서는 이 Codelab을 위해 Cloud Spanner 인스턴스를 설정합니다. 왼쪽 위에 있는 햄버거 메뉴 3129589f7bc9e5ce.png에서 Spanner 항목 1a6580bd3d3e6783.png을 검색하거나 '/'를 누르고 'Spanner'를 입력하여 Spanner를 검색합니다.

36e52f8df8e13b99.png

그런 후 95269e75bc8c3e4d.png를 클릭하고 해당 인스턴스 이름에 cloudspanner-leaderboard를 입력하고, 구성을 선택(리전 인스턴스 선택)하여 양식을 작성하고, 노드 수를 설정합니다. 이 Codelab에서는 1개 노드만 필요합니다. 프로덕션 인스턴스의 경우 그리고 Cloud Spanner SLA를 받기 위해서는 Cloud Spanner 인스턴스에서 노드를 3개 이상 실행해야 합니다.

마지막으로 '만들기'를 클릭하면 몇 초 지나지 않아 Cloud Spanner 인스턴스가 준비됩니다.

dceb68e9ed3801e8.png

다음 단계에서는 자바 클라이언트 라이브러리를 사용하여 새 인스턴스에 데이터베이스 및 스키마를 만듭니다.

이 단계에서는 샘플 데이터베이스 및 스키마를 만듭니다.

자바 클라이언트 라이브러리를 사용하여 테이블 2개를 만듭니다. 하나는 플레이어 정보를 위한 플레이어 테이블이고, 다른 하나는 플레이어 점수를 저장하기 위한 점수 테이블입니다. 이렇게 하기 위해 Cloud Shell에서 자바 콘솔 애플리케이션을 만드는 단계를 수행합니다.

먼저 Cloud Shell에 다음 명령어를 입력하여 GitHub에서 이 Codelab의 샘플 코드를 클론합니다.

git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

그런 후 애플리케이션을 만들려는 'applications' 디렉터리로 변경합니다.

cd java-docs-samples/spanner/leaderboard

이 Codelab에 필요한 모든 코드는 이 Codelab을 진행할 때 참조할 수 있도록 기존 java-docs-samples/spanner/leaderboard/complete 디렉터리에 Leaderboard라는 실행 가능한 C# 애플리케이션으로 제공되어 있습니다. 여기에서는 새 디렉터리를 만들고 각 단계에서 Leaderboard 애플리케이션의 복사본을 빌드합니다.

애플리케이션에 대해 'codelab'이라는 새 디렉터리를 만들고 다음 명령어를 사용하여 여기로 디렉터리를 변경합니다.

mkdir codelab && cd $_

다음 Maven(mvn) 명령어를 사용하여 'Leaderboard'라는 새로운 기본적인 자바 애플리케이션을 만듭니다.

mvn -B archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.google.codelabs -DartifactId=leaderboard -DarchetypeVersion=1.4

이 명령어는 2개의 기본 파일인 Maven 앱 구성 파일 pom.xml과 자바 앱 파일인 App.java로 구성되는 간단한 콘솔 애플리케이션을 만듭니다.

그런 후 바로 전에 만든 leaderboard 디렉터리로 이동하고 해당 콘텐츠를 나열합니다.

cd leaderboard && ls

pom.xmlsrc 디렉터리가 나열됩니다.

pom.xml  src

이제 자바 Spanner 클라이언트 라이브러리를 사용하여 2개 테이블 즉, Players 및 Scores로 구성된 리더보드를 만들기 위해 App.java를 수정하여 이 콘솔 앱을 업데이트합니다. 이 작업은 Cloud Shell 편집기에서 바로 수행할 수 있습니다.

아래에서 강조표시된 아이콘을 클릭하여 Cloud Shell 편집기를 엽니다.

73cf70e05f653ca.png

리더보드 폴더에서 pom.xml을 엽니다. java-docs-samples\ spanner\leaderboard\codelab\leaderboard 폴더에 있는 pom.xml 파일을 엽니다. 이 파일은 모든 종속 항목을 포함하여 애플리케이션을 jar로 빌드하도록 maven 빌드 시스템을 구성합니다.

기존 </properties> 요소 아래에 다음 1개의 새 종속 항목 관리 섹션을 추가합니다.

<dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-bom</artifactId>
        <version>0.83.0-alpha</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

또한 기존 <dependencies> 섹션에 1개의 새 종속 항목을 추가합니다. 그러면 Cloud Spanner 자바 클라이언트 라이브러리가 애플리케이션에 추가됩니다.

    <dependency>
      <!-- Version auto-managed by BOM -->
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-spanner</artifactId>
    </dependency>

그런 후 pom.xml 파일의 기존 <build> 섹션을 다음 <build> 섹션으로 바꿉니다.

 <build>
    <plugins>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>2.5.5</version>
        <configuration>
          <finalName>leaderboard</finalName>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>com.google.codelabs.App</mainClass>
            </manifest>
          </archive>
          <appendAssemblyId>false</appendAssemblyId>
          <attach>false</attach>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>3.0.0-M3</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M3</version>
        <configuration>
            <useSystemClassLoader>false</useSystemClassLoader>
        </configuration>
      </plugin>
    </plugins>
  </build>

Cloud Shell 편집기의 '파일' 메뉴에서 '저장'을 선택하거나 'Ctrl'과 'S' 키보드 키를 함께 눌러서 변경사항을 pom.xml 파일에 저장합니다.

그런 후 Cloud Shell 편집기에서 src/main/java/com/google/codelabs/ 폴더에 있는 App.java 파일을 엽니다. 다음 자바 코드를 App.java 파일에 붙여넣어서 파일의 기존 코드를 leaderboard 데이터베이스와 PlayersScores 테이블을 만드는 데 필요한 코드로 바꿉니다.

package com.google.codelabs;

import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;

/**
 * Example code for using the Cloud Spanner API with the Google Cloud Java client library
 * to create a simple leaderboard.
 *
 * This example demonstrates:
 *
 * <p>
 *
 * <ul>
 *   <li>Creating a Cloud Spanner database.
 * </ul>
 */
public class App {

  static void create(DatabaseAdminClient dbAdminClient, DatabaseId db) {
    OperationFuture<Database, CreateDatabaseMetadata> op =
        dbAdminClient.createDatabase(
            db.getInstanceId().getInstance(),
            db.getDatabase(),
            Arrays.asList(
                "CREATE TABLE Players(\n"
                    + "  PlayerId INT64 NOT NULL,\n"
                    + "  PlayerName STRING(2048) NOT NULL\n"
                    + ") PRIMARY KEY(PlayerId)",
                "CREATE TABLE Scores(\n"
                    + "  PlayerId INT64 NOT NULL,\n"
                    + "  Score INT64 NOT NULL,\n"
                    + "  Timestamp TIMESTAMP NOT NULL\n"
                    + "  OPTIONS(allow_commit_timestamp=true)\n"
                    + ") PRIMARY KEY(PlayerId, Timestamp),\n"
                    + "INTERLEAVE IN PARENT Players ON DELETE NO ACTION"));
    try {
      // Initiate the request which returns an OperationFuture.
      Database dbOperation = op.get();
      System.out.println("Created database [" + dbOperation.getId() + "]");
    } catch (ExecutionException e) {
      // If the operation failed during execution, expose the cause.
      throw (SpannerException) e.getCause();
    } catch (InterruptedException e) {
      // Throw when a thread is waiting, sleeping, or otherwise occupied,
      // and the thread is interrupted, either before or during the activity.
      throw SpannerExceptionFactory.propagateInterrupt(e);
    }
  }

  static void printUsageAndExit() {
    System.out.println("Leaderboard 1.0.0");
    System.out.println("Usage:");
    System.out.println("  java -jar leaderboard.jar "
        + "<command> <instance_id> <database_id> [command_option]");
    System.out.println("");
    System.out.println("Examples:");
    System.out.println("  java -jar leaderboard.jar create my-instance example-db");
    System.out.println("      - Create a sample Cloud Spanner database along with "
        + "sample tables in your project.\n");
    System.exit(1);
  }

  public static void main(String[] args) throws Exception {
    if (!(args.length == 3 || args.length == 4)) {
      printUsageAndExit();
    }
    SpannerOptions options = SpannerOptions.newBuilder().build();
    Spanner spanner = options.getService();
    try {
      String command = args[0];
      DatabaseId db = DatabaseId.of(options.getProjectId(), args[1], args[2]);
      DatabaseClient dbClient = spanner.getDatabaseClient(db);
      DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient();
      switch (command) {
        case "create":
          create(dbAdminClient, db);
          break;
        default:
          printUsageAndExit();
      }
    } finally {
      spanner.close();
    }
    System.out.println("Closed client");
  }
}

Cloud Shell 편집기의 '파일' 메뉴에서 '저장'을 선택하여 변경사항을 App.java 파일에 저장합니다.

java-docs-samples/spanner/leaderboard/step4/src 디렉터리에서 App.java 파일을 사용하여 create 명령어를 사용 설정하기 위해 코드를 추가한 후 App.java 파일에 필요한 결과 예시를 확인할 수 있습니다.

앱을 빌드하기 위해 pom.xml이 있는 디렉터리에서 mvn 패키지를 실행합니다.

mvn package

자바 jar 파일이 성공적으로 빌드되면 다음 명령어를 입력하여 Cloud Shell에서 결과 애플리케이션을 실행합니다.

java -jar target/leaderboard.jar

다음과 같은 출력이 표시되어야 합니다.

Leaderboard 1.0.0
Usage:
  java -jar leaderboard.jar <command> <instance_id> <database_id> [command_option]

Examples:
  java -jar leaderboard.jar create my-instance example-db
      - Create a sample Cloud Spanner database along with sample tables in your project.

이 응답에서 현재 사용 가능한 명령어 create 하나를 포함하는 Leaderboard 애플리케이션을 확인할 수 있습니다. create 명령어에 필요한 인수는 인스턴스 ID와 데이터베이스 ID인 것을 알 수 있습니다.

이제 다음 명령어를 실행해 보세요.

java -jar target/leaderboard.jar create cloudspanner-leaderboard leaderboard

몇 초 후 다음과 같은 응답이 표시됩니다.

Created database [projects/your-project/instances/cloudspanner-leaderboard/databases/leaderboard]

Cloud Console의 Cloud Spanner 섹션에서 새 데이터베이스 및 테이블이 왼쪽 측면 메뉴에 표시됩니다.

ba9008bb84cb90b0.png

다음 단계에서 일부 데이터를 새 데이터베이스에 로드하도록 애플리케이션을 업데이트합니다.

이제 leaderboard라는 데이터베이스에 PlayersScores라는 두 테이블이 포함되었습니다. 이제 자바 클라이언트 라이브러리를 사용하여 Players 테이블에 플레이어를 입력하고 Scores 테이블에 각 플레이어의 무작위 점수를 입력합니다.

아직 열려 있지 않으면 아래 강조 표시된 아이콘을 클릭하여 Cloud Shell 편집기를 엽니다.

ef49fcbaaed19024.png

그런 후 Cloud Shell 편집기에서 App.java 파일을 수정하여 Players 테이블에 100명의 플레이어를 삽입하기 위해 사용하거나 Players 테이블에 있는 각 플레이어에 대해 Scores 테이블에 4개의 무작위 점수를 삽입하기 위해 사용할 수 있는 insert 명령어를 추가합니다.

먼저 앱 파일의 상단에서 imports 섹션을 업데이트하고 현재 있는 항목을 바꿔서 다음과 같이 표시되도록 합니다.

package com.google.codelabs;

import static com.google.cloud.spanner.TransactionRunner.TransactionCallable;

import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TransactionContext;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;

그런 후 기존 create() 메서드 아래와 기존 printUsageAndExit() 메서드 위에 다음 insert, insertPlayers, insertScores 메서드를 추가합니다.

  static void insert(DatabaseClient dbClient, String insertType) {
    try {
      insertType = insertType.toLowerCase();
    } catch (Exception e) {
      // Invalid input received, set insertType to empty string.
      insertType = "";
    }
    if (insertType.equals("players")) {
      // Insert players.
      insertPlayers(dbClient);
    } else if (insertType.equals("scores")) {
      // Insert scores.
      insertScores(dbClient);
    } else {
      // Invalid input.
      System.out.println("Invalid value for 'type of insert'. "
          + "Specify a valid value: 'players' or 'scores'.");
      System.exit(1);
    }
  }

  static void insertPlayers(DatabaseClient dbClient) {
    dbClient
        .readWriteTransaction()
        .run(
            new TransactionCallable<Void>() {
              @Override
              public Void run(TransactionContext transaction) throws Exception {
                // Get the number of players.
                String sql = "SELECT Count(PlayerId) as PlayerCount FROM Players";
                ResultSet resultSet = transaction.executeQuery(Statement.of(sql));
                long numberOfPlayers = 0;
                if (resultSet.next()) {
                  numberOfPlayers = resultSet.getLong("PlayerCount");
                }
                // Insert 100 player records into the Players table.
                List<Statement> stmts = new ArrayList<Statement>();
                long randomId;
                for (int x = 1; x <= 100; x++) {
                  numberOfPlayers++;
                  randomId = (long) Math.floor(Math.random() * 9_000_000_000L) + 1_000_000_000L;
                  Statement statement =
                      Statement
                        .newBuilder(
                            "INSERT INTO Players (PlayerId, PlayerName) "
                            + "VALUES (@PlayerId, @PlayerName) ")
                        .bind("PlayerId")
                        .to(randomId)
                        .bind("PlayerName")
                        .to("Player " + numberOfPlayers)
                        .build();
                  stmts.add(statement);
                }
                transaction.batchUpdate(stmts);
                return null;
              }
            });
    System.out.println("Done inserting player records...");
  }

  static void insertScores(DatabaseClient dbClient) {
    boolean playerRecordsFound = false;
    ResultSet resultSet =
        dbClient
            .singleUse()
            .executeQuery(Statement.of("SELECT * FROM Players"));
    while (resultSet.next()) {
      playerRecordsFound = true;
      final long playerId = resultSet.getLong("PlayerId");
      dbClient
          .readWriteTransaction()
          .run(
              new TransactionCallable<Void>() {
                @Override
                public Void run(TransactionContext transaction) throws Exception {
                  // Initialize objects for random Score and random Timestamp.
                  LocalDate endDate = LocalDate.now();
                  long end = endDate.toEpochDay();
                  int startYear = endDate.getYear() - 2;
                  int startMonth = endDate.getMonthValue();
                  int startDay = endDate.getDayOfMonth();
                  LocalDate startDate = LocalDate.of(startYear, startMonth, startDay);
                  long start = startDate.toEpochDay();
                  Random r = new Random();
                  List<Statement> stmts = new ArrayList<Statement>();
                  // Insert 4 score records into the Scores table
                  // for each player in the Players table.
                  for (int x = 1; x <= 4; x++) {
                    // Generate random score between 1,000,000 and 1,000
                    long randomScore = r.nextInt(1000000 - 1000) + 1000;
                    // Get random day within the past two years.
                    long randomDay = ThreadLocalRandom.current().nextLong(start, end);
                    LocalDate randomDayDate = LocalDate.ofEpochDay(randomDay);
                    LocalTime randomTime = LocalTime.of(
                        r.nextInt(23), r.nextInt(59), r.nextInt(59), r.nextInt(9999));
                    LocalDateTime randomDate = LocalDateTime.of(randomDayDate, randomTime);
                    Instant randomInstant = randomDate.toInstant(ZoneOffset.UTC);
                    Statement statement =
                        Statement
                        .newBuilder(
                          "INSERT INTO Scores (PlayerId, Score, Timestamp) "
                          + "VALUES (@PlayerId, @Score, @Timestamp) ")
                        .bind("PlayerId")
                        .to(playerId)
                        .bind("Score")
                        .to(randomScore)
                        .bind("Timestamp")
                        .to(randomInstant.toString())
                        .build();
                    stmts.add(statement);
                  }
                  transaction.batchUpdate(stmts);
                  return null;
                }
              });

    }
    if (!playerRecordsFound) {
      System.out.println("Parameter 'scores' is invalid since "
          + "no player records currently exist. First insert players "
          + "then insert scores.");
      System.exit(1);
    } else {
      System.out.println("Done inserting score records...");
    }
  }

그런 후 insert 명령어 작동을 위해 switch (command) 문 내에서 앱의 'main' 메서드에 다음 코드를 추가합니다.

        case "insert":
          String insertType;
          try {
            insertType = args[3];
          } catch (ArrayIndexOutOfBoundsException exception) {
            insertType = "";
          }
          insert(dbClient, insertType);
          break;

완료되면 switch (command) 문이 다음과 같이 표시됩니다.

      switch (command) {
        case "create":
          create(dbAdminClient, db);
          break;
        case "insert":
          String insertType;
          try {
            insertType = args[3];
          } catch (ArrayIndexOutOfBoundsException exception) {
            insertType = "";
          }
          insert(dbClient, insertType);
          break;
        default:
          printUsageAndExit();
      }

앱에 '삽입' 기능 추가를 완료하기 위한 마지막 단계는 printUsageAndExit() 메서드에 '삽입' 명령어에 대한 도움말 텍스트를 추가하는 것입니다. 다음 코드 줄을 printUsageAndExit() 메서드에 추가하여 삽입 명령어에 대한 도움말 텍스트를 포함합니다.

    System.out.println("  java -jar leaderboard.jar insert my-instance example-db players");
    System.out.println("      - Insert 100 sample Player records into the database.\n");
    System.out.println("  java -jar leaderboard.jar insert my-instance example-db scores");
    System.out.println("      - Insert sample score data into Scores sample Cloud Spanner "
        + "database table.\n");

Cloud Shell 편집기의 '파일' 메뉴에서 '저장'을 선택하여 변경사항을 App.java 파일에 저장합니다.

java-docs-samples/spanner/leaderboard/step5/src 디렉터리에서 App.java 파일을 사용하여 insert 명령어를 사용 설정하기 위해 코드를 추가한 후 App.java 파일에 필요한 결과 예시를 확인할 수 있습니다.

이제 앱을 다시 빌드하고 실행하여 새 insert 명령어가 앱의 사용 가능한 명령어 목록에 포함되었는지 확인합니다.

앱을 빌드하려면 pom.xml이 있는 디렉터리에서 mvn package를 실행합니다.

mvn package

자바 jar 파일이 성공적으로 빌드되었으면 다음 명령어를 실행합니다.

java -jar target/leaderboard.jar

이제 insert 명령어가 앱의 기본 출력에 포함된 것을 확인할 수 있습니다.

Leaderboard 1.0.0
Usage:
  java -jar leaderboard.jar <command> <instance_id> <database_id> [command_option]

Examples:
  java -jar leaderboard.jar create my-instance example-db
      - Create a sample Cloud Spanner database along with sample tables in your project.

  java -jar leaderboard.jar insert my-instance example-db players
      - Insert 100 sample Player records into the database.

  java -jar leaderboard.jar insert my-instance example-db scores
      - Insert sample score data into Scores sample Cloud Spanner database table.

응답에서 인스턴스 ID 및 데이터베이스 ID 외에도 '플레이어' 또는 '점수' 값을 가질 수 있는 다른 인수가 있는 것을 확인할 수 있습니다.

이제 create 명령어를 호출할 때 사용한 것과 동일한 인수 값을 사용하여 insert 명령어를 실행하고, 추가적인 '삽입 유형' 인수로 'players'를 추가합니다.

java -jar target/leaderboard.jar insert cloudspanner-leaderboard leaderboard players

몇 초 후 다음과 같은 응답이 표시됩니다.

Done inserting player records...

이제 자바 클라이언트 라이브러리를 사용하여 Players 테이블의 각 플레이어에 대해 타임스탬프와 함께 4개의 무작위 점수를 Scores 테이블에 입력합니다.

Scores 테이블의 Timestamp 열은 이전에 create 명령어를 실행했을 때 수행된 다음 SQL 문을 통해 '커밋 타임스탬프' 열로 정의되었습니다.

CREATE TABLE Scores(
  PlayerId INT64 NOT NULL,
  Score INT64 NOT NULL,
  Timestamp TIMESTAMP NOT NULL OPTIONS(allow_commit_timestamp=true)
) PRIMARY KEY(PlayerId, Timestamp),
    INTERLEAVE IN PARENT Players ON DELETE NO ACTION

OPTIONS(allow_commit_timestamp=true) 속성을 확인합니다. 그러면 Timestamp가 '커밋 타임스탬프' 열로 지정되고 제공된 테이블 행에서 INSERT 및 UPDATE 작업에 대한 정확한 트랜잭션 타임스탬프로 자동으로 입력되도록 만듭니다.

또한 타임스탬프에 과거 시간의 값을 삽입하는 한 고유 타임스탬프 값을 '커밋 타임스탬프' 열에 삽입할 수 있습니다. 이 작업은 이 Codelab에서 수행됩니다.

이제 create 명령어를 호출할 때 사용한 것과 동일한 인수 값을 사용하여 insert 명령어를 실행하고, 추가적인 '삽입 유형' 인수로 'scores'를 추가합니다.

java -jar target/leaderboard.jar insert cloudspanner-leaderboard leaderboard scores

몇 초 후 다음과 같은 응답이 표시됩니다.

Done inserting score records...

'삽입 유형'을 scores로 지정하여 insert를 실행하면 과거의 날짜/시간으로 무작위로 생성된 타임스탬프를 삽입하기 위해 다음 코드 스니펫을 사용하는 insertScores 메서드를 호출합니다.

          LocalDate endDate = LocalDate.now();
          long end = endDate.toEpochDay();
          int startYear = endDate.getYear() - 2;
          int startMonth = endDate.getMonthValue();
          int startDay = endDate.getDayOfMonth();
          LocalDate startDate = LocalDate.of(startYear, startMonth, startDay);
          long start = startDate.toEpochDay();
...
            long randomDay = ThreadLocalRandom.current().nextLong(start, end);
            LocalDate randomDayDate = LocalDate.ofEpochDay(randomDay);
            LocalTime randomTime = LocalTime.of(
                        r.nextInt(23), r.nextInt(59), r.nextInt(59), r.nextInt(9999));
            LocalDateTime randomDate = LocalDateTime.of(randomDayDate, randomTime);
            Instant randomInstant = randomDate.toInstant(ZoneOffset.UTC);

...
               .bind("Timestamp")
               .to(randomInstant.toString())

정확히 '삽입' 트랜잭션이 수행될 때의 타임스탬프를 Timestamp 열에 자동으로 입력하려면 다음 코드 스니펫과 같이 자바 상수 Value.COMMIT_TIMESTAMP를 대신 삽입할 수 있습니다.

               .bind("Timestamp")
               .to(Value.COMMIT_TIMESTAMP)

이제 데이터 로드가 완료되었으므로 새 테이블에 기록한 값을 확인합니다. 먼저 leaderboard 데이터베이스를 선택하고 Players 테이블을 선택합니다. Data 탭을 클릭합니다. 테이블의 PlayerIdPlayerName 열에 데이터가 포함된 것을 알 수 있습니다.

7bc2c96293c31c49.png

이제 Scores 테이블을 클릭하고 Data 탭을 선택하여 점수 테이블에도 데이터가 있는지 확인합니다. 테이블의 PlayerId, Timestamp, Score 열에 데이터가 포함된 것을 알 수 있습니다.

d8a4ee4f13244c19.png

잘 하셨습니다. 게임 리더보드를 만들기 위해 사용할 수 있는 몇 가지 쿼리를 실행하기 위해 앱을 업데이트해보겠습니다.

이제 데이터베이스가 설정되었고 테이블에 정보가 로드되었으므로, 이 데이터를 사용하는 리더보드를 만듭니다. 이를 위해서는 다음 4개 질문에 답변해야 합니다.

  1. 항상 '상위 10대' 플레이어는 누구인가요?
  2. 올해 '상위 10대' 플레이어는 누구인가요?
  3. 이달의 '상위 10대' 플레이어는 누구인가요?
  4. 이번 주 '상위 10대' 플레이어는 누구인가요?

이러한 질문에 답변하는 SQL 쿼리를 실행하도록 앱을 업데이트합니다.

리더보드에 필요한 정보를 생성하는 질문에 답변하도록 쿼리를 실행할 수 있는 방법을 제공하는 query 명령어를 추가합니다.

Cloud Shell 편집기에서 App.java 파일을 수정하여 query 명령어를 추가하도록 앱을 업데이트합니다. query 명령어는 2개의 query 메서드로 구성되며, 하나는 DatabaseClient 인수만 사용하고, 다른 하나는 추가적인 timespan 인수를 사용합니다. 이렇게 해서 시간 단위로 지정되는 일정 기간 동안의 결과를 필터링할 수 있습니다.

다음 2개의 query 메서드를 기존 insertScores() 메서드 아래와 기존 printUsageAndExit() 메서드 위에 추가합니다.

  static void query(DatabaseClient dbClient) {
    String scoreDate;
    String score;
    ResultSet resultSet =
        dbClient
            .singleUse()
            .executeQuery(
                Statement.of(
                    "SELECT p.PlayerId, p.PlayerName, s.Score, s.Timestamp "
                        + "FROM Players p "
                        + "JOIN Scores s ON p.PlayerId = s.PlayerId "
                        + "ORDER BY s.Score DESC LIMIT 10"));
    while (resultSet.next()) {
      scoreDate = String.valueOf(resultSet.getTimestamp("Timestamp"));
      score = String.format("%,d", resultSet.getLong("Score"));
      System.out.printf(
          "PlayerId: %d  PlayerName: %s  Score: %s  Timestamp: %s\n",
          resultSet.getLong("PlayerId"), resultSet.getString("PlayerName"), score,
          scoreDate.substring(0,10));
    }
  }

  static void query(DatabaseClient dbClient, int timespan) {
    String scoreDate;
    String score;
    Statement statement =
        Statement
            .newBuilder(
              "SELECT p.PlayerId, p.PlayerName, s.Score, s.Timestamp "
              + "FROM Players p "
              + "JOIN Scores s ON p.PlayerId = s.PlayerId "
              + "WHERE s.Timestamp > "
              + "TIMESTAMP_SUB(CURRENT_TIMESTAMP(), "
              + "    INTERVAL @Timespan HOUR) "
              + "ORDER BY s.Score DESC LIMIT 10")
            .bind("Timespan")
            .to(timespan)
            .build();
    ResultSet resultSet =
        dbClient
            .singleUse()
            .executeQuery(statement);
    while (resultSet.next()) {
      scoreDate = String.valueOf(resultSet.getTimestamp("Timestamp"));
      score = String.format("%,d", resultSet.getLong("Score"));
      System.out.printf(
          "PlayerId: %d  PlayerName: %s  Score: %s  Timestamp: %s\n",
          resultSet.getLong("PlayerId"), resultSet.getString("PlayerName"), score,
          scoreDate.substring(0,10));
    }
  }

그런 후 query 명령어 작동을 위해 다음 코드를 앱의 'main' 메서드에 있는 switch(command) 문에 추가합니다.

        case "query":
          if (args.length == 4) {
            int timespan = 0;
            try {
              timespan = Integer.parseInt(args[3]);
            } catch (NumberFormatException e) {
              System.err.println("query command's 'timespan' parameter must be a valid integer.");
              System.exit(1);
            }
            query(dbClient, timespan);
          } else {
            query(dbClient);
          }
          break;

앱에 '쿼리' 기능 추가를 완료하기 위한 마지막 단계는 printUsageAndExit() 메서드에 '쿼리' 명령어에 대한 도움말 텍스트를 추가하는 것입니다. '쿼리' 명령어에 대한 도움말 텍스트를 포함하도록 printUsageAndExit() 메서드에 다음 코드 줄을 추가합니다.

    System.out.println("  java -jar leaderboard.jar query my-instance example-db");
    System.out.println("      - Query players with top ten scores of all time.\n");
    System.out.println("  java -jar leaderboard.jar query my-instance example-db 168");
    System.out.println("      - Query players with top ten scores within a timespan "
        + "specified in hours.\n");

Cloud Shell 편집기의 '파일' 메뉴에서 '저장'을 선택하여 변경사항을 App.java 파일에 저장합니다.

dotnet-docs-samples/applications/leaderboard/step6/src 디렉터리에서 App.java 파일을 사용하여 query 명령어를 사용 설정하기 위해 코드를 추가한 후 App.java 파일에 필요한 결과 예시를 확인할 수 있습니다.

앱을 빌드하려면 pom.xml이 있는 디렉터리에서 mvn package를 실행합니다.

mvn package

이제 앱을 실행하여 새 query 명령어가 앱의 사용 가능한 명령어 목록에 포함되었는지 확인합니다. 다음 명령어를 실행합니다.

java -jar target/leaderboard.jar

이제 query 명령어가 앱의 기본 출력에서 새 명령어 옵션으로 포함된 것을 확인할 수 있습니다.

Leaderboard 1.0.0
Usage:
  java -jar leaderboard.jar <command> <instance_id> <database_id> [command_option]

Examples:
  java -jar leaderboard.jar create my-instance example-db
      - Create a sample Cloud Spanner database along with sample tables in your project.

  java -jar leaderboard.jar insert my-instance example-db players
      - Insert 100 sample Player records into the database.

  java -jar leaderboard.jar insert my-instance example-db scores
      - Insert sample score data into Scores sample Cloud Spanner database table.

  java -jar leaderboard.jar query my-instance example-db
      - Query players with top ten scores of all time.

  java -jar leaderboard.jar query my-instance example-db 168
      - Query players with top ten scores within a timespan specified in hours.

이제 응답을 통해 인스턴스 ID 및 데이터베이스 ID 인수 외에도 query 명령어를 사용하여 Scores 테이블의 Timestamp 열에 있는 값을 기준으로 레코드를 필터링하는 데 사용할 수 있는 선택적인 일정 기간(시간 단위)을 지정할 수 있음을 알 수 있습니다. 기간 인수는 선택사항이기 때문에 기간 인수를 포함하지 않으면 타임스탬프로 레코드가 필터링되지 않습니다. 따라서 '기간' 값 없이 query 명령어를 사용하여 항상 '상위 10대' 플레이어 목록을 가져올 수 있습니다.

'기간'을 지정하지 않고 create 명령어를 실행했을 때 사용한 것과 동일한 인수 값을 사용하여 query 명령어를 실행합니다.

java -jar target/leaderboard.jar query cloudspanner-leaderboard leaderboard

다음과 같이 항상 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.

PlayerId: 4018687297  PlayerName: Player 83  Score: 999,618  Timestamp: 2017-07-01
PlayerId: 4018687297  PlayerName: Player 83  Score: 998,956  Timestamp: 2017-09-02
PlayerId: 4285713246  PlayerName: Player 51  Score: 998,648  Timestamp: 2017-12-01
PlayerId: 5267931774  PlayerName: Player 49  Score: 997,733  Timestamp: 2017-11-09
PlayerId: 1981654448  PlayerName: Player 35  Score: 997,480  Timestamp: 2018-12-06
PlayerId: 4953940705  PlayerName: Player 87  Score: 995,184  Timestamp: 2018-09-14
PlayerId: 2456736905  PlayerName: Player 84  Score: 992,881  Timestamp: 2017-04-14
PlayerId: 8234617611  PlayerName: Player 19  Score: 992,399  Timestamp: 2017-12-27
PlayerId: 1788051688  PlayerName: Player 76  Score: 992,265  Timestamp: 2018-11-22
PlayerId: 7127686505  PlayerName: Player 97  Score: 992,038  Timestamp: 2017-12-02

이제 1년 동안의 시간 수인 8760과 동일한 '기간'을 지정하여 1년 동안 '상위 10대' 플레이어를 쿼리하는 데 필요한 인수를 사용하여 query 명령어를 실행합니다.

java -jar target/leaderboard.jar query cloudspanner-leaderboard leaderboard 8760

다음과 같이 1년 동안 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.

PlayerId: 1981654448  PlayerName: Player 35  Score: 997,480  Timestamp: 2018-12-06
PlayerId: 4953940705  PlayerName: Player 87  Score: 995,184  Timestamp: 2018-09-14
PlayerId: 1788051688  PlayerName: Player 76  Score: 992,265  Timestamp: 2018-11-22
PlayerId: 6862349579  PlayerName: Player 30  Score: 990,877  Timestamp: 2018-09-14
PlayerId: 5529627211  PlayerName: Player 16  Score: 989,142  Timestamp: 2018-03-30
PlayerId: 9743904155  PlayerName: Player 1  Score: 988,765  Timestamp: 2018-05-30
PlayerId: 6809119884  PlayerName: Player 7  Score: 986,673  Timestamp: 2018-05-16
PlayerId: 2132710638  PlayerName: Player 54  Score: 983,108  Timestamp: 2018-09-11
PlayerId: 2320093590  PlayerName: Player 79  Score: 981,373  Timestamp: 2018-05-07
PlayerId: 9554181430  PlayerName: Player 80  Score: 981,087  Timestamp: 2018-06-21

이제 query 명령어를 실행하고 '기간'을 1개월 동안의 시간 수인 730과 동일하게 지정하여 1개월 동안 '상위 10'대 플레이어를 쿼리합니다.

java -jar target/leaderboard.jar query cloudspanner-leaderboard leaderboard 730

다음과 같이 1개월 동안 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.

PlayerId: 3869829195  PlayerName: Player 69  Score: 949,686  Timestamp: 2019-02-19
PlayerId: 7448359883  PlayerName: Player 20  Score: 938,998  Timestamp: 2019-02-07
PlayerId: 1981654448  PlayerName: Player 35  Score: 929,003  Timestamp: 2019-02-22
PlayerId: 9336678658  PlayerName: Player 44  Score: 914,106  Timestamp: 2019-01-27
PlayerId: 6968576389  PlayerName: Player 40  Score: 898,041  Timestamp: 2019-02-21
PlayerId: 5529627211  PlayerName: Player 16  Score: 896,433  Timestamp: 2019-01-29
PlayerId: 9395039625  PlayerName: Player 59  Score: 879,495  Timestamp: 2019-02-09
PlayerId: 2094604854  PlayerName: Player 39  Score: 860,434  Timestamp: 2019-02-01
PlayerId: 9395039625  PlayerName: Player 59  Score: 849,955  Timestamp: 2019-02-21
PlayerId: 4285713246  PlayerName: Player 51  Score: 805,654  Timestamp: 2019-02-02

이제 query 명령어를 실행하고 '기간'을 1주일 동안의 시간 수인 168과 동일하게 지정하여 1주일 동안 '상위 10'대 플레이어를 쿼리합니다.

java -jar target/leaderboard.jar query cloudspanner-leaderboard leaderboard 168

다음과 같이 1주일 동안 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.

PlayerId: 3869829195  PlayerName: Player 69  Score: 949,686  Timestamp: 2019-02-19
PlayerId: 1981654448  PlayerName: Player 35  Score: 929,003  Timestamp: 2019-02-22
PlayerId: 6968576389  PlayerName: Player 40  Score: 898,041  Timestamp: 2019-02-21
PlayerId: 9395039625  PlayerName: Player 59  Score: 849,955  Timestamp: 2019-02-21
PlayerId: 5954045812  PlayerName: Player 8  Score: 795,639  Timestamp: 2019-02-22
PlayerId: 3889939638  PlayerName: Player 71  Score: 775,252  Timestamp: 2019-02-21
PlayerId: 5529627211  PlayerName: Player 16  Score: 604,695  Timestamp: 2019-02-19
PlayerId: 9006728426  PlayerName: Player 3  Score: 457,208  Timestamp: 2019-02-22
PlayerId: 8289497066  PlayerName: Player 58  Score: 227,697  Timestamp: 2019-02-20
PlayerId: 8065482904  PlayerName: Player 99  Score: 198,429  Timestamp: 2019-02-24

훌륭합니다!

이제 필요한 레코드가 아무리 커도 레코드를 추가하면 Cloud Spanner가 데이터베이스를 확장합니다. 데이터베이스가 얼마나 커지더라도 Cloud Spanner 및 Truetime 기술을 통해 게임 리더보드가 계속 정확하게 확장될 수 있습니다.

Spanner에 대한 모든 학습이 완료되었으면 귀중한 리소스와 비용을 절약하기 위해 사용된 리소스를 삭제해야 합니다. 이 단계는 매우 간단합니다. Cloud Console의 Cloud Spanner 섹션으로 이동하고 'Cloud Spanner 인스턴스 설정' Codelab 단계에서 만든 인스턴스만 삭제하면 됩니다.

학습한 내용:

  • 리더보드에 대한 Google Cloud Spanner 인스턴스, 데이터베이스, 테이블 스키마
  • 자바 콘솔 애플리케이션을 만드는 방법
  • 자바 클라이언트 라이브러리를 사용하여 Spanner 데이터베이스 및 테이블을 만드는 방법
  • 자바 클라이언트 라이브러리를 사용하여 Spanner 데이터베이스에 데이터를 로드하는 방법
  • Spanner 커밋 타임스탬프 및 자바 클라이언트 라이브러리를 사용하여 데이터에서 '상위 10대' 결과를 쿼리하는 방법

다음 단계:

Google에 의견 보내기

  • 잠시 시간을 내어 간단한 설문조사에 응해주시기 바랍니다.