자바 시작하기

이 가이드는 Google Cloud에 적용되는 주요 앱 개발 개념을 배우려는 엔지니어 및 웹 개발자와 같이 클라우드에서 앱을 처음 빌드하는 사용자를 대상으로 합니다.

목표

비용

이 문서에서는 비용이 청구될 수 있는 다음과 같은 Google Cloud 구성요소를 사용합니다.

이 문서의 안내는 리소스 사용량을 Google Cloud의 항상 무료 등급 한도 내에서 유지하도록 작성되었습니다. 프로젝트 사용량을 기준으로 예상 비용을 산출하려면 가격 계산기를 사용하세요. Google Cloud를 처음 사용하는 사용자는 무료 체험판을 사용할 수 있습니다.

이 문서에 설명된 태스크를 완료했으면 만든 리소스를 삭제하여 청구가 계속되는 것을 방지할 수 있습니다. 자세한 내용은 삭제를 참조하세요.

시작하기 전에

  1. Google Cloud 계정에 로그인합니다. Google Cloud를 처음 사용하는 경우 계정을 만들고 Google 제품의 실제 성능을 평가해 보세요. 신규 고객에게는 워크로드를 실행, 테스트, 배포하는 데 사용할 수 있는 $300의 무료 크레딧이 제공됩니다.
  2. Google Cloud Console의 프로젝트 선택기 페이지에서 Google Cloud 프로젝트를 선택하거나 만듭니다.

    프로젝트 선택기로 이동

  3. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  4. Google Cloud Console의 프로젝트 선택기 페이지에서 Google Cloud 프로젝트를 선택하거나 만듭니다.

    프로젝트 선택기로 이동

  5. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  6. 기본 모드로 Firestore 데이터베이스를 만들려면 다음 단계를 완료하세요.
    1. Cloud Console에서 Firestore 뷰어 페이지로 이동합니다.
      Firestore 뷰어로 이동
    2. Cloud Firestore 모드 선택 화면에서 기본 모드 선택을 클릭합니다.
    3. Firestore 데이터베이스의 위치를 선택합니다. 이 위치 설정이 Cloud 프로젝트의 기본 Google Cloud 리소스 위치입니다. 이 위치는 위치 설정이 필요한 Cloud 프로젝트, 특히 기본 Cloud Storage 버킷 및 Cloud Run 앱의 Google Cloud 서비스에 사용됩니다.
    4. 데이터베이스 만들기를 클릭합니다.
  7. API Cloud Run Admin, Cloud Storage, Cloud Logging, and Error Reporting 사용 설정

    API 사용 설정

  8. 샘플 저장소를 클론하고 Cloud Shell에서 샘플 애플리케이션을 엽니다.
    Cloud Shell로 이동

    Cloud Shell을 사용하면 브라우저에서 직접 명령줄을 통해 Google Cloud 리소스에 액세스할 수 있습니다.

  9. 샘플 코드를 다운로드하고 앱 디렉터리로 변경하려면 계속을 클릭합니다.
  10. 새 Google Cloud 프로젝트를 사용하도록 Cloud Shell에서 gcloud 도구를 구성합니다.

    # Configure gcloud for your project
    gcloud config set project PROJECT_ID
    

    PROJECT_ID를 Cloud Console을 사용하여 만든 Google Cloud 프로젝트 ID로 바꿉니다.

    Google Cloud CLI는 명령줄에서 Google Cloud 리소스와 상호작용하는 주요 수단입니다. 이 튜토리얼에서는 앱을 배포하고 모니터링하는 데 gcloud 도구를 사용합니다.

앱 실행

  1. 이미 Cloud Shell을 사용하는 상태에서 자바 11을 사용하도록 구성한 경우 셸의 자바 대안인 JAVA_HOME, PATH 환경 변수가 자바 8을 지정하도록 업데이트합니다.
  2. bookshelf/1-cloud-run 디렉터리로 변경하고 앱을 실행합니다.
    GOOGLE_CLOUD_PROJECT=PROJECT_ID mvn -Plocal clean jetty:run-exploded
    
    PROJECT_ID를 이전에 만든 Google Cloud 프로젝트의 ID로 바꿉니다.
  3. Cloud Shell에서 웹 미리보기를 클릭하고, 포트 8080에서 미리보기를 선택합니다. 그러면 실행 중인 앱이 표시된 새 창이 열립니다.

Cloud Run에 앱 배포

Google Cloud는 코드를 실행하기 위한 몇 가지 옵션을 제공합니다. 이 예시에서는 Cloud Run을 사용하여 Google Cloud에 확장 가능한 앱을 배포합니다. Cloud Run은 서버를 관리할 필요가 없으며 트래픽 급증을 지원하도록 자동으로 확장됩니다.

  1. Jib을 사용하여 이미지를 빌드합니다.
    mvn package jib:build -Dimage=gcr.io/PROJECT_ID/bookshelf

    PROJECT_ID를 이전에 만든 Google Cloud 프로젝트 ID로 바꿉니다.

  2. 이미지를 배포합니다.
    gcloud run deploy bookshelf --image gcr.io/PROJECT_ID/bookshelf \
    --platform managed --region us-central1 --allow-unauthenticated
    PROJECT_ID를 이전에 만든 Google Cloud 프로젝트 ID로 바꿉니다.

배포가 성공하면 엔드포인트가 다음과 같은 형식으로 Cloud Run에서 실행되는 앱으로 출력됩니다.

https://bookshelf-abcdefghij-uc.a.run.app

이제 이 링크(이후 YOUR_CODE_RUN_URL)에서 앱을 볼 수 있습니다. 웹브라우저에서 URL을 입력하여 앱을 봅니다.

Bookshelf 앱 홈페이지

Firestore로 데이터 유지

Cloud Run 인스턴스에는 정보를 저장할 수 없습니다. 인스턴스가 다시 시작되면 정보가 손실되고 새 인스턴스 생성 시 존재하지 않기 때문입니다. 대신 모든 인스턴스가 읽고 쓸 수 있는 데이터베이스를 사용합니다.

Google Cloud는 데이터를 저장하기 위한 몇 가지 옵션을 제공합니다. 이 예시에서는 Firestore를 사용하여 각 도서의 데이터를 저장합니다. Firestore는 데이터를 저장하고 쿼리할 수 있게 해주는 완전 관리형 서버리스 NoSQL 문서 데이터베이스입니다. Firestore는 앱의 필요성에 맞게 자동 확장되며 사용하지 않을 때는 0으로 조정됩니다. 지금 첫 번째 도서를 추가하세요.

  1. 웹브라우저에서 YOUR_CODE_RUN_URL로 이동합니다.
  2. 배포한 앱에 사용할 도서를 만들려면 도서 추가를 클릭합니다.

    Bookshelf 앱에 도서 추가
  3. 제목 필드에 Moby Dick을 입력합니다.
  4. 저자 필드에 Herman Melville을 입력합니다.
  5. 저장을 클릭합니다. 이제 Bookshelf 앱에 항목이 표시됩니다.

    Moby Dick Bookshelf 앱 항목
  6. Cloud Console에서 Firestore 페이지를 새로고침하려면 새로고침 을 클릭합니다. Firestore에 데이터가 나타납니다. Bookshelf 앱은 고유한 ID가 있는 Firestore 문서로 각 도서를 저장하며, 모든 문서는 Firestore 컬렉션에 저장됩니다. 이 가이드에서는 컬렉션을 도서라고 합니다. Firestore 문서의 예시

Firestore는 Firestore 클라이언트 라이브러리를 사용하여 도서를 저장합니다. 다음은 Firestore 문서 가져오기의 예시입니다.

public class FirestoreDao implements BookDao {
  private CollectionReference booksCollection;

  public FirestoreDao() {
    Firestore firestore = FirestoreOptions.getDefaultInstance().getService();
    booksCollection = firestore.collection("books");
  }

  private Book documentToBook(DocumentSnapshot document) {
    Map<String, Object> data = document.getData();
    if (data == null) {
      System.out.println("No data in document " + document.getId());
      return null;
    }

    return new Book.Builder()
        .author((String) data.get(Book.AUTHOR))
        .description((String) data.get(Book.DESCRIPTION))
        .publishedDate((String) data.get(Book.PUBLISHED_DATE))
        .imageUrl((String) data.get(Book.IMAGE_URL))
        .createdBy((String) data.get(Book.CREATED_BY))
        .createdById((String) data.get(Book.CREATED_BY_ID))
        .title((String) data.get(Book.TITLE))
        .id(document.getId())
        .build();
  }

  @Override
  public String createBook(Book book) {
    String id = UUID.randomUUID().toString();
    DocumentReference document = booksCollection.document(id);
    Map<String, Object> data = Maps.newHashMap();

    data.put(Book.AUTHOR, book.getAuthor());
    data.put(Book.DESCRIPTION, book.getDescription());
    data.put(Book.PUBLISHED_DATE, book.getPublishedDate());
    data.put(Book.TITLE, book.getTitle());
    data.put(Book.IMAGE_URL, book.getImageUrl());
    data.put(Book.CREATED_BY, book.getCreatedBy());
    data.put(Book.CREATED_BY_ID, book.getCreatedById());
    try {
      document.set(data).get();
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }

    return id;
  }

  @Override
  public Book readBook(String bookId) {
    try {
      DocumentSnapshot document = booksCollection.document(bookId).get().get();

      return documentToBook(document);
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
    return null;
  }

  @Override
  public void updateBook(Book book) {
    DocumentReference document = booksCollection.document(book.getId());
    Map<String, Object> data = Maps.newHashMap();

    data.put(Book.AUTHOR, book.getAuthor());
    data.put(Book.DESCRIPTION, book.getDescription());
    data.put(Book.PUBLISHED_DATE, book.getPublishedDate());
    data.put(Book.TITLE, book.getTitle());
    data.put(Book.IMAGE_URL, book.getImageUrl());
    data.put(Book.CREATED_BY, book.getCreatedBy());
    data.put(Book.CREATED_BY_ID, book.getCreatedById());
    try {
      document.set(data).get();
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void deleteBook(String bookId) {
    try {
      booksCollection.document(bookId).delete().get();
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
  }

  private List<Book> documentsToBooks(List<QueryDocumentSnapshot> documents) {
    List<Book> resultBooks = new ArrayList<>();
    for (QueryDocumentSnapshot snapshot : documents) {
      resultBooks.add(documentToBook(snapshot));
    }
    return resultBooks;
  }

  @Override
  public Result<Book> listBooks(String startTitle) {
    Query booksQuery = booksCollection.orderBy("title").limit(10);
    if (startTitle != null) {
      booksQuery = booksQuery.startAfter(startTitle);
    }
    try {
      QuerySnapshot snapshot = booksQuery.get().get();
      List<Book> results = documentsToBooks(snapshot.getDocuments());
      String newCursor = null;
      if (results.size() > 0) {
        newCursor = results.get(results.size() - 1).getTitle();
      }
      return new Result<>(results, newCursor);
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
    return new Result<>(Lists.newArrayList(), null);
  }

  @Override
  public Result<Book> listBooksByUser(String userId, String startTitle) {
    Query booksQuery =
        booksCollection.orderBy("title").whereEqualTo(Book.CREATED_BY_ID, userId).limit(10);
    if (startTitle != null) {
      booksQuery = booksQuery.startAfter(startTitle);
    }
    try {
      QuerySnapshot snapshot = booksQuery.get().get();
      List<Book> results = documentsToBooks(snapshot.getDocuments());
      String newCursor = null;
      if (results.size() > 0) {
        newCursor = results.get(results.size() - 1).getTitle();
      }
      return new Result<>(results, newCursor);
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
    return new Result<>(Lists.newArrayList(), null);
  }
}

Firestore 사용에 관한 자세한 내용은 Firestore에 데이터 추가를 참조하세요.

Cloud Storage에 파일 업로드 저장

도서를 추가했으니 이제 도서 표지 이미지를 추가해 보겠습니다. 인스턴스에는 파일을 저장할 수 없습니다. 데이터베이스는 이미지 파일에 적합하지 않습니다. 대신 Cloud Storage를 사용합니다.

Cloud Storage는 Google Cloud의 기본 blob 저장소입니다. Cloud Storage를 사용하여 Google Cloud에서 공유하려는 앱 애셋을 호스팅할 수 있습니다. Cloud Storage를 사용하려면 데이터를 보관하는 기본 컨테이너인 Cloud Storage 버킷을 생성해야 합니다.

  1. Cloud Console에서 Cloud Storage 브라우저 페이지로 이동합니다.

    Cloud Storage 브라우저 페이지로 이동

  2. 버킷 생성을 클릭합니다.
  3. 버킷 생성 대화상자에서 Google Cloud 프로젝트 ID에 문자열 _bucket을 덧붙여 YOUR_PROJECT_ID_bucket과 같은 형식의 버킷 이름을 입력합니다. 이 이름은 버킷 이름 요구사항에 따라 지정해야 합니다. 다른 입력란은 모두 기본값 그대로 둡니다.
  4. 만들기를 클릭합니다.
  5. 버킷을 만든 후 사용자가 볼 수 있도록 객체를 공개적으로 액세스할 수 있게 설정해야 합니다. 객체를 공개적으로 액세스할 수 있도록 설정하려면 데이터 공개 설정을 참조하세요.
  6. 도서 편집을 클릭하고 도서 표지로 업로드할 이미지를 선택합니다. 예를 들어 다음과 같은 공개 도메인 이미지를 사용할 수 있습니다.
    Moby Dick 도서 표지
  7. 저장을 클릭합니다. 홈페이지로 리디렉션되면서 Bookshelf 앱에 입력된 항목을 확인할 수 있습니다.
    Moby Dick Bookshelf 앱 항목

Bookshelf 앱은 Cloud Storage 클라이언트 라이브러리를 사용하여 업로드된 파일을 Cloud Storage로 보냅니다.

public class CloudStorageHelper {

  private final Logger logger = Logger.getLogger(CloudStorageHelper.class.getName());
  private static Storage storage = null;

  static {
    storage = StorageOptions.getDefaultInstance().getService();
  }

  /**
   * Uploads a file to Google Cloud Storage to the bucket specified in the BUCKET_NAME environment
   * variable, appending a timestamp to end of the uploaded filename.
   */
  public String uploadFile(FileItemStream fileStream, final String bucketName)
      throws IOException, ServletException {
    checkFileExtension(fileStream.getName());

    System.out.println("FileStream name: " + fileStream.getName() + "\nBucket name: " + bucketName);

    DateTimeFormatter dtf = DateTimeFormat.forPattern("-YYYY-MM-dd-HHmmssSSS");
    DateTime dt = DateTime.now(DateTimeZone.UTC);
    String dtString = dt.toString(dtf);
    final String fileName = fileStream.getName() + dtString;

    // the inputstream is closed by default, so we don't need to close it here
    @SuppressWarnings("deprecation")
    BlobInfo blobInfo =
        storage.create(
            BlobInfo.newBuilder(bucketName, fileName)
                // Modify access list to allow all users with link to read file
                .setAcl(new ArrayList<>(Arrays.asList(Acl.of(User.ofAllUsers(), Role.READER))))
                .build(),
            fileStream.openStream());
    logger.log(
        Level.INFO, "Uploaded file {0} as {1}", new Object[] {fileStream.getName(), fileName});
    // return the public download link
    return blobInfo.getMediaLink();
  }

  /** Checks that the file extension is supported. */
  private void checkFileExtension(String fileName) throws ServletException {
    if (fileName != null && !fileName.isEmpty() && fileName.contains(".")) {
      String[] allowedExt = {".jpg", ".jpeg", ".png", ".gif"};
      for (String ext : allowedExt) {
        if (fileName.endsWith(ext)) {
          return;
        }
      }
      throw new ServletException("file must be an image");
    }
  }
}

Cloud Storage 사용에 대한 자세한 내용은 안내 가이드 목록을 참조하세요.

Google Cloud 운영 제품군을 사용하여 앱 모니터링

지금까지 앱을 배포하고 도서를 만들어 수정해 봤습니다. 사용자를 위해 이러한 이벤트를 모니터링하려면 애플리케이션 성능 관리를 사용하세요.

Cloud Logging으로 로그 모니터링

  1. Google Cloud에서 로그 탐색기로 이동합니다.

    로그 탐색기로 이동

    앱을 실시간으로 모니터링할 수 있습니다. 앱에 문제가 발생하면 이곳이 가장 먼저 살펴볼 곳 중 하나입니다.

    Stackdriver 로그 뷰어
  2. 리소스 드롭다운 목록에서 Cloud Run 버전, Bookshelf를 선택합니다.

Error Reporting을 사용한 오류 모니터링

  1. Cloud Console에서 Error Reporting 페이지로 이동합니다.
    Error Reporting 페이지로 이동
    Error Reporting은 앱의 오류 및 예외를 강조표시하고 이에 대한 알림을 설정할 수 있도록 해줍니다.
  2. 브라우저에서 앱의 /errors URL로 이동합니다.
    YOUR_CODE_RUN_URL/errors

    그러면 새 테스트 예외가 생성되어 Google Cloud 운영 제품군으로 전송됩니다.

  3. Cloud Console에서 Error Reporting 페이지로 돌아가면 잠시 후에 새 오류가 표시됩니다. 자동 새로고침을 클릭하면 페이지를 별도로 새로고침할 필요가 없습니다.

    Error Reporting의 오류 메시지

정리

이 튜토리얼에서 사용된 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 리소스가 포함된 프로젝트를 삭제하거나 프로젝트를 유지하고 개별 리소스를 삭제하세요.

프로젝트 삭제

  1. Google Cloud 콘솔에서 리소스 관리 페이지로 이동합니다.

    리소스 관리로 이동

  2. 프로젝트 목록에서 삭제할 프로젝트를 선택하고 삭제를 클릭합니다.
  3. 대화상자에서 프로젝트 ID를 입력한 후 종료를 클릭하여 프로젝트를 삭제합니다.

다음 단계