Python 3용 기존 번들 서비스에 액세스

이 페이지에서는 표준 환경용 Python 3 런타임으로 기존 번들 서비스를 설치하고 사용하는 방법을 설명합니다. 앱은 Python 3용 App Engine 서비스 SDK를 통해 번들 서비스에 액세스해야 합니다.

시작하기 전에

App Engine 서비스 SDK 설치

App Engine 서비스 SDK를 설치하려면 다음 단계를 따르세요.

  1. requirements.txt 파일에 다음 줄을 추가하여 앱에 SDK를 포함합니다.

    appengine-python-standard>=1.0.0
    

    appengine-python-standard 저장소와 PyPI에서 GitHub의 SDK를 찾을 수 있습니다.

  2. 기본 Python 스크립트에 다음 코드를 추가합니다. 이 코드는 API 호출을 사용 설정하는 데 필요한 변수를 설정하는 WSGI 미들웨어를 만듭니다.

    Flask

    from flask import Flask
    from google.appengine.api import wrap_wsgi_app
    
    app = Flask(__name__)
    app.wsgi_app = wrap_wsgi_app(app.wsgi_app)
    

    Django

    from DJANGO_PROJECT_NAME.wsgi import application
    from google.appengine.api import wrap_wsgi_app
    
    app = wrap_wsgi_app(application)
    

    Pyramid

    from pyramid.config import Configurator
    from google.appengine.api import wrap_wsgi_app
    
    config = Configurator()
    # make configuration settings
    app = config.make_wsgi_app()
    app = wrap_wsgi_app(app)
    

    WSGI

    import google.appengine.api
    
    def app(environ, start_response):
        start_response('200 OK', [('Content-Type', 'text/plain')])
        yield b'Hello world!\n'
    
    app = google.appengine.api.wrap_wsgi_app(app)
    
  3. 앱을 배포하기 전에 app.yaml 파일에 다음 줄을 추가합니다.

    app_engine_apis: true
    
  4. 앱을 배포하려면 gcloud app deploy 명령어를 사용합니다.

마이그레이션 고려사항

앱에서 기존 번들 서비스를 사용하는 경우 Python 3 런타임으로 마이그레이션할 때 다음 사항을 고려해야 합니다.

테스트

Python 3 앱에서 기존 번들 서비스 기능을 로컬로 테스트하려면 로컬 개발 서버를 사용합니다. dev_appserver.py 명령어를 실행할 때 Python 3 인터프리터의 경로를 포함하도록 --runtime_python_path 인수를 설정해야 합니다. 예를 들면 다음과 같습니다.

   python3 CLOUD_SDK_ROOT/bin/dev_appserver.py --runtime_python_path=/usr/bin/python3

인수를 [RUNTIME_ID]=[PYTHON_INTERPRETER_PATH] 쌍의 쉼표로 구분된 목록으로 설정할 수도 있습니다. 예를 들면 다음과 같습니다.

   python3 CLOUD_SDK_ROOT/bin/dev_appserver.py --runtime_python_path="python27=/user/bin/python2.7,python3=/usr/bin/python3"

피클 호환성

Memcache, Cloud NDB, deferred 등의 공유 서비스는 pickle 모듈을 사용하여 Python 객체를 직렬화 및 공유합니다. App Engine 환경에서 마이그레이션 중에 일반적으로 사용되는 Python 2와 Python 3을 모두 사용하는 경우, 한 버전의 Python이 작성한 공유 직렬화 객체를 다른 버전에서 재구성할 수 있는지 확인해야 합니다. 가이드에서 교차 버전 피클 호환성 구현에 대한 안내를 확인할 수 있습니다.

기본적으로 Python 3은 Python 2에서 지원되지 않는 피클링 프로토콜을 사용합니다. 이로 인해 앱이 Python 3 환경에서 작성된 Python 객체를 Python 2 환경에서 재구성하려 할 때 오류가 발생할 수 있습니다. 이 문제를 방지하려면 필요에 따라 Python 3 앱의 app.yaml 파일에서 다음 환경 변수를 설정합니다.

  • NDB를 사용하는 앱 등 Memcache를 사용하는 앱의 경우 MEMCACHE_USE_CROSS_COMPATIBLE_PROTOCOL: 'True'를 설정합니다.
  • NDB를 사용하여 Datastore에 연결하는 앱의 경우 NDB_USE_CROSS_COMPATIBLE_PICKLE_PROTOCOL: 'True'를 설정합니다.
  • deferred를 사용하는 앱의 경우 DEFERRED_USE_CROSS_COMPATIBLE_PICKLE_PROTOCOL: 'True'를 설정합니다.

Python 2에서 string 객체는 8비트 바이트 값의 시퀀스를 보유합니다. Python 3에서 string 객체는 일련의 유니코드 문자를 보유합니다. 기본적으로 Python 3 피클은 Python 3 string을 ASCII로 해석하여 Python 2 string을 유니코드로 변환합니다. 그러면 ASCII 문자 범위 0~127을 벗어나는 값에서 오류가 발생할 수 있습니다. Memcache는 이런 기본 매핑의 재정의를 지원합니다.

from google.appengine.api import memcache
import six.moves.cPickle as pickle

def _unpickle_factory(file):
    return pickle.Unpickler(file, encoding='latin1')

memcache.setup_client(memcache.Client(unpickler=_unpickle_factory))

latin1 인코딩은 Python 2 string에서 각 바이트에 대입 가능한 256개 값에 대한 매핑을 정의합니다. 이렇게 하면 디코딩 오류가 방지됩니다. 그러나 Python 2 string이 파일에서 읽은 데이터와 같이 latin1 범위 외부에 있는 실제 유니코드 데이터를 포함하는 경우 c Pickle은 데이터를 올바르게 매핑하지 않습니다. 따라서 피클링한 객체에 대해 string 객체가 아닌 unicode 객체로 유니코드 데이터를 보관하도록 Python 2 코드를 업데이트해야 합니다. 필요한 업데이트에 대한 자세한 내용은 호환성 가이드를 참조하세요.

앞서 설명한 Python 2 코드를 업데이트하여 Python 3와 호환되는 직렬화를 생성하는 방법은 Memcache에 저장된 것과 같은 단기 지속 직렬화에 해당합니다. 마이그레이션 중 Datastore에 저장된 것과 같은 장기 지속 Python 2 직렬화를 업데이트하거나 다시 작성해야 할 수 있습니다. 예를 들어 google.appengine.ext.ndb.model.PickleProperty를 사용하여 작성된 직렬화에는 업그레이드가 필요할 수 있습니다.

제한사항 및 일반적이지 않은 문제에 대한 자세한 내용은 호환성 가이드를 참조하세요.

웹 프레임워크

webapp2는 Python 3에서 번들되거나 지원되지 않으므로 모든 WSGI 호환 프레임워크(예: Flask)를 사용하려면 애플리케이션을 다시 작성해야 합니다.

권장되는 마이그레이션 전략은 Python 2.7을 계속 사용하는 동안 Python 2.7 앱의 webapp2를 Flask(또는 Django, Pyramid, Bottle 또는 web.py와 같은 대체 웹 프레임워크)로 바꿔 사용하는 것입니다. 그런 다음 업데이트된 앱이 안정화되면 코드를 Python 3으로 마이그레이션하고 Python 3용 App Engine을 사용하여 배포하고 테스트합니다.

webapp2를 사용하는 Python 2.7 앱을 Flask 프레임워크를 사용하도록 변환하는 방법의 예시는 추가 리소스를 참조하세요.

핸들러 사용

Python 3 앱에는 하나의 스크립트만 연결할 수 있으므로 app.yaml에서 여러 스크립트에 script URL을 매핑하는 여러 개의 핸들러가 있는 경우 이러한 스크립트는 URL 라우팅을 처리하는 스크립트 하나에 결합되어야 합니다.

다음 예는 각 런타임에서 app.yaml 파일의 핸들러 차이점을 보여줍니다.

Python 2

runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /
  script: home.app

- url: /index\.html
  script: home.app

- url: /stylesheets
  static_dir: stylesheets

- url: /(.*\.(gif|png|jpg))$
  static_files: static/\1
  upload: static/.*\.(gif|png|jpg)$

- url: /admin/.*
  script: admin.app
  login: admin

- url: /.*
  script: not_found.app

Python 3

runtime: python312
app_engine_apis: true

handlers:
- url: /stylesheets
  static_dir: stylesheets

- url: /(.*\.(gif|png|jpg))$
  static_files: static/\1
  upload: static/.*\.(gif|png|jpg)$

- url: /admin/.*
  script: auto
  login: admin

Python 3 앱은 URL 라우팅(예: Flask 데코레이터 사용)을 처리해야 합니다.

URL 패턴이 서로 다른 여러 script 핸들러를 사용하거나 핸들러에서 서로 다른 속성을 사용하려면 각 핸들러에서 script: auto를 지정해야 합니다.

app.yaml 파일에서 entrypoint 필드를 지정하여 기본 시작 동작을 재정의할 수도 있습니다.

특정 핸들러를 사용하는 방법에 대한 자세한 내용은 Blobstore, Deferred, Mail 개요를 참조하세요.

스레드 안전

앱은 스레드 안전성을 갖는 것으로 간주됩니다. API 호출은 요청 스레드에서 실행해야 합니다. 앱을 시작할 때 기존 번들 서비스 API를 사용하면 보안 오류가 발생할 수 있습니다.

자세한 내용은 Python용 기존 번들 서비스를 사용할 때 발생하는 보안 오류를 참조하세요.

URL 가져오기 사용

Python용 URL Fetch를 사용하려면 URL Fetch 라이브러리를 명시적으로 호출해야 합니다.

Python 3 앱에서 URL Fetch API를 사용하는 경우 앱이 다른 App Engine 앱에 요청을 전송하면 X-Appengine-Inbound-Appid 요청 헤더가 추가됩니다. 이렇게 하면 수신 앱이 호출 앱의 ID를 확인할 수 있습니다. 자세한 내용은 아웃바운드 요청 마이그레이션을 참조하세요.

예시(App Engine ndb)

다음은 페이지 방문을 등록하는 기본 Python 2 앱이며, App Engine ndb를 사용하여 Datastore에 액세스합니다. 두 번째 예시는 Python 3에 상응하는 앱입니다. 여기서 webapp2 사용량은 Flask로 대체되었고, 위에서 설명된 Python 3의 번들 서비스에 액세스하는 데 필요한 변경사항이 구현되었습니다.

Python 2(webapp2)

import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

class MainHandler(webapp2.RequestHandler):
    'main application (GET) handler'
    def get(self):
        store_visit(self.request.remote_addr, self.request.user_agent)
        visits = fetch_visits(10)
        tmpl = os.path.join(os.path.dirname(__file__), 'index.html')
        self.response.out.write(template.render(tmpl, {'visits': visits}))

app = webapp2.WSGIApplication([
    ('/', MainHandler),
], debug=True)

Python 3(Flask)

from flask import Flask, render_template, request
from google.appengine.api import wrap_wsgi_app
from google.appengine.ext import ndb

app = Flask(__name__)
app.wsgi_app = wrap_wsgi_app(app.wsgi_app)


class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)


@app.route('/')
def root():
    'main application (GET) handler'
    store_visit(request.remote_addr, request.user_agent)
    visits = fetch_visits(10)
    return render_template('index.html', visits=visits)

두 앱 모두 Python App Engine 마이그레이션 콘텐츠(코드 샘플, 동영상, Codelab)에 대한 오픈소스 저장소에서 찾을 수 있으며, 구체적으로는 각각 mod0 폴더 및 mod1b 폴더에 있습니다.