Python을 사용한 백그라운드 처리

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

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

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

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

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

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

목표

  • Cloud 함수를 이해하고 배포합니다.
  • App Engine 앱을 이해하고 배포합니다.
  • 앱을 사용해봅니다.

비용

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

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

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

시작하기 전에

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

    프로젝트 선택기로 이동

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

  4. API Firestore, Cloud Functions, Pub/Sub, and Cloud Translation 사용 설정

    API 사용 설정

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

    프로젝트 선택기로 이동

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

  7. API Firestore, Cloud Functions, Pub/Sub, and Cloud Translation 사용 설정

    API 사용 설정

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

    Cloud Shell로 이동

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

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

Cloud 함수 이해

  • 이 함수는 Firestore 및 Translation과 같은 일부 종속 항목을 가져오면서 시작합니다.
    import base64
    import hashlib
    import json
    
    from google.cloud import firestore
    from google.cloud import translate_v2 as translate
  • 함수를 호출할 때 재사용할 수 있도록 전역 Firestore 및 Translation 클라이언트가 초기화됩니다. 이렇게 하면 함수를 호출할 때마다 새 클라이언트를 초기화할 필요가 없으므로 실행 시간이 빨라집니다.
    # Get client objects once to reuse over multiple invocations.
    xlate = translate.Client()
    db = firestore.Client()
  • Translation API는 문자열을 선택된 언어로 번역합니다.
    def translate_string(from_string, to_language):
        """ Translates a string to a specified language.
    
        from_string - the original string before translation
    
        to_language - the language to translate to, as a two-letter code (e.g.,
            'en' for english, 'de' for german)
    
        Returns the translated string and the code for original language
        """
        result = xlate.translate(from_string, target_language=to_language)
        return result['translatedText'], result['detectedSourceLanguage']
  • Cloud 함수는 Pub/Sub 메시지를 파싱하여 번역할 텍스트와 원하는 도착어를 가져옵니다.

    그런 다음 Cloud 함수는 트랜잭션을 사용하여 텍스트를 번역하고 Firestore에 저장하여 중복 번역이 없는지 확인합니다.

    def document_name(message):
        """ Messages are saved in a Firestore database with document IDs generated
            from the original string and destination language. If the exact same
            translation is requested a second time, the result will overwrite the
            prior result.
    
            message - a dictionary with fields named Language and Original, and
                optionally other fields with any names
    
            Returns a unique name that is an allowed Firestore document ID
        """
        key = '{}/{}'.format(message['Language'], message['Original'])
        hashed = hashlib.sha512(key.encode()).digest()
    
        # Note that document IDs should not contain the '/' character
        name = base64.b64encode(hashed, altchars=b'+-').decode('utf-8')
        return name
    
    @firestore.transactional
    def update_database(transaction, message):
        name = document_name(message)
        doc_ref = db.collection('translations').document(document_id=name)
    
        try:
            doc_ref.get(transaction=transaction)
        except firestore.NotFound:
            return  # Don't replace an existing translation
    
        transaction.set(doc_ref, message)
    
    def translate_message(event, context):
        """ Process a pubsub message requesting a translation
        """
        message_data = base64.b64decode(event['data']).decode('utf-8')
        message = json.loads(message_data)
    
        from_string = message['Original']
        to_language = message['Language']
    
        to_string, from_language = translate_string(from_string, to_language)
    
        message['Translated'] = to_string
        message['OriginalLanguage'] = from_language
    
        transaction = db.transaction()
        update_database(transaction, message)

Cloud 함수 배포

  • Cloud Shell의 function 디렉터리에서 Pub/Sub 트리거를 사용하여 Cloud 함수를 배포합니다.

    gcloud functions deploy Translate --runtime=python37 \
    --entry-point=translate_message --trigger-topic=translate \
    --set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_GOOGLE_CLOUD_PROJECT
    

    여기서 YOUR_GOOGLE_CLOUD_PROJECT는 Google Cloud 프로젝트 ID입니다.

앱 이해

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

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

HTTP 서버

  • app 디렉터리에서 main.py는 종속 항목 가져오기, Flask 앱 만들기, Firestore 및 Translation 클라이언트 초기화, 지원되는 언어 목록 정의로 시작됩니다.

    import json
    import os
    
    from flask import Flask, redirect, render_template, request
    from google.cloud import firestore
    from google.cloud import pubsub
    
    app = Flask(__name__)
    
    # Get client objects to reuse over multiple invocations
    db = firestore.Client()
    publisher = pubsub.PublisherClient()
    
    # Keep this list of supported languages up to date
    ACCEPTABLE_LANGUAGES = ("de", "en", "es", "fr", "ja", "sw")
  • 색인 핸들러 /는 Firestore에서 모든 기존 번역을 가져오고 HTML 템플릿을 목록으로 채웁니다.

    @app.route("/", methods=["GET"])
    def index():
        """The home page has a list of prior translations and a form to
        ask for a new translation.
        """
    
        doc_list = []
        docs = db.collection("translations").stream()
        for doc in docs:
            doc_list.append(doc.to_dict())
    
        return render_template("index.html", translations=doc_list)
    
    
  • 새 번역은 HTML 양식을 제출하여 요청됩니다. /request-translation에 등록된 번역 요청 핸들러는 양식 제출을 파싱하고, 요청을 검증하고, 메시지를 Pub/Sub에 게시합니다.

    @app.route("/request-translation", methods=["POST"])
    def translate():
        """Handle a request to translate a string (form field 'v') to a given
        language (form field 'lang'), by sending a PubSub message to a topic.
        """
        source_string = request.form.get("v", "")
        to_language = request.form.get("lang", "")
    
        if source_string == "":
            error_message = "Empty value"
            return error_message, 400
    
        if to_language not in ACCEPTABLE_LANGUAGES:
            error_message = "Unsupported language: {}".format(to_language)
            return error_message, 400
    
        message = {
            "Original": source_string,
            "Language": to_language,
            "Translated": "",
            "OriginalLanguage": "",
        }
    
        topic_name = "projects/{}/topics/{}".format(
            os.getenv("GOOGLE_CLOUD_PROJECT"), "translate"
        )
        publisher.publish(
            topic=topic_name, data=json.dumps(message).encode("utf8")
        )
        return redirect("/")
    
    

HTML 템플릿

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

  • HTML 템플릿의 <head> 요소에는 해당 페이지의 메타데이터, 스타일 시트, 자바스크립트가 포함됩니다.
    <html>
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Translations</title>
    
        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
        <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
        <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script>
            $(document).ready(function() {
                $("#translate-form").submit(function(e) {
                    e.preventDefault();
                    // Get value, make sure it's not empty.
                    if ($("#v").val() == "") {
                        return;
                    }
                    $.ajax({
                        type: "POST",
                        url: "/request-translation",
                        data: $(this).serialize(),
                        success: function(data) {
                            // Show snackbar.
                            console.log(data);
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--red-100");
                            $("#snackbar").addClass("mdl-color--green-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation requested'
                            });
                        },
                        error: function(data) {
                            // Show snackbar.
                            console.log("Error requesting translation");
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--green-100");
                            $("#snackbar").addClass("mdl-color--red-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation request failed'
                            });
                        }
                    });
                });
            });
        </script>
        <style>
            .lang {
                width: 50px;
            }
            .translate-form {
                display: inline;
            }
        </style>
    </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>
                                {% for translation in translations %}
                                    <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['OriginalLanguage'] }} </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['Language'] }} </span>
                                            </span>
                                            {{ translation['Translated'] }}
                                        </td>
                                    </tr>
                                {% endfor %}
                                </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>

웹 앱 배포

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

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

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

runtime: python37
  • 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 배포 명령어가 성공적으로 완료되었고 오류를 출력하지 않았는지 확인합니다. 오류가 있으면 수정하고, Cloud 함수App Engine 앱 배포를 다시 시도합니다.
  2. Google Cloud Console에서 로그 뷰어 페이지로 이동합니다.

    로그 뷰어 페이지로 이동
    1. 최근에 선택한 리소스 드롭다운 목록에서 GAE 애플리케이션을 클릭하고 전체 module_id를 클릭합니다. 앱을 방문했을 때의 요청 목록이 표시됩니다. 요청 목록이 표시되지 않는 경우 드롭다운 목록에서 전체 module_id를 선택했는지 확인합니다. Cloud Console에 오류 메시지가 출력되는 것이 보이면 앱 이해 관련 섹션에 있는 코드와 앱 코드가 일치하는지 확인합니다.
    2. 최근에 선택한 리소스 드롭다운 목록에서 Cloud 함수를 클릭하고 전체 함수 이름을 클릭합니다. 요청된 각 번역마다 나열된 함수가 보입니다. 그렇지 않으면 Cloud 함수 및 App Engine 앱이 동일한 Pub/Sub 주제를 사용 중인지 확인합니다.
      • background/main.py 파일에서 topic_name"translate"인지 확인합니다.
      • Cloud 함수를 배포할 때, --trigger-topic=translate 플래그를 포함합니다.

정리

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

Cloud 프로젝트 삭제

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

    리소스 관리로 이동

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

App Engine 인스턴스 삭제

  1. Google Cloud 콘솔에서 App Engine의 버전 페이지로 이동합니다.

    Versions로 이동

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

Cloud 함수 삭제

  • 이 가이드에서 만든 Cloud 함수를 삭제합니다.
    gcloud functions delete Translate

다음 단계