백그라운드 처리

많은 앱은 웹 요청의 컨텍스트 외부에서 백그라운드를 처리해야 합니다. 이 가이드에서는 사용자가 번역할 텍스트를 입력한 후 이전 번역 목록을 표시하는 웹 앱을 만듭니다. 사용자 요청이 차단되지 않도록 번역이 백그라운드 프로세스로 수행됩니다.

다음 다이어그램은 번역 요청 프로세스를 보여줍니다.

아키텍처 다이어그램입니다.

다음은 가이드 앱 작동 방법을 보여주는 이벤트 시퀀스입니다.

  1. 웹 페이지를 방문하여 Firestore에 저장된 이전 번역 목록을 확인합니다.
  2. HTML 양식을 입력하여 텍스트 번역을 요청합니다.
  3. 번역 요청이 Pub/Sub에 게시됩니다.
  4. 해당 Pub/Sub 주제에 구독된 Cloud Run 서비스가 트리거됩니다.
  5. Cloud Run 서비스는 Cloud Translation을 사용하여 텍스트를 번역합니다.
  6. Cloud Run 서비스는 결과를 Firestore에 저장합니다.

이 가이드는 Google Cloud를 사용한 백그라운드 처리에 관심이 있는 모든 사용자를 대상으로 합니다. Pub/Sub, Firestore, Cloud Run과 관련된 사전 경험은 필요하지 않습니다. 하지만 모든 코드를 이해하는 데 자바와 HTML 관련 경험이 유용합니다.

목표

  • Cloud Run 서비스 이해 및 배포
  • 앱을 사용해봅니다.

비용

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

프로젝트 사용량을 기준으로 예상 비용을 산출하려면 가격 계산기를 사용하세요. Google Cloud를 처음 사용하는 사용자는 무료 체험판을 사용할 수 있습니다.

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

시작하기 전에

  1. Google Cloud 계정에 로그인합니다. Google Cloud를 처음 사용하는 경우 계정을 만들고 Google 제품의 실제 성능을 평가해 보세요. 신규 고객에게는 워크로드를 실행, 테스트, 배포하는 데 사용할 수 있는 $300의 무료 크레딧이 제공됩니다.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

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

  4. Enable the Firestore, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  5. Install the Google Cloud CLI.
  6. To initialize the gcloud CLI, run the following command:

    gcloud init
  7. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

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

  9. Enable the Firestore, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  10. Install the Google Cloud CLI.
  11. To initialize the gcloud CLI, run the following command:

    gcloud init
  12. gcloud 구성요소를 업데이트합니다.
    gcloud components update
  13. 개발 환경을 준비합니다.

    자바 설정 가이드로 이동

앱 준비

  1. 터미널 창에서 샘플 앱 저장소를 로컬 머신에 클론합니다.

    git clone https://github.com/GoogleCloudPlatform/getting-started-java.git

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

  2. 백그라운드 처리 샘플 코드가 포함된 디렉터리로 변경합니다.

    cd getting-started-java/background

앱 이해

웹 앱에는 두 가지 주요 구성요소가 있습니다.

  • 하나는 웹 요청을 처리하는 자바 HTTP 서버입니다. 이 서버에는 다음 두 가지 엔드포인트가 있습니다.
    • /translate
      • GET(웹브라우저 사용): 사용자가 제출한 가장 최근에 처리된 번역 요청 10개를 표시합니다.
      • POST(Pub/Sub 구독 포함): Cloud Translation API를 사용하여 번역 요청을 처리하고 결과를 Firestore에 저장합니다.
    • /create: 새로운 번역 요청을 제출하는 양식입니다.
  • 웹 양식으로 제출한 번역 요청을 처리하는 서비스 클라이언트입니다. 다음 세 가지 클라이언트가 함께 작동합니다.
    • Pub/Sub: 사용자가 웹 양식을 제출하면 Pub/Sub 클라이언트는 요청 세부 사항이 포함된 메시지를 게시합니다. 이 가이드에서 생성된 구독은 이 메시지를 번역을 수행하도록 만든 Cloud Run 엔드포인트로 릴레이합니다.
    • Translation: 이 클라이언트는 번역을 수행하여 Pub/Sub 요청을 처리합니다.
    • Firestore: 번역이 완료되면 이 클라이언트는 번역과 함께 요청 데이터를 Firestore에 저장합니다. 이 클라이언트는 기본 /translate 엔드포인트에서 가장 최근 요청을 읽습니다.

Cloud Run 코드 이해

  • Cloud Run 앱은 Firestore, Translation 및 Pub/Sub에 종속됩니다.

    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-firestore</artifactId>
      <version>3.0.16</version>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-translate</artifactId>
      <version>2.1.1</version>
    </dependency>
    
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-pubsub</artifactId>
      <version>1.113.7</version>
    </dependency>
  • 전역 Firestore, Translation, Pub/Sub 클라이언트는 호출 간에 재사용될 수 있도록 초기화됩니다. 이렇게 하면 호출할 때마다 새 클라이언트를 초기화할 필요가 없으므로 실행 시간이 빨라집니다.

    @WebListener("Creates Firestore and TranslateServlet service clients for reuse between requests.")
    public class BackgroundContextListener implements ServletContextListener {
      @Override
      public void contextDestroyed(javax.servlet.ServletContextEvent event) {}
    
      @Override
      public void contextInitialized(ServletContextEvent event) {
        String firestoreProjectId = System.getenv("FIRESTORE_CLOUD_PROJECT");
        Firestore firestore = (Firestore) event.getServletContext().getAttribute("firestore");
        if (firestore == null) {
          firestore =
              FirestoreOptions.getDefaultInstance().toBuilder()
                  .setProjectId(firestoreProjectId)
                  .build()
                  .getService();
          event.getServletContext().setAttribute("firestore", firestore);
        }
    
        Translate translate = (Translate) event.getServletContext().getAttribute("translate");
        if (translate == null) {
          translate = TranslateOptions.getDefaultInstance().getService();
          event.getServletContext().setAttribute("translate", translate);
        }
    
        String topicId = System.getenv("PUBSUB_TOPIC");
        TopicName topicName = TopicName.of(firestoreProjectId, topicId);
        Publisher publisher = (Publisher) event.getServletContext().getAttribute("publisher");
        if (publisher == null) {
          try {
            publisher = Publisher.newBuilder(topicName).build();
            event.getServletContext().setAttribute("publisher", publisher);
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }
  • 색인 핸들러 /는 Firestore에서 모든 기존 번역을 가져오고 HTML 템플릿을 목록으로 채웁니다.

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
      Firestore firestore = (Firestore) this.getServletContext().getAttribute("firestore");
      CollectionReference translations = firestore.collection("translations");
      QuerySnapshot snapshot;
      try {
        snapshot = translations.limit(10).get().get();
      } catch (InterruptedException | ExecutionException e) {
        throw new ServletException("Exception retrieving documents from Firestore.", e);
      }
      List<TranslateMessage> translateMessages = Lists.newArrayList();
      List<QueryDocumentSnapshot> documents = Lists.newArrayList(snapshot.getDocuments());
      documents.sort(Comparator.comparing(DocumentSnapshot::getCreateTime));
    
      for (DocumentSnapshot document : Lists.reverse(documents)) {
        String encoded = gson.toJson(document.getData());
        TranslateMessage message = gson.fromJson(encoded, TranslateMessage.class);
        message.setData(decode(message.getData()));
        translateMessages.add(message);
      }
      req.setAttribute("messages", translateMessages);
      req.setAttribute("page", "list");
      req.getRequestDispatcher("/base.jsp").forward(req, resp);
    }
  • 새 번역은 HTML 양식을 제출하여 요청됩니다. /create에 등록된 번역 요청 핸들러는 양식 제출을 파싱하고, 요청을 검증하고, 메시지를 Pub/Sub에 게시합니다.

    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
      String text = req.getParameter("data");
      String sourceLang = req.getParameter("sourceLang");
      String targetLang = req.getParameter("targetLang");
    
      Enumeration<String> paramNames = req.getParameterNames();
      while (paramNames.hasMoreElements()) {
        String paramName = paramNames.nextElement();
        logger.warning("Param name: " + paramName + " = " + req.getParameter(paramName));
      }
    
      Publisher publisher = (Publisher) getServletContext().getAttribute("publisher");
    
      PubsubMessage pubsubMessage =
          PubsubMessage.newBuilder()
              .setData(ByteString.copyFromUtf8(text))
              .putAttributes("sourceLang", sourceLang)
              .putAttributes("targetLang", targetLang)
              .build();
    
      try {
        publisher.publish(pubsubMessage).get();
      } catch (InterruptedException | ExecutionException e) {
        throw new ServletException("Exception publishing message to topic.", e);
      }
    
      resp.sendRedirect("/");
    }
  • 만든 Pub/Sub 구독은 요청을 Cloud Run 엔드포인트로 전달합니다. Cloud Run 엔드포인트는 Pub/Sub 메시지를 파싱하여 번역할 텍스트와 원하는 대상 언어를 가져옵니다. 그런 다음 Translation API는 문자열을 선택한 언어로 번역합니다.

    String body = req.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
    
    PubSubMessage pubsubMessage = gson.fromJson(body, PubSubMessage.class);
    TranslateMessage message = pubsubMessage.getMessage();
    
    // Use Translate service client to translate the message.
    Translate translate = (Translate) this.getServletContext().getAttribute("translate");
    message.setData(decode(message.getData()));
    Translation translation =
        translate.translate(
            message.getData(),
            Translate.TranslateOption.sourceLanguage(message.getAttributes().getSourceLang()),
            Translate.TranslateOption.targetLanguage(message.getAttributes().getTargetLang()));
  • 앱은 번역 데이터를 Firestore에서 만든 새 문서에 저장합니다.

    // Use Firestore service client to store the translation in Firestore.
    Firestore firestore = (Firestore) this.getServletContext().getAttribute("firestore");
    
    CollectionReference translations = firestore.collection("translations");
    
    ApiFuture<WriteResult> setFuture = translations.document().set(message, SetOptions.merge());
    
    setFuture.get();
    resp.getWriter().write(translation.getTranslatedText());

Cloud Run 앱 배포

  1. Pub/Sub 주제 이름을 선택하고 uuidgen 또는 uuidgenerator.net과 같은 온라인 UUID 생성기를 사용하여 Pub/Sub 확인 토큰을 생성합니다. 이 토큰은 Cloud Run 엔드포인트가 개발자가 만든 Pub/Sub 구독의 요청만 수락하도록 합니다.

    export PUBSUB_TOPIC=background-translate
    export PUBSUB_VERIFICATION_TOKEN=your-verification-token
  2. Pub/Sub 주제를 만듭니다.

     gcloud pubsub topics create $PUBSUB_TOPIC
    
    • pom.xml 파일의 MY_PROJECT를 Cloud 프로젝트 ID로 바꿉니다.
  3. Jib Maven 플러그인을 사용하여 코드 이미지를 GCR(이미지 저장소)에 빌드 및 배포합니다.

     mvn clean package jib:build
    
  4. Cloud Run에 앱을 배포합니다.

    gcloud run deploy background --image gcr.io/MY_PROJECT/background \
          --platform managed --region us-central1 --memory 512M \
          --update-env-vars PUBSUB_TOPIC=$PUBSUB_TOPIC,PUBSUB_VERIFICATION_TOKEN=$PUBSUB_VERIFICATION_TOKEN

    여기서 MY_PROJECT는 개발자가 만든 Cloud 프로젝트의 이름입니다. 이 명령어는 Pub/Sub 구독이 변환 요청을 내보내는 엔드포인트를 출력합니다. Pub/Sub 구독을 만드는 데 필요하므로 브라우저에서 이 엔드포인트를 기록하고 브라우저의 엔드포인트에 방문하여 새 번역을 요청합니다.

앱 테스트

Cloud Run 서비스를 배포한 후에 번역을 요청해 봅니다.

  1. 브라우저에서 앱을 보려면 이전에 만든 Cloud Run 엔드포인트로 이동합니다.

    빈 번역 목록과 새 번역 요청 양식이 포함된 페이지가 있습니다.

  2. + 요청 번역을 클릭하고 요청 양식을 작성한 후 제출을 클릭합니다.

  3. 제출하면 자동으로 /translate 경로로 돌아오지만 새 번역이 아직 나타나지 않을 수 있습니다. 페이지를 새로고치려면 새로고침 을 클릭합니다. 번역 목록에 새 행이 있습니다. 번역이 표시되지 않으면 몇 초 후 다시 시도합니다. 그래도 번역이 표시되지 않으면 앱 디버깅에 대한 다음 섹션을 참조하세요.

앱 디버깅

Cloud Run 서비스에 연결할 수 없거나 새 번역이 표시되지 않으면 다음을 확인합니다.

  • gcloud run deploy 명령어가 성공적으로 완료되었고 오류가 출력되지 않았는지 확인합니다. 오류가 있으면(예: message=Build failed) 오류를 수정하고 다시 실행해 봅니다.

  • 로그에서 오류를 검사합니다.

    1. Google Cloud Console에서 Cloud Run 페이지로 이동합니다.

      Cloud Run 페이지로 이동

    2. 서비스 이름 background를 클릭합니다.

    3. 로그를 클릭합니다.

삭제

프로젝트 삭제

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Cloud Run 서비스를 삭제합니다.

  • 이 가이드에서 만든 Cloud Run 서비스를 삭제합니다.

    gcloud run services delete --region=$region background

다음 단계