PHP를 사용한 백그라운드 처리

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

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

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

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

  1. 웹 페이지를 방문하여 Firestore에 저장된 이전 번역 목록을 확인합니다.
  2. HTML 양식을 입력하여 텍스트 번역을 요청합니다.
  3. 번역 요청이 Pub/Sub에 게시됩니다.
  4. Cloud Run 앱은 Pub/Sub 메시지를 수신합니다.
  5. Cloud Run 앱은 Cloud Translation을 사용하여 텍스트를 번역합니다.
  6. Cloud Run 앱은 결과를 Firestore에 저장합니다.

이 가이드는 Google Cloud를 사용한 백그라운드 처리에 관심이 있는 모든 사용자를 대상으로 합니다. Pub/Sub, Firestore, App Engine 또는 Cloud Run과 관련된 사전 경험은 필요하지 않습니다. 하지만 모든 코드를 이해하기 위해서는 PHP, 자바스크립트, HTML 관련 경험이 도움이 됩니다.

목표

  • Cloud Run 서비스 이해 및 배포
  • App Engine 앱을 이해하고 배포합니다.
  • 앱을 사용해봅니다.

비용

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

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

이 가이드를 마치면 만든 리소스를 삭제하여 비용이 계속 청구되지 않게 할 수 있습니다. 자세한 내용은 삭제를 참조하세요.

시작하기 전에

  1. Google 계정으로 로그인합니다.

    아직 계정이 없으면 새 계정을 등록하세요.

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

    프로젝트 선택기 페이지로 이동

  3. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다. 프로젝트에 결제가 사용 설정되어 있는지 확인하는 방법을 알아보세요.

  4. Firestore, Cloud Run, Pub/Sub, and Cloud Translation API를 사용 설정합니다.

    API 사용 설정

  5. Google Cloud Console의 Cloud Shell에서 앱을 엽니다.

    Cloud Shell로 이동

    Cloud Shell을 사용하면 브라우저에서 직접 명령줄을 통해 클라우드 리소스에 액세스할 수 있습니다. 브라우저에서 Cloud Shell을 열고 계속을 클릭하여 샘플 코드를 다운로드하고 앱 디렉터리로 변경합니다.

  6. Google Cloud 프로젝트를 사용하도록 Cloud Shell에서 gcloud 도구를 구성합니다.
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

Cloud Run 백엔드 이해

단일 PHP 함수 translateString을 정의하고 이 함수를 호출하여 Pub/Sub 메시지에 응답하도록 Cloud Run 서비스를 구성합니다.

use Google\Cloud\Firestore\FirestoreClient;
use Google\Cloud\Firestore\Transaction;
use Google\Cloud\Translate\TranslateClient;

/**
 * @param array $data {
 *     The PubSub message data containing text and target language.
 *
 *     @type string $text
 *           The full text to translate.
 *     @type string $language
 *           The target language for the translation.
 * }
 */
function translateString(array $data)
{
    if (empty($data['language']) || empty($data['text'])) {
        throw new Exception('Error parsing translation data');
    }

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();

    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];

    $docId = sprintf('%s:%s', $data['language'], base64_encode($data['text']));
    $docRef = $firestore->collection('translations')->document($docId);

    $firestore->runTransaction(
        function (Transaction $transaction) use ($translate, $translation, $docRef) {
            $snapshot = $transaction->snapshot($docRef);
            if ($snapshot->exists()) {
                return; // Do nothing if the document already exists
            }

            $result = $translate->translate($translation['original'], [
                'target' => $translation['lang'],
            ]);
            $transaction->set($docRef, $translation + [
                'translated' => $result['text'],
                'originalLang' => $result['source'],
            ]);
        }
    );

    echo "Done.";
}
  1. 함수는 Firestore 및 번역과 연결하기 위해 종속 항목을 여러 개 가져와야 합니다.

    use Google\Cloud\Firestore\FirestoreClient;
    use Google\Cloud\Firestore\Transaction;
    use Google\Cloud\Translate\TranslateClient;
    
  2. Firestore와 Pub/Sub 클라이언트를 초기화하면 Cloud Run이 시작합니다. 그런 다음 Pub/Sub 메시지 데이터를 파싱하여 번역할 텍스트와 원하는 타겟 언어를 가져옵니다.

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();
    
    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];
  3. Translation API는 문자열을 원하는 언어로 번역하는 데 사용됩니다.

    $result = $translate->translate($translation['original'], [
        'target' => $translation['lang'],
    ]);
  4. 함수는 중복된 번역이 저장되지 않도록 번역 요청의 고유한 이름을 표시합니다. 그런 다음 Firestore 트랜잭션으로 번역하여 동시 실행이 실수로 동일한 번역을 두 번 실행하지 않도록 합니다.

    $docId = sprintf('%s:%s', $data['language'], base64_encode($data['text']));
    $docRef = $firestore->collection('translations')->document($docId);
    
    $firestore->runTransaction(
        function (Transaction $transaction) use ($translate, $translation, $docRef) {
            $snapshot = $transaction->snapshot($docRef);
            if ($snapshot->exists()) {
                return; // Do nothing if the document already exists
            }
    
            $result = $translate->translate($translation['original'], [
                'target' => $translation['lang'],
            ]);
            $transaction->set($docRef, $translation + [
                'translated' => $result['text'],
                'originalLang' => $result['source'],
            ]);
        }
    );

Cloud Run 백엔드 빌드 및 배포

  • backend 디렉터리에 Cloud Run 앱을 빌드합니다.

    gcloud builds submit backend/ \
      --tag gcr.io/PROJECT_ID/background-function
  • 이전 단계의 이미지 태그를 사용하여 Cloud Run 앱을 배포합니다.

    gcloud run deploy background-processing-function --platform managed \
      --image gcr.io/PROJECT_ID/background-function --region REGION

    여기에서 REGIONGoogle Cloud 리전입니다.

  • 배포가 완료되면 명령어 결과에 배포된 앱의 URL이 표시됩니다. 예:

    Service [background-processing-function] revision [default-00002-vav] has been deployed and is serving 100 percent of traffic at https://default-c457u4v2ma-uc.a.run.app

    다음 단계를 위해 이 URL을 복사합니다.

Pub/Sub 구독 설정

Cloud Run 앱은 메시지가 주제 translate에 게시될 때마다 Pub/Sub에서 메시지를 수신합니다.

기본 제공 인증 확인은 Pub/Sub 메시지가 Cloud Run 백엔드를 호출할 수 있는 권한이 있는 서비스 계정의 유효한 인증 토큰을 포함하는지 확인합니다.

다음 단계에서는 Cloud Run 백엔드에 대한 인증된 호출을 실행하기 위한 Pub/Sub 주제, 구독 및 서비스 계정 설정 과정을 단계별로 설명합니다. 서비스 간 인증에서 통합에 대해 자세히 알아보세요.

  1. 새 번역 요청을 게시할 주제 translate를 만듭니다.

    gcloud pubsub topics create translate
    
  2. 프로젝트가 Pub/Sub 인증 토큰을 만들도록 사용 설정합니다.

    gcloud projects add-iam-policy-binding PROJECT_ID \
         --member=serviceAccount:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
         --role=roles/iam.serviceAccountTokenCreator

    여기에서 PROJECT_NUMBER는 Google Cloud 프로젝트 번호로, gcloud projects describe PROJECT_ID | grep projectNumber를 실행하여 확인할 수 있습니다.

  3. 서비스 계정을 만들거나 선택하여 Pub/Sub 구독 ID를 나타냅니다.

    gcloud iam service-accounts create cloud-run-pubsub-invoker \
         --display-name "Cloud Run Pub/Sub Invoker"

    참고: cloud-run-pubsub-invoker를 사용하거나 Google Cloud 프로젝트 내에서 고유한 이름으로 바꿀 수 있습니다.

  4. 호출자 서비스 계정에 background-processing-function 서비스 호출 권한을 부여합니다.

    gcloud run services add-iam-policy-binding background-processing-function \
       --member=serviceAccount:cloud-run-pubsub-invoker@PROJECT_ID.iam.gserviceaccount.com \
       --role=roles/run.invoker  --platform managed --region REGION

    Identity and Access Management 변경사항이 적용되는 데 몇 분 정도 걸릴 수 있습니다. 그동안 서비스 로그에 HTTP 403 오류가 발생할 수 있습니다.

  5. 서비스 계정으로 Pub/Sub 구독을 만듭니다.

    gcloud pubsub subscriptions create run-translate-string --topic translate \
       --push-endpoint=CLOUD_RUN_URL \
       --push-auth-service-account=cloud-run-pubsub-invoker@PROJECT_ID.iam.gserviceaccount.com

    여기에서 CLOUD_RUN_URL백엔드 빌드 및 배포 후 복사한 HTTPS URL입니다.

    --push-account-service-account 플래그는 인증 및 승인을 위한 Pub/Sub 푸시 기능을 활성화합니다.

    Cloud Run 서비스 도메인은 Pub/Sub 구독에 사용할 수 있도록 자동으로 등록됩니다.

앱 이해

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

  • 하나는 웹 요청을 처리하는 PHP HTTP 서버입니다. 이 서버에는 다음 두 가지 엔드포인트가 있습니다.
    • /: 기존 번역을 모두 나열하고 새 번역을 요청하기 위해 사용자가 제출할 수 있는 양식을 표시합니다.
    • /request-translation: 양식 제출은 이 엔드포인트로 전송됩니다. 이 엔드포인트는 비동기적으로 번역되도록 요청을 Pub/Sub에 게시합니다.
  • 다른 하나는 PHP 서버가 기존 번역으로 채우는 HTML 템플릿입니다.

HTTP 서버

  • app 디렉터리에서 Lumen 앱을 설정하고 HTTP 핸들러를 등록하면 index.php가 시작됩니다.

    $app = new Laravel\Lumen\Application(__DIR__);
    $app->router->group([
    ], function ($router) {
        require __DIR__ . '/routes/web.php';
    });
    $app->run();
  • 색인 핸들러(/)는 Firestore에서 모든 기존 번역을 가져오고 템플릿을 목록으로 렌더링합니다.

    /**
     * Homepage listing all requested translations and their results.
     */
    $router->get('/', function (Request $request) use ($projectId) {
        $firestore = new FirestoreClient([
            'projectId' => $projectId,
        ]);
        $translations = $firestore->collection('translations')->documents();
        return view('home', ['translations' => $translations]);
    });
  • /request-translation에 등록된 번역 요청 핸들러는 HTML 양식 제출을 파싱하고, 요청을 검증하고, 메시지를 Pub/Sub에 게시합니다.

    /**
     * Endpoint which publishes a PubSub request for a new translation.
     */
    $router->post('/request-translation', function (Request $request) use ($projectId) {
        $acceptableLanguages = ['de', 'en', 'es', 'fr', 'ja', 'sw'];
        if (!in_array($lang = $request->get('lang'), $acceptableLanguages)) {
            throw new Exception('Unsupported Language: ' . $lang);
        }
        if (!$text = $request->get('v')) {
            throw new Exception('No text to translate');
        }
        $pubsub = new PubSubClient([
            'projectId' => $projectId,
        ]);
        $topic = $pubsub->topic('translate');
        $topic->publish(['data' => json_encode([
            'language' => $lang,
            'text' => $text,
        ])]);
    
        return '';
    });

HTML 템플릿

HTML 템플릿은 이전 번역을 보고 새 번역을 요청할 수 있도록 사용자에게 표시되는 HTML 페이지의 기본 페이지입니다. HTTP 서버가 이 템플릿에 기존 번역 목록을 채웁니다.

  • HTML 템플릿의 <head> 요소에는 페이지의 메타데이터, 스타일 시트, 자바스크립트가 포함됩니다.

    이 페이지는 Material Design Lite(MDL) CSS 및 자바스크립트 애셋을 확인합니다. MDL을 사용하면 머터리얼 디자인 스타일과 느낌을 웹 사이트에 추가할 수 있습니다.

    이 페이지는 JQuery를 사용하여 문서 로드가 완료될 때까지 기다리고 양식 제출 핸들러를 설정합니다. 번역 요청 양식이 제출될 때마다 페이지가 최소 양식 검증을 수행하여 값이 비어 있지 않은지 확인한 후 비동기 요청을 /request-translation 엔드포인트로 전송합니다.

    마지막으로 요청이 성공했는지 또는 오류가 발생했는지를 나타내는 MDL 스낵바가 표시됩니다.

  • 페이지의 HTML 본문은 MDL 레이아웃 및 여러 MDL 구성요소를 사용하여 번역 목록과 추가 번역 요청 양식을 표시합니다.
    <body>
      <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
        <header class="mdl-layout__header">
          <div class="mdl-layout__header-row">
            <!-- Title -->
            <span class="mdl-layout-title">Translate with Background Processing</span>
          </div>
        </header>
        <main class="mdl-layout__content">
          <div class="page-content">
            <div class="mdl-grid">
              <div class="mdl-cell mdl-cell--1-col"></div>
              <div class="mdl-cell mdl-cell--3-col">
                <form id="translate-form" class="translate-form">
                  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="text" id="v" name="v">
                    <label class="mdl-textfield__label" for="v">Text to translate...</label>
                  </div>
                  <select class="mdl-textfield__input lang" name="lang">
                    <option value="de">de</option>
                    <option value="en">en</option>
                    <option value="es">es</option>
                    <option value="fr">fr</option>
                    <option value="ja">ja</option>
                    <option value="sw">sw</option>
                  </select>
                  <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent" type="submit"
                      name="submit">Submit</button>
                </form>
              </div>
              <div class="mdl-cell mdl-cell--8-col">
                <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
                  <thead>
                    <tr>
                      <th class="mdl-data-table__cell--non-numeric"><strong>Original</strong></th>
                      <th class="mdl-data-table__cell--non-numeric"><strong>Translation</strong></th>
                    </tr>
                  </thead>
                  <tbody>
                  <?php foreach ($translations as $translation): ?>
                    <tr>
                      <td class="mdl-data-table__cell--non-numeric">
                        <span class="mdl-chip mdl-color--primary">
                          <span class="mdl-chip__text mdl-color-text--white"><?= $translation['originalLang'] ?></span>
                        </span>
                      <?= $translation['original'] ?>
                      </td>
                      <td class="mdl-data-table__cell--non-numeric">
                        <span class="mdl-chip mdl-color--accent">
                          <span class="mdl-chip__text mdl-color-text--white"><?= $translation['lang'] ?></span>
                        </span>
                        <?= $translation['translated'] ?>
                      </td>
                    </tr>
                  <?php endforeach ?>
                  </tbody>
                </table>
                <br/>
                <button class="mdl-button mdl-js-button mdl-button--raised" type="button" onClick="window.location.reload();">Refresh</button>
              </div>
            </div>
          </div>
          <div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-snackbar mdl-js-snackbar" id="snackbar">
            <div class="mdl-snackbar__text mdl-color-text--black"></div>
            <button type="button" class="mdl-snackbar__action"></button>
          </div>
        </main>
      </div>
    </body>
    </html>
    

Cloud Shell에서 앱 실행

웹 앱을 배포하기 전에 종속 항목을 설치하고 로컬로 실행합니다.

  1. 먼저 Composer로 종속 항목을 설치합니다. PHP용 gRPC 확장 프로그램은 필수이며 Cloud Shell에 사전 설치되어 있습니다.

    composer install -d app
    
  2. 다음으로 PHP 내장 웹 서버를 실행하여 앱을 제공합니다.

    APP_DEBUG=true php -S localhost:8080 -t app
    

    APP_DEBUG=true 플래그는 발생하는 모든 예외를 표시합니다.

  3. Cloud Shell에서 웹 미리보기 를 클릭하고 포트 8080에서 미리보기를 선택합니다. 그러면 실행 중인 앱이 표시된 새 창이 열립니다.

웹 앱 배포

App Engine 표준 환경을 사용하면 많은 양의 데이터를 사용해 작업하고 과도한 부하가 발생하는 애플리케이션을 쉽게 빌드하고 배포하며 안정적으로 실행할 수 있습니다.

이 가이드에서는 App Engine 표준 환경을 사용하여 HTTP 프런트엔드를 배포합니다.

app.yaml은 App Engine 앱을 구성합니다.

runtime: php73

env_variables:
  APP_DEBUG: true
  LOG_CHANNEL: stderr
  APP_STORAGE: /tmp
  • app.yaml 파일과 동일한 디렉터리에서 App Engine 표준 환경에 앱을 배포합니다.
    gcloud app deploy

앱 테스트

Cloud 함수 및 App Engine 앱을 배포한 후 번역 요청을 시도합니다.

  1. 브라우저에서 앱을 보려면 다음 URL을 입력합니다.

    https://PROJECT_ID.REGION_ID.r.appspot.com

    다음을 바꿉니다.

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

  2. 번역 할 텍스트 필드에 번역할 텍스트를 입력합니다(예: Hello, World).
  3. 드롭다운 목록에서 텍스트를 번역할 언어를 선택합니다.
  4. 제출을 클릭합니다.
  5. 페이지를 새로 고침하려면 새로 고침을 클릭합니다. 번역 목록에 새 행이 있습니다. 번역이 표시되지 않으면 몇 초 후 다시 시도합니다. 그래도 번역이 표시되지 않으면 앱 디버깅에 대한 다음 섹션을 참조하세요.

앱 디버깅

App Engine 앱에 연결할 수 없거나 새 번역이 표시되지 않으면 다음을 확인합니다.

  1. gcloud 배포 명령어가 성공적으로 완료되었고 오류를 출력하지 않았는지 확인합니다. 오류가 있으면(예: message=Build failed) 수정하고 Cloud Run 앱을 빌드 및 배포하고 App Engine 앱을 다시 배포해 봅니다.
  2. Google Cloud Console에서 로그 뷰어 페이지로 이동합니다.

    로그 뷰어 페이지로 이동

    1. 최근에 선택한 리소스 드롭다운 목록에서 GAE 애플리케이션을 클릭하고 전체 module_id를 클릭합니다. 앱을 방문했을 때의 요청 목록이 표시됩니다. 요청 목록이 표시되지 않는 경우 드롭다운 목록에서 전체 module_id를 선택했는지 확인합니다. Cloud Console에 오류 메시지가 표시되면 앱 코드가 웹 앱 이해 섹션의 코드와 일치하는지 확인합니다.
    2. 최근에 선택한 리소스 드롭다운 목록에서 Cloud Run 버전을 클릭한 후 모든 로그를 클릭합니다. 배포된 앱의 URL로 전송된 POST 요청이 표시됩니다. 그렇지 않으면 Cloud Run과 App Engine 앱이 동일한 Pub/Sub 주제를 사용 중이고 Pub/Sub 구독이 Cloud Run 엔드포인트로 푸시하는지 확인합니다.

삭제

이 가이드에서 사용한 리소스에 대한 비용이 Google Cloud Platform 계정에 청구되지 않도록 다음을 따릅니다.

Cloud 프로젝트 삭제

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

    리소스 관리 페이지로 이동

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

가이드 리소스 삭제

  1. 이 가이드에서 만든 App Engine 앱을 삭제합니다.

    1. Cloud Console에서 App Engine의 Versions 페이지로 이동합니다.

      Versions 페이지로 이동

    2. 삭제할 기본이 아닌 앱 버전의 체크박스를 선택합니다.
    3. 삭제 를 클릭하여 앱 버전을 삭제합니다.

  2. 이 가이드에서 배포한 Cloud Run 서비스를 삭제합니다.

    gcloud run services delete background-processing-function

    Google Cloud Console에서 Cloud Run 서비스를 삭제할 수도 있습니다.

  3. 이 가이드에서 만든 다른 Google Cloud 리소스를 삭제합니다.

다음 단계