Source

JythonBook_KO / JythonDjango.rst

14장. 장고 웹 애플리케이션

장고(Django)는 파이썬 세계에서 웹 개발의 정의를 새롭게 내린 현대적인 파이썬 웹 프레임워크이다. 풀-스택 접근, 실용적인 디자인, 그리고 최고의 문서화가 그 주요한 성공 요인이다. 파이썬 언어로 빠르게 웹 개발하는 것이 그럴 듯하게 들린다면, (기업 웹 분야에서 강력한 입지를 구축한) 자바와 통합된 파이썬 언어로 빠르게 웹 개발을 하는 것은 더욱 좋을 것이다. 자이썬에서 장고를 운영함으로써 그것이 가능하다. 자바 세계에 있어서도, 기존의 자바 API와 기술을 그대로 사용하면서 웹 애플리케이션을 재빨리 구축할 수 있는 장고는 대단히 매력적이다. 이 장에서는 몇단계만으로 자이썬에서 장고를 실행시키는 간단한 소개를 하려한다. 그 다음에 간단한 웹 애플리케이션을 구축하여 프레임워크에 대한 감을 잡을 수 있도록 할 것이다. 후반부에서는, 장고 웹 애플리케이션과 JavaEE의 통합으로 얻을 수 있는 여러 장점에 대하여 살펴보도록 하겠다.

장고 구하기

엄밀히 말하자면, 자이썬에서 장고를 사용하기 위해서 필요한 것은 장고 그 자체 밖에 없다. 하지만 데이터베이스에 접속하려면 서드-파티 라이브러리가 필요하다. 장고에 내장된 데이터베이스 뒷단은 C 언어로 쓰여진 라이브러리를 사용하는데, 자이썬에는 그것을 쓸 수 없기 때문이다. 실제로는 Django와 django-jython, 최소 두개의 꾸러미가 필요하다. 이름에서 연상할 수 있듯이, django-jython은 자이썬에서 장고를 실행할 때 유용한 부가기능의 모음이다. 특히, 거기에는 데이터베이스 뒷단이 포함되어 있다. 이 두가지 라이브러리를 가져오는 것은 플랫폼에 따라 조금 다르고, 수동으로 처리하는 지루한 작업이기 때문에, 이러한 라이브러리를 자동으로 받아서 설치해주는 유틸리티를 사용하도록 하겠다. 이 유틸리티는 setuptools라고 한다. 그 댓가로 setuptools를 수동으로 설치해야하지만, 이것은 매우 간단하다. setuptools를 설치해본 적이 없다면, 부록 A에서 자세한 내용을 살펴보기 바란다. setuptools를 설치한 다음에는 easy_install 명령을 사용할 수 있다. 윈도우 플랫폼에서는 그냥 easy_install이 아니라 easy_install.py를 쳐야할 수도 있다. 준비가 되었으면 장고를 설치해보도록 하자.

$ easy_install Django

Note

위에서는 자이썬의 bin 디렉토리가 PATH 환경변수에 잡혀있다고 가정하고 있다. 그렇지 않다면, jython 또는 easy_install과 같은 명령을 칠 때 그 경로까지 명시적으로 지정해주어야한다. (즉, 그냥 easy_install이라고 치는 것이 아니라 /path/to/jython/bin/easy_install이라고 쳐야한다는 말이다.)

easy_install이 출력하는 메시지를 통해서, 올바른 꾸러미를 찾고, 다운로드하고, 설치하는 작업이 잘 되어가고 있는지 확인할 수 있다. 예제 14-1을 보자.

예제 14-1. easy_install 명령의 출력

Searching for Django
Reading http://pypi.python.org/simple/Django/
Reading http://www.djangoproject.com/
Reading http://www.djangoproject.com/download/1.1.1/tarball/
Best match: Django 1.1.1
Downloading http://media.djangoproject.com/releases/1.1.1/Django-1.1.1.tar.gz
Processing Django-1.1.1.tar.gz
Running Django-1.1.1/setup.py -q bdist_egg --dist-dir
/tmp/easy_install-nTnmlU/Django-1.1.1/egg-dist-tmp-L-pq4s
zip_safe flag not set; analyzing archive contents...
Unable to analyze compiled code on this platform.
Please ask the author to include a 'zip_safe' setting (either True or False)
in the package's setup.py
Adding Django 1.1.1 to easy-install.pth file
Installing django-admin.py script to /home/lsoto/jython2.5.0/bin

Installed /home/lsoto/jython2.5.0/Lib/site-packages/Django-1.1.1-py2.5.egg
Processing dependencies for Django==1.1.1
Finished processing dependencies for Django==1.1.1

이제 장고-자이썬을 설치하자.

$ easy_install django-jython

마찬가지로, 앞에서 본 것과 비슷한 출력을 볼 수 있을 것이다. 여기까지 완료했으면, 준비를 마친 것이다. 무대 뒤에서 무슨 일이 벌어진 것인지 궁금하다면 자이썬이 설치된 곳의 하위 디렉토리인 Lib/site-packages에서 확인할 수 있다. 이러한 항목들은 또한 기본적으로 sys.path의 일부를 만드는 easy-install.pth에도 등재되어있다. 모든 것이 잘 되었는지 확실히 해두고 싶다면, 자이썬을 시작시켜서 예제 14-2의, 장고와 장고-자이썬의 최상위 꾸러미를 가져오는 구문을 시험해보라.

예제 14-2.장고 꾸러미 가져오기

>>> import django
>>> import doj

화면에 에러가 보이지 않는다면 아무 문제가 없는 것이다. 그러면 첫번째 애플리케이션을 만들어보자.

장고 둘러보기

장고는 풀-스택 프레임워크이다. 이 말은, 통신에서 데이터베이스까지, 그리고 URL 처리에서 웹 페이지 템플릿 작업까지의 모든 것을 망라한 기능을 갖추고 있다는 의미이다. 알다시피, 장고만을 주제로 하는 책도 있다. 우리는 깊이 들어가지는 않더라도 프레임워크의 여러가지 기능을 건드려볼 것이므로, 이전에 장고를 사용해본 적이 없다면 그것의 강력함을 느껴볼 수 있을 것이다. 그리하여 어떨 때 장고를 사용하는 것이 적당한지 알게 될 것이다.

Note

장고에 대해 이미 잘 알고 있다면, 더 이상 새로운 내용은 나오지 않는다. J2EE 배포 및 통합 단락으로 건너뛰어서, 자이썬에서 장고를 사용할 때 무엇이 정말로 특별한지 살펴보아도 좋다.

장고와 같이 다양한 기능을 제공하는 프레임워크에 대한 폭넓은 시야를 갖기 위한 유일한 방법은, 아주 간단한 것부터 만들어보고 프레임워크에서 제공하는 기능을 점차로 추가해나가는 것이다. 프레임워크에서 제공하는 기능들로 확장해나가기에 적합한, 공식 장고 튜토리얼의 간단한 설문조사 사이트에서 시작하려고 한다. 달리 말하자면, 이 장에서 볼 수 있는 대부분의 코드는 장고 튜토리얼 http://docs.djangoproject.com/en/1.0/intro/tutorial01/에서 나온 것이다. 그외에 장고의 기능을 더 추가하고, 이 장에 맞는 것들을 첨가하였다. 이제, 앞의 단란에서 언급했던 데이터베이스와의 통신을 다루어보도록 하겠다. 현시점에서 장고/자이썬을 위한 가장 강력한 뒷단은, PostgreSQL 뒷단이다. 그러므로 PostgreSQL을 여러분의 컴퓨터에 설치하고 사용자와 빈 데이터베이스를 설정하기를 권한다.

프로젝트 시작

일반적으로 웹사이트(또는 큰 포탈의 하위 사이트)를 만들기 위한 장고 프로젝트는 설정 파일, URL 매핑 파일, 그리고 웹사이트의 실제적인 기능을 제공하는 앱들로 구성된다. 알고 있겠지만, 많은 웹사이트들이 관리자 인터페이스, 사용자 인증 및 등록, 댓글 시스템, 뉴스 피드, 연락 양식 등 여러 기능을 제공하고 있다. 그것이 장고가 앱 개념상 실제적인 사이트의 기능을 분리하는 이유이다. 앱들은 서로 다른 프로젝트(사이트) 간에서 재사용가능하여야 한다.

우리는 작은 것부터 시작할 것이므로, 우리의 프로젝트는 처음에는 단 하나의 앱으로 구성해보겠다. 프로젝트의 이름은 pollsite로 하겠다. 그러면, 이 장에서 구축할 것들을 갖다놓고 실행시킬 수 있도록 새로운 디렉토리를 하나 만들어보자.

$ django-admin.py startproject pollsite

이렇게 하면 아까 생성했던 디렉토리 아래에 pollsite라는 이름의 꾸러미가 만들어진다. 여기서 우리의 따끈따끈한 새 프로젝트의 기본 설정에서 변경시켜야할 중요한 부분은, 장고가 데이터베이스와 이야기할 수 있도록 정보를 적어주는 것이다. 그러면, pollsite/settings.py 파일을 텍스트 편집기로 열고, DATABASE로 시작하는 줄을 예제 14-3과 같이 변경시켜보자.

예제 14-3. 장고 데이터베이스 설정

DATABASE_ENGINE = 'doj.backends.zxjdbc.postgresql'
DATABASE_NAME = '<여러분이 만든 비어있는 데이터베이스>'
DATABASE_USER = '<데이터베이스에 읽기/쓰기 접근 가능한 사용자 이름>'
DATABASE_PASSWORD = '<사용자의 패스워드>'

이렇게 하여, 장고에게 doj 꾸러미(앞의 장고 구하기에서 말한 장고-자이썬 프로젝트 꾸러미)에서 제공하는 PostgreSQL 드라이버를 사용하고, 주어진 사용자 정보로 접속하도록 알려준다. 이 뒷단은 http://jdbc.postgresql.org/download.html에서 다운로드할 수있는 PostgreSQL JDBC 드라이버가 필요하다.

JDBC 드라이버를 내려받은 다음에는, 자바 CLASSPATH에 그것을 추가할 필요가 있다. 리눅스, 유닉스, MacOS X에서 할 수 있는 또 다른 방법은 다음과 같다.

$ export CLASSPATH=$CLASSPATH:/path/to/postgresql-jdbc.jar

윈도우에서는 다음과 같은 명령을 실행한다.

$ set CLASSPATH=%CLASSPATH%:\path\to\postgresql-jdbc.jar

그렇게 한 다음에는, 우리의 프로젝트의 핵심이 될 앱을 생성한다. 이것은 반드시 pollsite 디렉토리에서 실행해야 한다.

$ jython manage.py startapp polls

이렇게 하면 장고 앱의 기본 구조가 만들어진다. 장고에서의 앱은 웹사이트의 일부 기능을 담당하는 한 조각이라 할 수 있다. 예를 들어, 장고에는 시스템 내의 모든 개체에 대하여 댓글을 달 수 있도록 해주는 comments 앱이 있다. 또한 웹 사이트 데이터베이스에 관리자를 위한 앞단을 제공하는 admin 앱이 있다. 이 두가지 앱을 다룰 것이기는 하지만, 지금으로서는 장고 프로젝트(웹사이트)가 각기 주요 기능을 담당하는 여러가지 장고 앱으로 구성된다고 이해하면 된다. 우리는 사이트 내에서 기본적인 설문조사를 처리할 수 있는 polls 앱으로부터 시작할 것이다. polls 앱은 프로젝트 꾸러미 내에 생성되었으므로, 우리는 pollsite 프로젝트와 pollsite.polls 앱을 갖고 있다. 장고 앱 안에는 무엇이 있는지 들여다보자.

모델

장고에서는 자료 스키마를 파이썬 코드로 작성하며, 파이썬 클래스들을 사용한다. 이 중앙집중식 스키마는 데이터베이스 스키마를 생성하기 위한 SQL 구문을 생성하는데 사용되며, 또한 이러한 특수 파이썬 클래스의 개체를 조작할 때 SQL을 동적으로 생성하는데 사용된다. 자, 장고에서는 전체 프로젝트의 스키마를 단일한 중앙의 지점에서 정의하지 않는다. 기능을 실제로 제공하는 것은 각각의 앱들이기 때문에, 전체 프로젝트의 스키마가 각 앱의 스키마의 조합에 비하여 나을 것이 없다. 그건 그렇고, 데이터 스키마는 장고에서 쓰이는 용어인 모델로 바꿔부르도록 하자(사실 모델과 스키마에는 차이가 있지만 여기서 그리 중요하지는 않다). pollsite/polls 디렉토리를 보게되면, models.py 파일이 보일 것이다. 여기에 앱의 모델이 정의되어있다. 예제 14-4는 간단한 설문조사(poll)에 대한 모델을 갖고 있으며, 각각의 설문조사는 여러 선택(choice)을 갖는다.

예제 14-4. 설문조사를 위한 간단한 모델 코드

from django.db import models

class Poll(models.Model):
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __unicode__(self):
        return self.question

class Choice(models.Model):
    poll = models.ForeignKey(Poll)
    choice = models.CharField(max_length=200)
    votes = models.IntegerField()

    def __unicode__(self):
        return self.choice

보는 것과 같이 models.Model로부터 상속한 클래스와 데이터베이스의 테이블 사이의 대응은 명확하며, 장고의 각 필드가 어떻게 SQL 필드로 변환되는지가 잘 드러난다. pub_date 필드는 date published라는 설명이 있어서 사람이 이해하기에 좋은데, 이와 같이 장고 필드는 SQL 필드보다도 더 많은 정보를 담을 수 있다. 장고는 또한 EmailField, URLField 및 FileField와 같이 오늘날의 웹 애플리케이션에서 공통적으로 쓰이는 특화된 필드들도 제공한다. 이런 것들 덕분에 여러분은 데이터 필드에 대한 유효성 검증이나 저장공간 관리를 위한 코드를 반복해서 작성하는 일을 떠맡지 않아도 된다. 일단 모델을 정의하였으면, 자료를 담을 테이블을 데이터베이스에 만들 차례이다. 첫째, 프로젝트 설정 파일에 앱을 추가한다(프로젝트에 앱만 만들었다고 다가 아니다). pollsite/settings.py 파일을 열고 INSTALLED_APPS 목록에 'pollsite.polls'를 추가한다.

예제 14-5. INSTALLED_APPS 목록에 행을 추가

INSTALLED_APPS = (
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.sites',
   'pollsite.polls',
)

Note

보다시피, 프로젝트에는 이미 두 개의 앱이 있다. 이러한 앱은 세션과 같은 프레임 워크의 기본적인 기능의 일부를 제공하고, 기본적으로 모든 Django 프로젝트에 포함되어 있다.

그 후에, 프로젝트 디렉토리에 위치하고 있는지 확인하고 나서, 다음의 명령을 실행한다.

$ jython manage.py syncdb

데이터베이스 연결 정보가 올바로 지정되어 있으면, 장고는 우리의 모델과, INSTALLED_APPS에 기본적으로 포함되어 있던 모델에 대하여 테이블과 색인을 생성해준다.이러한 앱 가운데 하나는 사용자 인증을 처리해주는 django.contrib.auth이다. 여러분의 사이트에 관리자로 처음 접속할 때 사용자와 암호를 물어보는 것이 바로 이것 때문이다. 예제 14-6을 보자.

예제 14-6. 장고 인증 데이터

Creating table auth_permission
Creating table auth_group
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
Creating table polls_poll
Creating table polls_choice

You just installed Django's auth system, which means you don't have any
superusers defined.
Would you like to create one now? (yes/no):

질문에 yes로 답하면 예제 14-7과 같이 요청한 정보를 얻을 수 있다.

예제 14-7. 인증 질문에 답변

Username (Leave blank to use u'lsoto'): admin
E-mail address: admin@mailinator.com
Warning: Problem with getpass. Passwords may be echoed.
Password: admin
Warning: Problem with getpass. Passwords may be echoed.
Password (again): admin
Superuser created successfully.

이어서, 장고는 모델이 RDBMS의 구조에 대응하도록, 테이블에 대한 색인을 생성하는 일을 계속한다. 예제 14-8을 보자.

예제 14-8. 장고에서 자동으로 색인 생성

Installing index for auth.Permission model
Installing index for auth.Message model
Installing index for polls.Choice model

장고가 무대 뒤에서 어떤 일을 하는지 궁금하다면, sqlall 관리 명령(앞에서 사용한 syncdb와 같이, manage.py를 호출할 때 인식되는 명령)을 사용하여 요청할 수 있다. 이 명령은 앱의 라벨을 인자로 받아, 그 모델에 관련된 SQL 문장들을 출력한다. 그런데, 라벨에 대한 강조는 의도적인 것인데, 그것은 전체 이름이 아니라 전체 이름 중 마지막 부분에 관련된다. 우리의 경우, pollsite.polls의 라벨은 단순히 polls이다. 자, 다음과 같이 실행한다.

$ jython manage.py sqlall polls

그러면 예제 14-9와 같이 출력된다.

예제 14-9. manage.py 출력

BEGIN;
CREATE TABLE "polls_poll" (
    "id" serial NOT NULL PRIMARY KEY,
    "question" varchar(200) NOT NULL,
    "pub_date" timestamp with time zone NOT NULL
)
;
CREATE TABLE "polls_choice" (
    "id" serial NOT NULL PRIMARY KEY,
    "poll_id" integer NOT NULL
        REFERENCES "polls_poll" ("id") DEFERRABLE INITIALLY DEFERRED,
    "choice" varchar(200) NOT NULL,
    "votes" integer NOT NULL
)
;
CREATE INDEX "polls_choice_poll_id" ON "polls_choice" ("poll_id");
COMMIT;

여기서 두 가지 주의할 점이 있다. 첫째, 각각의 테이블은 id 필드를 갖고 있는데, 우리가 모델을 작성할 때 이를 명시적으로 지정하지는 않았다. 이것은 자동으로 적당한 기본값이 정해진다(기본 키를 반드시 다른 유형으로 지정하고 싶다면 그렇게 할 수도 있지만, 이 책에서는 다루지 않는다). 둘째, 우리가 사용하는 특정 RDBMS(여기서는 PostgreSQL)에 맞는 SQL 명령이 어떻게 만들어졌는지 볼 수 있는데, 다른 데이터베이스 뒷단을 사용할 경우 당연히 바뀔 수 있는 부분이다.

다음으로 넘어가자. 우리는 모델을 정의하였고, 설문조사를 저장할 준비가 되었다. 여기서 다음 순서로는 설문조사에 대한 생성, 편집, 삭제 등을 할 수 있는 CRUD 관리 화면을 만드는 것이 보통일 것이다. 아, 그리고 설문조사가 너무 많아져서 관리하기 힘들어질 때를 대비하여, 관리자를 위한 검색과 필터 기능을 생각해볼 수도 있다.

하지만, 우리는 관리 인터페이스를 밑바닥부터 작성하지는 않을 것이다. 장고의 가장 유용한 기능 중 하나인 관리자 앱을 갖다 쓸 것이다.

관리자

장고 프로젝트의 주요 구조(즉, 모델, 뷰, 템플릿)에 대한 설명에서 약간 벗어나서 휴식시간을 갖도록 하자. 위에서 언급한 관리자 인터페이스를 만드는 데에는 채 스무 줄의 코드도 필요하지 않다!

첫째, admin 앱을 활성화하자. pollsite/settings.py를 열어서 INSTALLED_APPS에 'django.contrib.admin'을 추가한다. 다음으로는 아래의 예제 14-10와 같이 pollsite/urls.py를 편집한다.

예제 14-10. 변경되지 않은 원래의 urls.py

from django.conf.urls.defaults import *

# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()

urlpatterns = patterns('',
    # Example:
    # (r'^pollsite/', include('pollsite.foo.urls')),

    # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
    # to INSTALLED_APPS to enable admin documentation:
    # (r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    # (r'^admin/(.*)', admin.site.root),
)

admin을 활성화시키는 행의 주석을 제거하면 예제 14-11과 같이 보일 것이다(admin/doc 행은 손대지 않는다).

예제 14-11. urls.py에서 admin 앱을 활성화

from django.conf.urls.defaults import *

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Example:
    # (r'^pollsite/', include('pollsite.foo.urls')),

    # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
    # to INSTALLED_APPS to enable admin documentation:
    # (r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    (r'^admin/(.*)', admin.site.root),
)

이제 남아있는 주석을 모두 제거하면 urls.py는 예제 14-12와 같이 보일 것이다.

예제 14-12. urls.py의 최종 상태

from django.conf.urls.defaults import *

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
)

아직 이 urls.py 파일에 대하여 설명하지 않았는데, 다음 절에서 좀 더 깊이 들어가도록 하겠다.

끝으로, admin 앱에서 필요한 물리적 데이터베이스를 생성한다.

$ jython manage.py syncdb

이제 admin이 어떻게 생겼는지 눈으로 확인해볼 수 있을 것이다. 우리의 사이트를 개발 모드로 실행시켜 보도록 하자.

$ jython manage.py runserver

Note

개발 웹 서버를 사용하면 웹 프로젝트를 손쉽게 테스트할 수 있다. 강제 종료(Ctrl + C를 누른다)하기 전까지 무한히 수행되며, 서버에 이미 적재한 소스 파일을 변경하면 스스로 재적재하므로 거의 즉각으로 피드백을 얻을 수 있다. 그러나 이 개발 서버는 여러 개의 동시 연결을 처리할 수 없기 때문에 일반적으로 성능이 떨어지므로, 운영 환경에서는 사용하지 않도록 한다.

웹 브라우저를 사용하여 http://localhost:8000/admin/으로 이동한다. 로그인 화면이 나타날 것이다. 앞에서 syncdb를 처음 실행하였을 때 만든 사용자 자격증명을 입력하기 바란다. 로그인하면 그림 14-1과 같은 페이지를 볼 수 있을 것이다.

images/chapter14-tour-admin.png

그림 14-1. 장고 admin

그림과 같이, admin의 중심 부분에는 각각 Auth와 Sites라는 제목이 붙은 상자들이 있다. 그것들은 각각 장고에서 만들어진 auth 앱과 sites 앱에 해당한다. Auth 상자에는 Groups와 Users라는 두 개의 항목이 있는데, 각각은 auth 앱에 포함된 모델에 해당한다. Users 링크를 클릭하면 사용자를 추가, 수정, 삭제하는 전형적인 화면을 볼 수 있다. 이것은 다른 장고 앱에서도 똑같이 제공하는 관리자 앱 인터페이스로서, 우리는 여기에 polls 앱을 추가할 것이다. 앱이 있는 곳에 admin.py 파일을 만들면 된다(pollsite/polls/admin.py). 그런 다음 admin 앱에, 우리의 모델을 어떻게 표현할 것인지 선언한다. polls를 관리하기 위해, 예제 14-13이 그러한 일을 해줄 것이다.

예제 14-13. admin 앱에 polls 앱 추가

# polls admin.py
from pollsite.polls.models import Poll, Choice
from django.contrib import admin

class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3

class PollAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question']}),
        ('Date information', {'fields': ['pub_date'],
                              'classes': ['collapse']}),
    ]
    inlines = [ChoiceInline]

admin.site.register(Poll, PollAdmin)

이런 것이 어떻게 가능한지 의아할 수도 있겠지만, 장고를 통해 무엇이 가능한지 보여주기 위해 빠르게 훑어내려가고 있음을 기억하기 바란다. 이 코드를 작성함으로써 무엇을 얻을 수 있는지부터 살펴보자. 개발 서버를 시작하고, http://localhost:8000/admin/으로 가서, Polls가 어떻게 보이는지 확인해보자. Polls 항목의 Add 링크를 클릭하면, 그림 14-2와 같은 페이지가 표시된다.

images/chapter14-tour-addpoll.png

그림 14-2. 설문을 추가

인터페이스를 조금 사용해보자. 두 개의 설문조사를 생성하고, 하나를 삭제하고, 나머지 하나는 수정하자. 사용자 인터페이스는 세 부분으로 나뉘는데, 하나는 질문, 다른 하나는 날짜(처음에는 숨겨져있다), 마지막으로는 선택을 위한 부분이다. 처음 두 부분은 PollAdmin 클래스의 필드셋으로 정의되었는데, 각 부분의 제목을 정의하고(None은 제목이 없음을 뜻한다), 포함된 필드(물론 한 개 이상이 될 수 있다)와, 'collapse'와 같은 동작을 제공하는 추가적인 CSS 클래스이다.

하나의 사용자 인터페이스 내에 Poll과 Choice 두 모델에 대하여 관리를 통합하였는데, 이는 설문조사의 보기를 인라인(inline)으로 편집할 수 있어야하기 때문이다. 이는 ChoiceInline 클래스를 통하여 구현하였는데, 어떤 모델이 인라인으로 구현되며 몇 개의 빈 슬롯이 보여져야할지 선언하는 모델이다. inline은 나중에 PollAdmin에 포함된다(이는 많은 인라인을 어떠한 ModelAdmin 클래스에나 포함할 수 있기 때문이다).

끝으로, PollAdmin은 admin.site.register()를 통해 Poll 모델의 관리 인터페이스로 등록된다. 살펴본 바와 같이, 모든 것은 선언해주기만 하면 마법처럼 동작한다.

몇 단락 앞에서 언급했던 찾기/필터 기능이 어떻게 된 것인지 궁금할지 모르겠다. 그것들은 주 인터페이스에서 Poll의 Change 링크를 클릭하여(혹은 Polls 링크를 클릭하거나 Poll을 추가한 이후에) 접근할 수 있는 설문조사 인터페이스에서 구현하도록 하겠다.

그럼, PollAdmin 클래스에 다음과 같은 내용을 추가한다.

search_fields = ['question']
list_filter = ['pub_date']

또다시 관리자로 작업한다(그래서 마지막단계에 설문조사를 만드는 것이 좋다). 그림 14-3은 검색어로 django를 사용하여 검색을 하는 모습이다.

images/chapter14-tour-adminsearch.png

그림 14-3. 장고 관리자 화면에서 검색

발행일로 필터를 하려고 하면, 약간 이상하게 느껴질 수도 있지만, 설문조사의 목록에는 단지 설문조사의 이름만 보이기 때문에, 필터가 제대로 동작하는지 확인하려고 해도 걸러진 설문조사의 발행일을 볼 수가 없다. 그것은 다음의 행을 PollAdmin 클래스에 추가하여 간단히 해결할 수 있다.

list_display = ['question', 'pub_date']

추가한 후의 인터페이스는 그림 14-4와 같이 보이게 된다.

images/chapter14-tour-adminfilter.png

그림 14-4. 장고 관리자에서 필터링 시 더 많은 필드를 보여주기

admin 앱이 모든 일반적인 기능을 무료로 제공하며, 우리가 무엇을 원하는지에 대하여 순수하게 선언해주기만 하면 된다는 점을 다시 한번 확인할 수 있다. 그렇지만, 보다 특별한 요구가 있을 경우에 admin 앱의 행위를 커스터마이즈할 수 있는 방법도 있다. admin은 너무나 강력하여, 그것만으로 전체 웹 애플리케이션을 구축하는 경우도 있다. 자세한 내용은 공식 문서 http://docs.djangoproject.com/en/1.0/ref/contrib/admin/를 참조하라.

뷰와 템플릿

이제 admin을 알아버렸으니, 장고 웹 프레임워크의 구조를 보여주는 목적으로 간단한 CRUD 예제를 써먹을 수는 없게 되었다. CRUD가 거의 대부분의 데이터 기반의 웹 애플리케이션에서 쓰이기는 하지만, 여러분의 사이트를 차별화해주지는 못하므로 개의치 않겠다. 지겨운 일들은 admin 앱에 맡기고, 우리는 polls에 집중하도록 하자.

우리는 이미 모델을 갖추고 있으므로, HTTP를 기반으로 하여 우리의 앱이 외부 세계와 대화할 수 있도록(바로 웹 애플리케이션 제작의 핵심이다) 뷰를 작성할 때이다.

Note

장고 개발자들 사이에서 농담처럼 하는 말로, 장고는 모델, 템플릿, 뷰의 MTV 패턴을 따른다. 이 세 가지 구성요소는, 다른 프레임워크에서는 모델, 뷰, 컨트롤러라고 부르는 것들에 해당한다. 장고가 일반적인 웹 프레임워크의 명명법을 따르지 않는 이유는, 엄밀히 말해서 프레임워크 자체가 바로 컨트롤러이기 때문이다. 다른 프레임워크에서 컨트롤러라고 불리는 것은 실은 HTTP와 출력 템플릿과 묶여있기 때문에, 이것을 뷰 계층으로 간주할 수 있다. 이러한 관점이 마음에 들지 않는다면, 그냥 장고에서의 템플릿은 뷰이고, 장고의 뷰는 컨트롤러라고 기억해두도록 하자.

관례적으로, 뷰를 위한 코드는 views.py 파일에 작성한다. 뷰는 HTTP 요청을 받아서 처리하고 HTTP 응답을 반환하는 단순한 기능 요소이다. HTTP 응답은 HTML 페이지의 생성에 관여하는 것이 보통이기 때문에, 템플릿은 뷰가 HTML 출력을 생성하는 작업을 도와서(다른 텍스트 기반의 출력도 포함하여), 수작업으로 문자열을 처리하는 것에 비해 유지보수성을 높여준다.

polls 앱은 아주 간단한 네비게이션을 갖추고 있다. 첫째, 사용자에게는 최신 설문 조사 목록에 접근할 수 있는 색인이 나타난다. 사용자가 설문조사를 하나 선택하면 상세 내용이 보이며, 유효한 선택지와 단추가 있어서 사용자는 자신이 선택한 것을 제출할 수 있다. 선택이 이루어지면, 사용자가 방금 투표한 설문조사의 현재 결과를 보여주는 페이지로 이동한다.

장고 앱을 디자인하는 좋은 출발점은, 보여주는 코드를 작성하기에 앞서, URL부터 디자인하는 것이다. 장고에서는 URL을 뷰 함수에 대응시키는데, 이때 정규표현식을 활용한다. 요즘은 웹 개발에서 URL에 공을 들이며, 훌륭한(DoSomething.do 또는 ThisIsNotNice.aspx와 같이 읽기 어렵지 않은) URL이 비일비재하다. 장고에서는, URL 재작성(rewriting)을 통해 지저분한 이름을 짜깁기하는 대신에, 뷰를 기동시키는 URL과 내부적인 이름 사이를 간접적으로 이어주는 계층을 제공한다. 또한, 장고에서는 여러 프로젝트에서 재사용할 수 있는 앱을 구현하는 데에 중점을 두고 있다. 그에 따라, 앱에서 사용하는 뷰의 상대적 URL을 정의할 수 있고, 차후에 다른 프로젝트에서 재사용도 가능하도록, URL을 정의하는 모듈화된 방법을 제공한다.

pollsite/urls.py 파일을 예제 14-14와 같이 수정해보자.

예제 14-14. 앱의 뷰 기능을 위해 urls.py를 수정하여 상대적 URL을 정의

from django.conf.urls.defaults import *

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^polls/', include('pollsite.polls.urls')),
)

위의 패턴의 의미는 이러하다. URL이 polls/로 시작하면 pollsite.polls.urls에 정의된 패턴을 따르는지 매칭을 계속한다. pollsite/polls/urls.py 파일을 만들고(앱 내부에 있어야함에 주의), 예제 14-15와 같이 코드를 추가하자.

예제 14-15. pollsite/polls/urls.py로부터 대체 URL을 매치

from django.conf.urls.defaults import *

urlpatterns = patterns('pollsite.polls.views',
    (r'^$', 'index'),
    (r'^(\d+)/$', 'detail'),
    (r'^(\d+)/vote/$', 'vote'),
    (r'^(\d+)/results/$', 'results'),
)

첫 번째 패턴의 의미는 다음과 같다. 일치되는 것이 없다면(polls/는 이미 앞의 패턴에서 일치되었음을 기억하라) index 뷰를 사용하라. 그 외의 패턴에서는 d+와 같이 숫자를 찾는 정규표현식을 써서, (괄호를 사용하여) 캡처한 다음, 그에 해당하는 뷰에 인자로 넘겨주도록 하였다. 최종적으로는 polls/5/results/과 같은 URL이 results 뷰를 호출하면서 문자열 '5'를 두번째 인자로 넘겨주게 된다(첫번째 인자는 항상 request 개체이다). 장고 URL 디스패치에 대하여 자세한 내용을 알고 싶다면, http://docs.djangoproject.com/en/1.1/topics/http/urls/를 참조하라.

자, 색인(index), 세부(detail), 투표(vote) 및 결과(results)에 대한 URL 패턴을 만들었으니, 그에 해당하는 뷰 함수를 각각 작성하도록 하자. 예제 14-16은 pollsite/polls/views.py의 코드이다.

예제 14-16. URL Patterns로부터 사용되는 뷰 함수들

from django.shortcuts import get_object_or_404, render_to_response
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from pollsite.polls.models import Choice, Poll

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('polls/index.html',
                              {'latest_poll_list': latest_poll_list})

def detail(request, poll_id):
    poll = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/detail.html', {'poll': poll})

def vote(request, poll_id):
    poll = get_object_or_404(Poll, pk=poll_id)
    try:
        selected_choice = poll.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the poll voting form.
        return render_to_response('polls/detail.html', {
            'poll': poll,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(
            reverse('pollsite.polls.views.results', args=(poll.id,)))

def results(request, poll_id):
    poll = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/results.html', {'poll': poll})

약간 성급한 듯 하기는 하지만, 어디까지나 둘러보기임을 명심하라. 여기서 중요한 것은 개요를 파악하는 것이다. 이 파일에서 정의된 각 함수는 뷰이다. 이 함수들은 view.py 파일 내에 정의되어 있으므로 식별할 수 있다. 그것들은 첫 번째 인수로 요청을 받을 수 있다는 점으로도 식별할 수 있다.

이렇게 해서, 우리는 앞에서 정의한 URL 패턴과 일치할 경우에 호출되도록 index, details, vote, results라고 이름붙인 뷰를 정의하였다. vote의 경우를 제외하면, 그것들은 직관적이며 동일한 패턴을 따른다.그것들은 몇몇 데이터를 검색하며(get_object_or_404와 같은 장고 ORM 및 도움말 기능을 사용하는데, 그것이 무엇인지 잘 모르더라도 이름에서 상상할 수 있을 것이다), 결국에는 render_to_response를 호출하며, 이때 템플릿의 경로와 함께 템플릿에 전달된 사전을 전달하게 된다.

Note

예로 든 세 개의 간단한 뷰를 통하여, 장고 웹 개발에서는 적은 코드만으로도 간단히 구현할 수 있는 추상화를 제공하는 것이 매우 일반적임을 보였다. 이러한 추상화는 일반적인 뷰(Generic Views)라고 하는데, http://docs.djangoproject.com/en/1.1/ref/generic-views/ 및 장고 자습서 http://docs.djangoproject.com/en/1.1/intro/tutorial04/#use-generic-views-less-code-is-better에서 배울 수 있다.

vote 뷰에는 투표를 등록하는 흥미로운 기능이 있으므로 좀 더 살펴보는 것이 마땅하다. 두 가지 경우가 있을 수 있는데, 하나는 예외사항으로서 사용자가 아무 것도 선택하지 않은 경우이고, 다른 하나는 사용자가 무언가를 선택한 경우이다. 처음의 경우에 뷰는 결국 상세 뷰가 렌더한 것과 똑같은 템플릿을 렌더하게 된다: polls/detail.html; 하지만 에러 메시지를 표시하기 위한 추가적인 변수를 템플릿에 전달함으로써 사용자가 왜 동일한 페이지가 보이는지 알 수 있도록 할 수 있다. 사용자가 선택을 한 경우에는, 득표수(votes)를 증가시키고 결과 뷰로 리디렉션한다.

단지 뷰를 호출하여(예컨대 return results(request, poll.id)와 같이) 리디렉션을 구현할 수도 있지만, 주석에서 밝힌 것과 같이, POST 제출 후에 실제로 HTTP 리다이렉트를 하는 것이 브라우저의 뒤로 가기(혹은 새로고침)로 인하여 발생할 수 있는 문제를 피할 수 있는 모범답안이다. 뷰 코드는 어떤 URL을 매핑하는지 알지 못하기 때문에(이는 앱을 재사용할 때 기회로 남겨지므로), reverse function은 주어진 뷰와 파라이터에 대한 URL을 제공한다.

템플릿을 살펴보기 전에, 한 가지 기억해 둘 것이 있다. 장고 템플릿 언어는 꽤 단순하며, 의도적으로 프로그래밍 언어와 같이 강력하지 않게끔 설계되었다. 임의로 파이썬 코드를 실행시키거나 어떠한 함수도 호출할 수 없다. 이는 템플릿이 단순하고 웹 디자이너에게 친화적이도록 하기 위한 것이다. 템플릿 언어의 주요 구성은, 중괄호 두개({{와 }})로 감싸진 표현식과, 중괄호 및 퍼센트 기호({%와 %})를 사용하는 지시자(템플릿 태그라고도 함)이다. 표현식(expressions)은 파이썬 속성 및 사전 항목에 접근하기 위한 점(.)을 포함할 수 있으며(파이썬에서는 foo['bar']라고 쓸 것이라 할지라도 {{ foo.bar }}와 같이 쓴다), 표현식에 필터를 적용하기 위하여 파이프(|)를 포함할 수 있다(단락의 처음 다섯 단어를 잘라내려면 {{ comment.text|truncatewords:5 }}와 같이 쓴다). 이런 식이다. 뒤따르는 템플릿들에서 마땅한 것들을 볼 수 있겠지만, 그렇지 않은 것들에 대하여 설명을 하겠다.

이제 우리의 뷰에 대한 템플릿을 살펴볼 차례이다. 우리가 방금 작성한 뷰 코드를 읽어보면 세 개의 템플릿, polls/index.html, polls/detail.html 그리고 polls/results.html이 필요함을 유추할 수 있을 것이다. polls 앱에 templates 하위 디렉토리를 생성하고, 그 아래에 템플릿을 만든다. 예제 14-17은 pollsite/polls/templates/polls/index.html/의 내용을 보여준다.

예제 14-17. 템플릿을 포함하는 polls 앱의 index 파일

{% if latest_poll_list %}
<ul>
  {% for poll in latest_poll_list %}
  <li><a href="{{ poll.id }}/">{{ poll.question }}</a></li>
  {% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}

보는 바와 같이 아주 단순하다. pollsite/polls/templates/polls/detail.html를 살펴보자(예제 14-18).

예제 14-18. poll 템플릿

<h1>{{ poll.question }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="./vote/" method="post">
{% for choice in poll.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}"
value="{    { choice.id }}" />
    <label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>

이 템플릿이 {{ forloop.counter }} 표현식으로 구축되어, 내부의 카운터를 {% for %} 루프에 제공하고 있음에 놀랄지도 모르겠다.

또한 {% if %} 템플릿 태그는 표현식이 false로 정의되지 않았음을 검증하는데, 이는 템플릿이 상세 뷰로부터 호출되어 error_message를 갖는 경우가 될 것이다.

끝으로, 예제 14-19는 pollsite/polls/templates/polls/results.html이다.

예제 14-19. 결과 템플릿 코드

<h1>{{ poll.question }}</h1>

<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice }} -- {{ choice.votes }}
    vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

이 템플릿에서는 {{ choice.votes|pluralize }} 표현에서 필터 사용을 볼 수 있다. 이것은 vote의 수가 1보다 크면 ?를 출력하고, 그렇지 않으면 아무 것도 출력하지 않는다. 장고의 템플릿 태그와 필터에 대하여 자세히 알고 싶으면 http://docs.djangoproject.com/en/1.1/ref/templates/builtins/를 참조하라. 그리고, 새로운 필터와 템플릿 태그를 생성하는 방법을 알고 싶으면 http://docs.djangoproject.com/en/1.1/ref/templates/api/를 참조하라.

이제 우리는 완전히 작동하는 설문조사 사이트를 만들었다. 그리 예쁘지는 않으므로 많이 다듬어야 하겠지만, 어쨌든 작동한다! http://localhost:8000/polls/로 이동하여 사용해보기 바란다.

템플릿 상속

다른 많은 템플릿 언어와 마찬가지로, 장고 또한 include 지시자를 포함하고 있다. 그러나 그것은 거의 쓰이지 않는데, 그 이유는 템플릿을 재사용하는 데에 있어 상속이라고 하는 더 좋은 방법이 있기 때문이다.

이것은 클래스 상속과 마찬가지이다. 우선 기본 템플릿을 정의하는데, 여기에는 많은 블록block이 있다. 각 블록에는 이름이 붙여진다. 그런 다음, 다른 템플릿에서는 기본 템플릿으로부터의 상속을 통해 블록들을 재정의하거나 확장할 수 있다. 클래스 계층 구조와 마찬가지로, 원한다면 얼마든지 상속 구조를 구축할 수 있다.

여러분은 우리의 템플릿인 유효한 HTML이 아닌, 조각만 만들어낸다는 것을 눈치챘을 지 모른다. 이것은 물론, 템플릿의 중요한 부분에 초점을 맞추기 위해서이다. 완전하고, 미려한 HTML 페이지를 생성하는 데에 있어 사소한 수정이 일어나기는 한다. 지금쯤 눈치챘겠지만, 그것들은 전체 사이트의 기본 템플릿으로부터 확장된다.

우리는 웹 디자인에 대해서는 문외한이므로, http://www.freecsstemplates.org/에서 이미 만들어져있는 템플릿을 가져다가 쓸 것이다. 이 템플릿을 수정하도록 하자. http://www.freecsstemplates.org/preview/exposure/

참고로 기초 템플릿은 사이트 전체에 적용되기 때문에, 이것은 개별 앱이 아니라 프로젝트에 속하게 된다. 우리는 프로젝트 디렉토리 아래에 템플릿 하위 디렉토리를 만들 것이다. 예제 14-20는 pollsite/templates/base.html의 내용이다.

예제 14-20. 사이트 전체 템플릿

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/    xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>Polls</title>
    <link rel="alternate" type="application/rss+xml"
          title="RSS Feed"  href="/feeds/polls/" />
    <style>
      /* Irrelevant CSS code, see book sources if you are interested */
    </style>
  </head>
  <body>
    <!-- start header -->
    <div id="header">
      <div id="logo">
        <h1><a href="/polls/">Polls</a></h1>
        <p>an example for the Jython book</a></p>
      </div>
      <div id="menu">
        <ul>
          <li><a href="/polls/">Home</a></li>
          <li><a href="/contact/">Contact Us</a></li>
          <li><a href="/admin/">Admin</a></li>
        </ul>
      </div>
    </div>
<!-- end header -->
<!-- start page -->
    <div id="page">
    <!-- start content -->
      <div id="content">
        {% block content %} {% endblock %}
      </div>
<!-- end content -->
      <br style="clear: both;" />
    </div>
<!-- end page -->
<!-- start footer -->
    <div id="footer">
      <p> <a href="/feeds/polls/">Subscribe to RSS Feed</a> </p>
      <p class="legal">
        &copy;2009 Apress. All Rights Reserved.
        &nbsp;&nbsp;&bull;&nbsp;&nbsp;
        Design by
        <a href="http://www.freecsstemplates.org/">Free CSS Templates</a>
        &nbsp;&nbsp;&bull;&nbsp;&nbsp;
        Icons by <a href="http://famfamfam.com/">FAMFAMFAM</a>. </p>
    </div>
<!-- end footer -->
  </body>
</html>

보다시피, 템플릿에는 content라는 이름의 단 하나의 블록이 (템플릿 마지막의 footer 전에) 선언된다. 원하는 만큼 블록을 정의할 수 있지만, 단순하게 하기 위해 하나만 만들었다.

자, 장고가 이 템플릿을 찾을 수 있도록 설정해보자. pollsite/settings.py를 편집하고 TEMPLATE_DIRS 부분을 찾아보기 바란다. 그것을 예제 14-21와 같이 바꾼다.

예제 14-21. setting.py 편집하기

import os
TEMPLATE_DIRS = (
    os.path.dirname(__file__) + '/templates',
    # Put strings here, like "/home/html/django_templates" or
    # "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

이것은 프로젝트의 루트 디렉토리를 하드 코딩하는 것을 피하기 위한 트릭이다. 이러한 트릭이 모든 상황에서 적용되지는 않겠지만, 우리에게는 유효하다. 이제 우리는 base.html 템플릿을 제 위치에 갖게 되었으며, pollsite/polls/templates/polls/index.html에서 그것으로부터 상속할 것이다. 예제 14-22를 보라.

예제 14-22. base.html로부터 상속

{% extends 'base.html' %}
{% block content %}
{% if latest_poll_list %}
<ul>
  {% for poll in latest_poll_list %}
  <li><a href="{{ poll.id }}/">{{ poll.question }}</a></li>
  {% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
{% endblock %}

보는 바와 같이, 처음 두 줄과 마지막 한 줄만 변경한다. 여기서 보여주고자하는 바는, 이 템플릿에서는 content 블록만을 재정의하며 나머지는 모두 상속받는다는 점이다. poll 앱의 다른 두 템플릿에 대해서도 동일한 작업을 한 다음, http://localhost:8000/polls/를 방문하여 애플리케이션을 테스트 한다. 그림 14-5와 같이 보일 것이다.

images/chapter14-tour-aftertemplate.png

그림 14-5. 템플릿을 적용한 후의 poll 사이트

이만하면 우리의 예제 웹 애플리케이션이 완전하다고도 볼 수 있다. 하지만 우리는 (admin과 마찬가지로) 여러분의 웹 앱 개발을 도와주는 장고의 다른 기능을 부각시키고자 한다. 시연을 위해 다음과 같은 기능을 사이트에 추가할 것이다.

1.문의 양식 (링크는 이미 우리의 일반적인 기본 템플릿에 포함된다)

2.최근 설문조사에 대한 RSS 피드 (또한 링크가 이미 바닥글에 추가되었다)

3.설문조사에 대한 사용자의 댓글

양식

장고에는 HTML 양식을 다루는 따분한 일을 처리하는 데 도움이 될만한 기능이 갖추어져 있다. 이러한 도움을 활용하여 문의(contact us) 기능을 구현해볼 것이다. 이것은 앞으로 재사용할 수 있을 만한 기능이므로, 새로운 앱을 생성하도록 하겠다. 프로젝트 디렉토리로 이동하여 다음과 같이 실행한다.

$ jython manage.py startapp contactus

pollsite/settings.py의 INSTALLED_APPS 목록에 'pollsite.contactus'를 추가하는 것을 기억하기 바란다.

그런 다음, pollsite/urls.py를 수정하고 한 줄을 추가함으로써 앱에 대한 /contact/ 패턴 URL 매칭을 위임한다(예제 14-23 참조).

예제 14-23. urls.py 다시 수정

from django.conf.urls.defaults import *

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^polls/', include('pollsite.polls.urls')),
    (r'^contact/', include('pollsite.contactus.urls')),
)

우리는 지금 pollsite/contactus/urls.py를 만든다. 예제를 단순화하기 위해, 양식을 보여주고 처리하는 데에 있어 단 하나의 뷰만을 사용할 것이다. 따라서 pollsite/contactus/urls.py 파일은 예제 14-24와 같이 구성된다.

예제 14-24. 짧은 urls.py 만들기

from django.conf.urls.defaults import *

urlpatterns = patterns('pollsite.contactus.views',
    (r'^$', 'index'),
)

그리고 pollsite/contactus/views.py의 내용은 예제 14-25와 같다.

예제 14-25. views.py에 추가

from django.shortcuts import render_to_response
from django.core.mail import mail_admins
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=200)
    email = forms.EmailField()
    title = forms.CharField(max_length=200)
    text = forms.CharField(widget=forms.Textarea)


def index(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            mail_admins(
                "Contact Form: %s" % form.title,
                "%s <%s> Said: %s" % (form.name, form.email, form.text))
            return render_to_response("contactus/success.html")
    else:
        form = ContactForm()
    return render_to_response("contactus/form.html", {'form': form})

여기서 중요한 것은 ContactForm 클래스인데, 양식이 선언적으로 정의되고 유효성 검사 논리를 캡슐화한다. 그러한 논리를 적절히 사용하기 위해, 우리는 뷰에서 is_valid() 메소드를 호출하기만 하면 된다.장고에 포함된 main_admins 함수와 그것을 사용하기 위한 프로젝트 설정 방법에 대해서는 http://docs.djangoproject.com/en/1.1/topics/email/#mail-admins 문서를 참조하라.

forms는 또한 템플릿에서 양식을 재빨리 그려내는(render) 방법을 제공한다. 한번 시도해보자. 예제 14-26은 pollsite/contactus/templates/contactus/form.html의 코드로서, 우리가 방금 작성한 뷰의 내부에서 사용되는 템플릿이다.

예제 14-26. 템플릿 내에서 그려진 양식

{% extends "base.html" %}
{% block content %}
<form action="." method="POST">
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Send Message" >
</form>
{% endblock %}

여기서 우리는 장고 양식의 as_table() 메소드의 장점인, 그리기 유효성 오류를 처리해주는 장점을 취한다. 장고의 폼은 또한 양식을 그리는 편리한 기능을 제공하지만, 여러분의 필요에 맞지 않다면 언제든지 개별적인 요구에 대하여 맞춤으로 만들어 낼 수도 있다. 양식을 다루는 자세한 내용은 http://docs.djangoproject.com/en/1.1/topics/forms/를 참조하라.

이 문의 양식을 테스트하기 전에, pollsite.contactus.views.index에서 사용할 pollsite/contactus/templates/contactus/success.html 템플릿을 작성할 필요가 있다. 이 템플릿은 매우 간단하다(예제 14-27 참조).

예제 14-27. 문의 양식 템플릿

{% extends "base.html" %}
{% block content %}
<h1> Send us a message </h1>
<p><b>Message received, thanks for your feedback!</p>
{% endblock %}

이제 다 끝냈다. http://localhost:8000/contact/를 열어서 테스트해보자. 데이터 없이 양식을 제출한다든지, 잘못된 데이터(잘못된 이메일 주소 등)를 입력해보라. 그림 14-6과 같은 것을 볼 수 있을 것이다. 많은 양의 코드를 작성하지 않더라도 여러가지 유효성 검증을 손쉽게 구현할 수 있다. 물론 양식 프레임워크는 확장 가능하므로, 양식의 항목 및 그에 맞는 유효성 검증과 그리기 코드를 맞춤으로 만들어낼 수 있다. 다시 말하지만, 자세한 내용은 http://docs.djangoproject.com/en/1.1/topics/forms/ 문서를 참조하기 바란다.

images/chapter14-tour-formvalidation.png

그림 14-6. 장고 양식 유효성 검증 실행

피드

바닥글 바로 윗 부분에 링크로 제공할 피드를 구현해보도록 하자. 장고에서는 피드를 선언적으로 기술하여 매우 빨리 구현하는 방법을 제공한다는 것이 전혀 놀라울 일이 없다. 예제 14-28에 보이는 것과 같이 pollsite/urls.py를 수정하자.

예제 14-28. urls.py 수정

from django.conf.urls.defaults import *
from pollsite.polls.feeds import PollFeed

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^polls/', include('pollsite.polls.urls')),
    (r'^contact/', include('pollsite.contactus.urls')),
    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
     {'feed_dict': {'polls': PollFeed}}),
)

우리는 (아직 작성하지 않은) PollFeed 클래스에 대한 들여오기를 변경하며 /feed/로 시작하는 마지막 URL 패턴도 필요한데, 그것은 사전을 인자로 받는 내장 뷰에 매핑할 것이기 때문이다. 이 경우에는 PollFeed가 바로 그것이다. 피드를 기술하는 클래스를 작성하는 것은 매우 간단하다. pollsite/polls/feeds.py 파일을 만들어 코드를 집어넣어보자. 예제 14-29를 보라.

예제 14-29. feeds.py 생성

from django.contrib.syndication.feeds import Feed
from django.core.urlresolvers import reverse
from pollsite.polls.models import Poll

class PollFeed(Feed):
    title = "Polls"
    link = "/polls"
    description = "Latest Polls"

    def items(self):
        return Poll.objects.all().order_by('-pub_date')

    def item_link(self, poll):
        return reverse('pollsite.polls.views.detail', args=(poll.id,))

    def item_pubdate(self, poll):
        return poll.pub_date

준비가 거의 다 되었다. 장고가 /feeds/polls/ URL에 대한 요청을 받으면, 피드에 대하여 기술된 대로 모든 XML 자료를 구축한다. 여기서 피드에서 설문조사의 내용을 어떻게 표출할 것인가에 대한 부분은 빠져있다. 그것을 위해서는 또 다른 템플릿을 만들어야 한다. 관례적으로 feeds/<피드_이름>_description.html 과 같이 이름을 짓는데, <피드_이름>은 pollsite/urls.py에서 feed_dict의 키로 지정한 것이다. 하여 우리는 예제 14-30과 같은 매우 단순한 내용의 pollsite/polls/templates/feeds/polls_description.html 파일을 만든다.

예제 14-30. 설문조사 표출에 대한 기술

<ul>
   {% for choice in obj.choice_set.all %}
   <li>{{ choice.choice }}</li>
   {% endfor %}
</ul>

아이디어는 간단하다: 장고는 PollFeed.items()이 반환하는 각 개체를, 이름 개체를 취하는 이 템플릿에 전달한다. 그런 다음 피드 결과에 포함될 HTML 조각을 생성한다.

이것이 전부이다. 테스트하려면 브라우저로 http://localhost:8000/feeds/polls/를 가리키거나, 그 URL을 여러분이 선호하는 피드 리더로 구독하면 된다. 예를 들어 오페라 브라우저에서는 그림 14-7과 같이 피드를 보여준다.

images/chapter14-tour-feed.png

그림 14-7. 오페라 브라우저에서 보이는 설문조사 피드

댓글

댓글은 요즘의 웹 사이트에서 일반적인 기능이므로, 장고에는 어떤 프로젝트나 앱에서 간단하게 댓글 시스템을 넣을 수 있도록 작은 프레임워크가 포함되어 있다. 우리 프로젝트에서 어떻게 사용할 것인지 보여주도록 하겠다. 우선, 예제 14-31과 같이 pollsite/urls.py 파일에 comments 앱을 위한 새로운 URL 패턴을 추가한다.

예제 14-31. urls.py에 새로운 패턴을 추가

from django.conf.urls.defaults import *
from pollsite.polls.feeds import PollFeed

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^polls/', include('pollsite.polls.urls')),
    (r'^contact/', include('pollsite.contactus.urls')),
    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
     {'feed_dict': {'polls': PollFeed}}),
    (r'^comments/', include('django.contrib.comments.urls')),
)

다음으로 pollsite/settings.py의 INSTALLED_APPS에 'django.contrib.comments'를 추가한다. 그런 다음, 다음의 명령을 수행하여 장고가 필요한 표들을 생성하도록 한다.

$ jython manage.py syncdb

댓글은 설문조사 페이지에 부가되므로, pollsite/polls/templates/polls/detail.html를 편집해야한다. 다음의 코드를 현재 파일의 마지막 행인 {% endblock %} 행 바로 앞에 추가하도록 한다(예제 14-32 참조).

예제 14-32. Details.html에 댓글 추가

{% load comments %}
{% get_comment_list for poll as comments %}
{% get_comment_count for poll as comments_count %}

{% if comments %}
<p>{{ comments_count }} comments:</p>
{% for comment in comments %}
<div class="comment">
  <div class="title">
    <p><small>
    Posted by <a href="{{ comment.user_url }}">{{ comment.user_name }}</a>,
        {{ comment.submit_date|timesince }} ago:
    </small></p>
  </div>
  <div class="entry">
    <p>
    {{ comment.comment }}
    </p>
  </div>
</div>

{% endfor %}

{% else %}
<p>No comments yet.</p>
{% endif %}

<h2>Left your comment:</h2>
{% render_comment_form for poll %}

기본적으로, 우리는 ({% load comments %}를 함으로써) 댓글 템플릿 태그 라이브러리를 들여온 다음에 그것을 사용하기만 한다. 어떠한 데이터베이스 개체에 대해서든 댓글을 구현해주기 때문에, 그것이 작동하도록 하기 위해 특별한 일을 할 필요가 없다. 우리가 코드의 짧은 발췌 문장에 대한 대가로 얻을 그림 14-8 보여준다.

images/chapter14-tour-comments.png

그림 14-8. 댓글 기능이 추가된 설문조사

만약에 애플리케이션을 시험해본다면 댓글을 제출한 후에 성공 메시지를 나타내는 못생긴 페이지를 볼 수 있을 것이다. 혹은 데이터를 모두 입력하지 않았다면, 못생긴 오류 양식을 보게 될 것이다. 그것은 우리가 댓글 템플릿을 사용하기 때문이다. 신속하고 효과적인 해결책은 다음과 같은 내용으로 pollsite/templates/comments/base.html 파일을 만드는 것이다.

{% extends 'base.html' %}

단 한 줄이다! 이것이 바로 템플릿 상속의 힘이다. 우리가 해야 할 일은 단 하나, 댓글 프레임워크의 기본 템플릿 대신에 우리의 글로벌 기본 템플릿으로부터 상속하도록 변경하기만 하면 된다.

기타

이만하면 장고의 강력함을 충분히 맛보았을 것으로 기대한다. 그 자체로 아주 좋은 웹 프레임워크이기도 하지만, '배터리 포함' 철학을 바탕으로, 웹 개발에서 겪는 공통적인 문제의 해법을 제공해준다. 그리하여 새로운 웹 사이트를 만드는 많은 과정의 속도를 높여준다. 그리고, 장고에서 제공하는 사용자 인증이나 제너릭 뷰와 같은 기능은 다루지 않았다.

하지만 이 책은 자이썬을 주제로 하기 때문에, 이 장의 나머지 부분은 자이썬 환경에서 장고를 사용할 때 얻을 수 있는 흥미로운 가능성을 소개하는 데 할애하려고 한다. 장고에 대하여 더 알고 싶은 독자에게는, http://docs.djangoproject.com/에 있는 훌륭한 공식 문서를 다시금 추천한다.

J2EE 배포 및 통합

애플리케이션을 배포할 때에는 장고에 내장된 개발 서버를 사용해서는 안된다. 개발 서버는 부하를 견딜 수 있도록 설계되지 않았으므로, 운영에 적합한 애플리케이션 서버를 찾아보아야 할 것이다. 여기서는 선 마이크로시스템즈의 오픈소스 고성능 JaveEE 5 애플리케이션 서버인 글래스피쉬 2.1을 설치하여 배포해보도록 하겠다.

다음 주소에서 글래스피쉬를 구하여 설치해보자.

https://glassfish.dev.java.net/public/downloadsindex.html

이 글을 쓰고 있을 당시, 장고와 자이썬 지원 기능이 내장된 글래스피쉬 3.0의 출시가 준비되고 있기는 하지만, 문서화가 완전히 이루어지고 안정적인 버전을 기준으로 삼으려고 한다. 2.1 판을 내려받도록 하자(현재 v2.1-b60e). 배포에는 JDK6을 사용하기를 강력히 권장한다. 설치 JAR 파일을 내려받은 후에는 다음의 명령어를 사용하여 설치할 수 있다.

% java -Xmx256m -jar glassfish-installer-v2.1-b60e-windows.jar

파일명은 위의 예제와 차이가 있을 수도 있으므로, 그에 맞춰서 실행하도록 하자. 글래스피쉬는 설치 프로그램을 시작하는 디렉토리에 glassfish 하위 디렉토리가 생성되어 애플리케이션 서버를 풀어넣게 되므로 위의 명령을 실행하는 위치에 주의하라. 이제 ant를 호출하여 설치를 마무리하자. 유닉스 계열의 시스템에서는 다음의 명령을 호출한다.

% chmod -R +x lib/ant/bin
% lib/ant/bin/ant -f setup.xml

윈도우에서는 다음과 같다.

% lib\ant\bin\ant -f setup.xml

이제 설치가 완료되었다. bin 디렉토리에 asadmin 또는 asadmin.bat 파일이 보인다면, 애플리케이션 서버가 설치된 것이다. 다음의 명령을 호출하여 서버를 시작한다.

% bin/asadmin start-domain -v

윈도우에서는 서버가 foreground 프로세스로 실행되며, 백그라운드 데몬으로 실행되지 않는다. 유닉스 운영체제에서는 자동으로 데몬으로서 백그라운드로 실행된다. 어느 경우이든, 일단 서버가 살아서 동작하게 되면, 브라우저에서 http://localhost:5000/을 열어서 웹 관리 화면을 볼 수 있을 것이다. 기본 로그인은 admin이며 비밀번호는 adminadmin이다.

현재, 자이썬에서의 장고는 공식적으로 PostgreSQL, 오라클, MySql 데이터베이스만 지원하지만, SQLite3 뒷단도 있다. PostgreSQL 뒷단을 사용하려면 PostgreSQL JDBC 드라이브를 http://jdbc.postgresql.org에서 구해야 한다.

이 글을 쓰는 현재의 최신 버전은 postgresql-8.4-701.jdbc4.jar이다. 이 jar 파일을 여러분의 GLASSFISH_HOME/domains/domain/domain1/lib 디렉토리로 복사하자. 동일한 JDBC 드라이버를 사용하여 애플리케이션 서버 내의 모든 애플리케이션에서 사용할 수 있게 된다.

이제 GLASSFISH_HOME/domains/domain1/lib 디렉토리에는 예제 14-33과 같은 것이 있을 것이다.

예제 14-33. lib 디렉토리

applibs/
classes/
databases/
ext/
postgresql-8.3-604.jdbc4.jar

애플리케이션 서버를 정지하였다가 시작함으로써 라이브러리를 적재하도록 하자.

% bin/asadmin stop-domain
% bin/asadmin start-domain -v

애플리케이션 배포

자이썬에서의 장고는 WAR 파일을 생성할 수 있도록 명령을 내장하고 있지만, 우선은, 모든 것이 원활하게 돌아갈 수 있도록 약간의 구성이 필요하다. 일단은 관리 애플리케이션에서 모델을 만질 수 있도록 간단한 장고 애플리케이션을 구성하도록 하겠다. hello라는 프로젝트를 생성하고 INSTALLED_APPS에 add django.contrib.admin 및 doj 애플리케이션을 추가하도록 하자.

이제 urls.py를 열어서 admin 행의 주석을 제거하자. urls.py가 예제 14-34와 같이 되도록 한다.

예제 14-34.urls.py에서 admin 활성화

from django.conf.urls.defaults import *
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
)

PostgreSQL 설정

PostgreSQL을 사용하는 개발 서버에서는 아무래도 데이터베이스 인증 검사를 해제하는 것이 낫다. 그렇게 하는 가장 빠른 방법은 pg_hba.conf 파일을 편집하여 로컬에서만 데이터베이스에 접속할 수 있도록 하는 것이다. PostgreSQL 8.3의 경우, 이 파일의 경로는 일반적으로 C:PostgreSQL8.3datapg_hba.conf이며 유닉스의 경우 /etc/PostgreSQL/8.3/data/pg_hba.conf에 위치한다.

이 파일의 끝 부분에 접속 설정 정보가 있다. 모든 행을 주석 처리하고 로컬 호스트로부터의 신뢰할 수 있는 접속만 사용 가능하도록 하라. 수정한 구성은 예제 14-35와 같이 될 것이다.

예제 14-35. PostgreSQL의 인증 설정

# TYPE  DATABASE    USER        CIDR-ADDRESS          METHOD
host    all         all         127.0.0.1/32          trust

이와 같이 하면 PostgreSQL에서 인식하는 사용자명은 데이터베이스로의 접속이 허용된다. createuser 명령을 사용하여 PostgreSQL 사용자를 생성해야 할 수도 있다. 자세한 내용은 PostgreSQL 문서를 참조하기 바란다. 공개된 프로덕션 서버에서의 구성은 위와 달라져야 할 것이다. 보다 적합한 구성을 위한 절차에 대해서는 PostgreSQL 문서를 참조하여야 한다. 접속 설정을 수정한 후에는, PostgreSQL 서버를 재시작해야 할 것이다.

이제 createdb 명령을 사용하여 PostgreSQL 데이터베이스를 만든다.

> createdb demodb

데이터베이스의 설정은 직관적이다. 자이썬에서의 장고에서는 pgsql 뒷단(backend)을 활성화하기만 하면 된다. 그런데 PostgreSQL에서는 비활성화하였다 하더라도, pgsql 뒷단에서는 사용자명과 패스워드의 쌍을 요구한다. 이 경우에 DATABASE_NAME 및 DATABASE_USER 설정은 아무 값으로나 채워도 무방하다. 이제 설정 모듈은 예제 14-36과 같이 보일 것이다.

예제 14-36. 설정 모듈에서 PostgreSQL을 위한 데이터베이스 부분

DATABASE_ENGINE = 'doj.backends.zxjdbc.postgresql'
DATABASE_NAME = 'demodb'
DATABASE_USER = 'ngvictor'
DATABASE_PASSWORD = 'nosecrets'

이제 데이터베이스를 초기화한다.

> jython manage.py syncdb
Creating table django_admin_log
Creating table auth_permission
Creating table auth_group
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username: admin
E-mail address: admin@abc.com
Warning: Problem with getpass. Passwords may be echoed.
Password: admin
Warning: Problem with getpass. Passwords may be echoed.
Password (again): admin
Superuser created successfully.
Installing index for admin.
LogEntry model
Installing index for auth.Permission model
Installing index for auth.Message model

위의 사항을 검토하고 나서, 애플리케이션을 운영 중인 글래스피쉬 서버에 배포하자. 이것은 사실 쉬운 부분이다. 자이썬에서의 장고는 자동 압축 파일을 만들어주는 맞춤형 war 명령을 갖고 있어, 자바 서블릿 컨테이터에 배포할 수 있다.

WAR 관련 참고 사항

JavaEE 서버의 경우, 응용 프로그램을 배포하는 일반적인 방법은 WAR 파일을 배포하는 것이다. 이것은 이름만 그럴듯한 zip 파일에 불과하며, 여러분의 애플리케이션 및 거기에서 필요로 하는 공유 자원들을 담고 있다. 앱 서버에서 여러 애플리케이션을 배포하고자 할 때, 라이브러리의 버전 변경에 따른 영향을 최소화할 수 있는 방법이다.

시간을 두고 장고 애플리케이션을 검토하라. 장고의 버전을 업그레이드함은 말할 것도 없고, 데이터베이스 드라이버의 버전도 업그레이드하게 될 지 모른다. 심지어 자이썬 언어의 버전을 업그레이드하게 될 수도 있다. 의존성을 가지는 모든 것을 WAR 파일에 몰아넣을 것인지는 여러분이 선택하는 것이다. 의존성을 가지는 모든 것을 WAR 파일에 포함시킨다면, 애플리케이션을 배포하였을 때 제대로 동작할 것을 보장할 수 있다. 동일한 코드에 대하여 동시적으로 수행되는 각 애플리케이션에 필요한 공간은 서버가 자동으로 할당할 것이다.

war 명령을 활성화하려면, INSTALLED_APPS 목록에서 설정 doj 응용 프로그램을 추가할 수 있다. 다음으로, 당신은 귀하의 사이트의 media 디렉터리와 그에 대한 상대적인 루트를 활성화해야 한다. 미디어 파일이 제대로 제공되고 구성되었는지 있도록 settings.py 모듈을 수정한다. war 명령은 미디어 파일들을 자동으로 구성하여 정적 파일 서블릿을 사용하여 제공되며 URL은 문맥상의 루트 이후로 다시 매핑될 것이다.

setting 모듈에서 MEDIA_ROOT 및 MEDIA_URL 행을 편집하자.

MEDIA_ROOT = 'c:\dev\hello\media_root'
MEDIA_URL = '/site_media/'

hello 프로젝트 아래에 media_root 하위 디렉터리를 생성하고 거기에 샘플 파일을 집어넣어서 정적 컨텐트 제공이 작동하는지 확인해보도록 하라. media_root 디렉터리에 sample.html을 만들어보자. 정적 파일이 올바로 제공되는지 확인해보려는 것이므로 어떤 내용이든 상관 없다.

영어에서 이전의 설정을 사용한다는 것의 의미는, 글래스피쉬의 경우 hello가 여러분의 서블릿 컨테이너에 배포되어 컨테이너는 어떤 URL 경로를 그 context root에 할당함을 의미한다. 이는 여러분의 앱이 http://localhost:8000/hello/에 있음을 뜻한다. site_media 디렉토리는 http://localhost:8000/hello/site_media에서 볼 수 있다. DOJ는 자동적으로 정적 컨텐트를 고성능인 글래스피쉬 파일서블릿으로 구동하도록 설정한다. 대부분의 배포에 있어서 정적 서버 설정을 별도로 할 필요가 없다.

이제 표준 manage.py를 사용하여 WAR 파일을 빌드하고, asadmin 도구를 사용하여 배포한다. 예제 14-37을 보라.

예제 14-37. 윈도우에서 WAR 파일 배포

c:\dev\hello>jython manage.py war

Assembling WAR on c:\docume~1\ngvictor\locals~1\temp\tmp1-_snn\hello

Copying WAR skeleton...
Copying jython.jar...
Copying Lib...
Copying django...
Copying media...
Copying hello...
Copying site_media...
Copying doj...
Building WAR on C:\dev\hello.war...
Cleaning c:\docume~1\ngvictor\locals~1\temp\tmp1-_snn...

Finished.

Now you can copy C:\dev\hello.war to whatever location your application server wants it.

C:\dev\hello>cd \glassfish
C:\glassfish>bin\asadmin.bat deploy hello.war
Command deploy executed successfully.

C:\glassfish>

이제 됐다. 이곳에서 애플리케이션이 구동되는 것을 볼 수 있을 것이다.

http://localhost:8080/hello/

관리자 화면은 이곳에서 볼 수 있다.

http://localhost:8080/hello/admin/

정적인 미디어가 올바로 서비스되고 있는지 확인하려면 이곳으로 가면 된다.

http://localhost:8080/hello/site_media/sample.html

잘 끝났다. 이제 서블릿 컨테이너로의 기본적인 배포본은 작동한다.

확장 설치

doj의 war 명령에서는 외부의 JAR 파일을 지정함으로써 여러분의 JAR 파일의 크기를 줄일 수 있는 부가적인 선택사항을 제공한다. 기본적으로, war 명령은 다음의 항목들을 묶는다.

  • 자이썬
  • 장고 및 관리 미디어 파일
  • 여러분의 프로젝트 및 미디어 파일
  • site-packages 내의 모든 라이브러리

특정 JAR 파일을 포함하고 doj로 하여금 여러분이 필요로하는 파이썬 패키지만 가지고 WAR 파일을 구성하도록 WAR 파일을 특화시킬 수 있다. manage.py war에 대한 옵션은 --include-py-packages와 --include-jar-libs이다. 기본적인 사용법은 직관적이다. 파이썬 패키지 및 JAR 파일의 위치을 위의 두 인자로 전달하면 distutils는 압축된 볼륨을 자동으로 압축 해제하여 다시 여러분의 WAR 파일로 압축한다.

JAR 파일을 포함하기 위해서는 --include-java-libs에 파일의 목록을 지정해야 한다.

다음은 jTDS JAR와 함께 urllib3라는 일반적인 파이썬 모듈을 WAR 파일로 묶는 예이다.

$ jython manage.py war --include-java-libs=$HOME/downloads/jtds-1.2.2.jar \
        --include-py-package=$HOME/PYTHON_ENV/lib/python2.5/site-packages/urllib3

여러 개의 JAR 파일 또는 파이썬 꾸러미 목록을 가질 수 있지만, 운영체제의 경로 구분자로 그것들을 구분해주어야 한다. 유닉스 시스템에서는 콜론(:)을, 윈도우에서는 세미 콜론(;)을 사용한다.

다음과 같이 --include-py-path-entries를 egg 파일명과 함께 사용함으로써 egg도 설치할 수 있다.

$ jython manage.py war --include-py-path-entries=$HOME/PYTHON_ENV/lib/python2.5/site-packages/urllib3

JavaEE 접속 풀링

웹 애플리케이션이 데이터베이스로부터 데이터를 얻으러 갈 때마다, 데이터는 데이터베이스 연결을 통하여 돌아와야한다. MySQL과 같은 몇몇 데이터베이스에서는 데이터베이스 접속에 부담이 적지만, 대부분의 데이터베이스에서는 접속을 맺고 끊는 비용이 만만치 않다. 부하가 많이 일어나는 상황에서, 모든 요청에 대하여 데이터베이스 연결을 열었다 닫았다하는 것은 파일 핸들을 너무 많이 소모하여 애플리케이션이 주저않을 수도 있다.

이에 대한 일반적인 해법은 데이터베이스 접속 풀링이다. 애플리케이션은 여전히 새로운 접속을 생성하고 닫지만, 접속 풀은 재사용 가능한 집합으로부터 데이터베이스 접속을 관리해준다. 여러분이 접속을 닫으려할 때, 접속 풀은 단순히 그 접속을 나중에 재사용한다. 풀을 사용한다는 것은 동시적으로 데이터베이스로 접속하는 숫자의 상한을 정할 수 있다는 의미가 된다. 그러한 상한선을 갖는다는 것은 데이터베이스 접속의 상한선에 도달하였을 때 여러분의 애플리케이션이 어떻게 동작할 것인지에 대하여 생각해볼 수 있다는 것을 의미한다.

장고에서는 기본적으로 CPython을 가지고 데이터베이스 연결 풀을 지원하지 않지만, 자이썬에서는 장고를 위한 PostgreSQL 드라이버를 사용할 수 있다. 장고/자이썬에서 가시적인 접속 풀 생성은 글래스피쉬의 두 단계 프로세스이다. 첫째, 우리는 JDBC 접속 풀을 생성한 다음, 거기에 JNDI 이름을 바인드할 필요가 있다. JavaEE 컨테이너에서 JNDI, 즉 자바 명명 및 디렉토리 인터페이스란, 개체들에 바인딩된 이름의 레지스트리이다. 개체를 만들어내는 팩토리를 추상화해주는 해시테이블과 같다고 생각하면 맞을 것이다.

데이터베이스 연결의 경우에 있어서는, JNDI는 마치 데이터베이스 접속처럼 행동하는 프록시 개체를 제공하는 ConnectionFactory를 추상화한다. 이러한 프록시가 우리를 위해 모든 풀링 동작을 자동으로 관리해준다. 실제 예제를 통하여 살펴보도록 하자.

일단 JDBC ConnectionFactory를 만들어야 한다. 글래스피쉬의 관리 화면에서 Resources - JDBC - JDBC Resources - Connection Pools로 이동한다. 그곳에서 New 단추를 눌러서 풀을 구성할 수 있다.

이름은 pgpool-demo로 하고, resource type은 javax.sql.ConnectionPoolDataSource로, Database Vendor는 PostgreSQL로 한다. 양식을 모두 채우면 그림 14-9와 같이 보일 것이다. Next를 클릭한다.

images/chapter14-conn-pool.png

그림 14-9.글래스피쉬 JDBC 연결 풀 추가

다음은 Additional Properties 화면이다. 데이터베이스 접속이 올바로 동작하도록 네 개의 파라미터를 설정한다. User와 Password는 각각 ngvictor 및 nosecrets로 정하였다고 가정한다. 표 14-1은 데이터베이스에 접속하기 위하여 필요한 것들을 보여준다.

표 14-1. 데이터베이스 연결 풀 속성

이름
databaseName demodb
serverName localhost
password nosecrets
user ngvictor

필요하지 않은 다른 다른 속성들을 안전하게 삭제할 수 있다. 여러분의 속성들을 그림 14-10과 비슷하게 만든 다음에 Finish를 클릭한다.

images/chapter14-conn-props.png

Figure 14-10. Connection Pool Properties in Glassfish Admin Console

Your pool will now be visible on the left-hand tree control in the Connection Pools list. Select it and try pinging it to make sure it? working. If all is well, Glassfish will show you a successful Ping message as seen in Figure 14-11.

images/chapter14-ping.png

Figure 14-11. Successful Connection Pool Test

We now need to bind a JNDI name to the connection factory to provide a mechanism for Jython to see the pool. Go to the JDBC Resources and click ?ew.?Use the JNDI name: ?dbc/pgpool-demo,?and select the ?gpool-demo?as your pool name. Your form should now resemble that shown in Figure 14-12, and you can now hit ?K.?

images/chapter14-glassfish-jdbc.png

Figure 14-12. Adding a New JDBC Resource in Glassfish Admin Console

Verify from the command line that the resource is available. See Listing 14-38.

Listing 14-38. Verifying Connection Pools

glassfish\bin $ asadmin list-jndi-entries --context jdbc
Jndi Entries for server within jdbc context:
pgpool-demo__pm: javax.naming.Reference
__TimerPool: javax.naming.Reference
__TimerPool__pm: javax.naming.Reference
pgpool-demo: javax.naming.Reference
Command list-jndi-entries executed successfully.

Now, we need to enable the Django application to use the JNDI name based lookup if we are running in an application server, and fail back to regular database connection binding if JNDI can? be found. Edit your settings.py module and add an extra configuration to enable JNDI. See Listing 14-39.

Listing 14-39. Enabling JNDI in Settings.py

DATABASE_ENGINE = 'doj.backends.zxjdbc.postgresql'
DATABASE_NAME = 'demodb'
DATABASE_USER = 'ngvictor'
DATABASE_PASSWORD = 'nosecrets'
DATABASE_OPTIONS  = {'RAW_CONNECTION_FALLBACK': True, \
                     'JNDI_NAME': 'jdbc/pgpool-demo' }

Note that we?e duplicating the configuration to connect to the database. This is because we want to be able to fall back to regular connection binding in the event that JNDI lookups fail. This makes our life easier when we?e running in a testing or development environment.

That? it, you?e finished configuring database connection pooling. That wasn? that bad now, was it?

장시간 수행작업

When you?e building a complex web application, you will inevitably end up having to deal with processes that need to be processed in the background. If you?e building on top of CPython and Apache, you?e out of luck here?here? no standard infrastructure available for you to handle these tasks. Luckily these services have had years of engineering work already done for you in the Java world. We?l take a look at two different strategies for dealing with long running tasks.

스레드 풀

The first strategy is to leverage managed thread pools in the JavaEE container. When your web application is running within Glassfish, each HTTP request is processed by the HTTP Service, which contains a threadpool. You can change the number of threads to affect the performance of the webserver. Glassfish will also let you create your own threadpools to execute arbitrary work units for you.

The basic API for threadpools is simple:

  • WorkManager, which provides an abstracted interface to the thread pool.
  • Work is an interface, which encapsulates your unit of work.
  • WorkListener, which is an interface that lets you monitor the progress of your Work tasks.

First, we need to tell Glassfish to provision a threadpool for our use. In the Administration screen, go down to Configuration/Thread Pools. Click on ?ew?to create a new thread pool. Give your threadpool the name ?ackend-workers.?Leave all the other settings as the default values and click ?K.?

You?e now got a thread pool that you can use. The threadpool exposes an interface where you can submit jobs to the pool and the pool will either execute the job synchronously within a thread, or you can schedule the job to run asynchronously. As long as your unit of work implements the javax.resource.spi.work.Work interface, the threadpool will happily run your code. A WorkUnit class may be as simple as Listing 14-40.

Listing 14-40. Implementing a WorkUnit Class

from javax.resource.spi.work import Work

class WorkUnit(Work):
    """
    This is an implementation of the Work interface.
    """
    def __init__(self, job_id):
        self.job_id = job_id

    def release(self):
        """
        This method is invoked by the threadpool to tell threads
        to abort the execution of a unit of work.
        """
        logger.warn("[%d] Glassfish asked the job to stop quickly" % self.job_id)

    def run(self):
        """
        This method is invoked by the threadpool when work is
        'running'
        """
        for i in range(20):
            logger.info("[%d] just doing some work" % self.job_id)

This WorkUnit class doesn? do anything very interesting, but it does illustrate the basic structure of what unit of work requires. We?e just logging message to disk so that we can visually see the thread execute.

WorkManager implements several methods that can run your job and block until the threadpool completes your work, or it can run the job asynchronously. Generally, we prefer to run things asynchronously and simply check the status of the work over time. This lets me submit multiple jobs to the threadpool at once and check the status of each of the jobs.

To monitor the progress of work, we need to implement the WorkListener interface. This interface gives us notifications as a task progresses through the three phases of execution within the thread pool. Those states are:

  • Accepted
  • Started
  • Completed

All jobs must go to either Completed or Rejected states. The simplest thing to do then is to simply build up lists capturing the events. When the length of the completed and the rejected lists together are the same as the number of jobs we submitted, we know that we are done. By using lists instead of simple counters, we can inspect the work objects in much more detail. Listing 14-41 shows the code for our SimpleWorkListener.

Listing 14-41. Writing SimpleWorkListener Code

from javax.resource.spi.work import WorkListener
class SimpleWorkListener(WorkListener):
    """
    Just keep track of all work events as they come in
    """
    def __init__(self):
        self.accepted = []
        self.completed = []
        self.rejected = []
        self.started = []

    def workAccepted(self, work_event):
        self.accepted.append(work_event.getWork())
        logger.info("Work accepted %s" % str(work_event.getWork()))

    def workCompleted(self, work_event):
        self.completed.append(work_event.getWork())
        logger.info("Work completed %s" % str(work_event.getWork()))

    def workRejected(self, work_event):
        self.rejected.append(work_event.getWork())
        logger.info("Work rejected %s" % str(work_event.getWork()))

    def workStarted(self, work_event):
        self.started.append(work_event.getWork())
        logger.info("Work started %s" % str(work_event.getWork()))

To access the threadpool, you simply need to know the name of the pool we want to access and schedule our jobs. Each time we schedule a unit of work, we need to tell the pool how long to wait until we timeout the job. We also need to provide a reference to the WorkListener object so that we can monitor the status of the jobs.

The code to do this is shown in Listing 14-42.

Listing 14-42. Dealing with the Threadpool

from com.sun.enterprise.connectors.work import CommonWorkManager
from javax.resource.spi.work import Work, WorkManager, WorkListener
wm = CommonWorkManager('backend-workers')
listener = SimpleWorkListener()
for i in range(5):
    work = WorkUnit(i)
    wm.scheduleWork(work, -1, None, listener)

You may notice that the scheduleWork method takes in a None constant in the third argument. This is the execution context?or our purposes, it? best to just ignore it and set it to None. The scheduleWork method will return immediately and the listener will get callback notifications as our work objects pass through. To verify that all our jobs have completed (or rejected), we simply need to check the listener? internal lists. See Listing 14-43.

Listing 14-43. Checking the Listener? Internal Lists

while len(listener.completed) + len(listener.rejected) < num_jobs:
    logger.info("Found %d jobs completed" % len(listener.completed))
    time.sleep(0.1)

That covers all the code you need to access thread pools and monitor the status of each unit of work. Ignoring the actual WorkUnit class, the actual code to manage the threadpool is about a dozen lines long.

Note

불행하게도, 이 API는 JavaEE 5 명세에서는 아직 표준이 아니며 글래스피쉬에서만 동작한다. 병렬 처리를 위한 API는 JavaEE 6에서 표준이 될 것이며 그때까지는 스레드풀이 작동하도록 하기 위해서는 애플리케이션 서버의 내부에 대한 지식이 필요하다. 웹로직 또는 웹스피어를 사용한다면 스레드풀에 접근하기 위해 CommonJ API를 사용할 필요가 있으나, 로직은 대체로 같다.

프로세스 간 통신

스레드풀을 통해 백그라운드 작업 처리에 접근할 수는 있지만, 프로세스의 경계를 넘어서 메시지를 전달하는 것이 이로울 때도 있다. 이 문제를 풀기 위해 사흘이 멀다하고 새로운 파이썬 꾸러미가 나오고 있는데, 자이썬에서는 운 좋게도 자바의 JMS를 활용할 수 있다. JMS는 발행/구독 또는 점대점(point to point) 메시지 전달을 정의할 수 있는 메시지 중개 기법이다. 메시지는 결합도 감소, 메시지 전달 보증, 보안, 서버 및 클러스터 장애에 대한 내성과 같이 그다지 재미 없는 기술적인 요구를 처리하기 위해 비동기적으로 보내어진다.

소소한 RESTful 메시징 구현을 사용할 수도 있지만, OpenMQ과 JMS는 많은 장점을 가지고 있다.

  • 성숙도

    여러분이 직접 구현한 메시지 전달 방법이 어떠한 상황에서도 올바로 동작할 것이라고 확신할 수 있는가?서버 장애? 네트워크 연결 오류?신뢰성 보장?클러스터링?보안? OpenMQ는 거의 10년 동안 다듬어져왔다.

  • 표준화

    JMS는 표준 그 자체이다. 어떠한 JavaEE 코드에서도 메시지를 주고받을 수 있다.

  • 상호운용성

    JMS는 유일한 메시지 중개자가 아니다. 스트리밍 텍스트 기반 메시징 프로토콜(STOMP)은 자바를 사용하지 않는 개발자들 사이에 인기있는 또 다른 표준이다. stompconnect를 사용하여 JMS 중개자를 STOMP 중개자로 바꿀 수 있다. 이것은 수많은 언어 중 어떠한 것을 사용하는지, 어떠한 메시징 클라이언트와 어떠한 메시지 브로커를 사용하는지에 관계 없이 그것들 사이에서 메시지를 전달할 수 있음을 뜻한다.

JMS에는 두 가지 유형의 메시지 배달 방식이 있다.

  • 발행/구독

    이것은 현재 발생하고 있는 이벤트에 대하여 하나 이상의 구독자에게 메시지를 보내고자 할 때 사용한다. 이것은 JMS 토픽을 통해 이루어진다.

  • 점대점 전송

    이것은 단일한 송신자와 수신자 사이의 메시지 대기열이다. JMS가 이러한 대기열을 적절히 제어한다.

우리는 JMS를 위해 글래스피쉬의 두 개체를 provision할 필요가 있다. 간단히 말하자면, 클라이언트가 JMS 중개자에 연결하기 위해 사용할 연결 공장(factory)을 생성해야 한다. 발행/구독 자원 및 점대점 메시지 대기열을 생성할 것이다. JMS의 용어로는, 이것들을 목적지라고 부른다. 편지를 받아보는 우편함 같은 것이라고 생각할 수 있다. 글래스피쉬 관리 화면의 Resources/JMS Resources/Connection Factories로 이동한다. JNDI Name에 jms/MyConnectionFactory를 입력한다. Resource Type으로는 javax.jms.ConnectionFactory를 선택한다. 화면 하단의 사용자 및 패스워드를 지운 다음 그림 14-13과 같이 imqDisableSetClientID의 값을 'false'로 하는 속성을 추가한다. OK를 누른다.

images/14-13.png

Figure 14-13. 글래스피쉬 관리 콘솔에서의 Connection Factory 속성

imqDisableSetClientID를 false로 설정함으로써, 클라이언트가 ConnectionFactory를 사용할 때 사용자명과 패스워드를 선언하도록 강제한다. OpenMQ는 JMS 서비스의 클라이언트를 식별하여 각 목적지에 메시지를 올바로 보낼 수 있도록 하기 위해 로그인을 사용한다. 이제 우리는 실제 목적지를 생성하여 발행/구독할 주제 및 점대점 메시징을 위한 대기열을 생성해야 한다 Resources/JMS Resources/Destination Resources로 이동하여 new를 누른다. JNDI를 설정하다 이름 jms은 / MyTopic, MyTopic의 대상 이름과 javax.jms 될 수있는 리소스 유형이다.주제 그림 14-14에 표시된다. 항목을 저장하려면 확인을 누른다.

images/14-14.png

Figure 14-14. Adding a New JMS Topic Resource

Now we need to create the JMS queue for point to point messages. Create a new resource, set the JNDI name to ?ms/MyQueue.?the destination name to ?yQueue,?and the resource type to ?avax.jms.Queue,?as shown in Figure 14-15. Click ?K?to save.

images/14-15.png

Figure 14-15. Adding a New JMS Queue Resource

Like the database connections discussed earlier, the JMS services are also acquired in the JavaEE container through the use of JNDI name lookups. Unlike the database code, we?e going to have to do some manual work to acquire the naming context which we do our lookups against. When our application is running inside of Glassfish, acquiring a context is very simple. We just import the class and instantiate it. The context provides a lookup() method which we use to acquire the JMS connection factory and get access to the particular destinations that we are interested in. In Listing 14-44, we?l publish a message onto our topic. Let? see some code first and we?l go over the finer details of what? going on.

Listing 14-44. Context for Creating a Text Message

from javax.naming import InitialContext, Session
from javax.naming import DeliverMode, Message
context = InitialContext()

tfactory = context.lookup("jms/MyConnectionFactory")

tconnection = tfactory.createTopicConnection('senduser', 'sendpass')
tsession = tconnection.createTopicSession(False, Session.AUTO_ACKNOWLEDGE)
publisher = tsession.createPublisher(context.lookup("jms/MyTopic"))

message = tsession.createTextMessage()
msg = "Hello there : %s" % datetime.datetime.now()
message.setText(msg)
publisher.publish(message, DeliveryMode.PERSISTENT,
        Message.DEFAULT_PRIORITY, 100)
tconnection.close()
context.close()

In this code snippet, we acquire a topic connection through the connection factory. To reiterate, topics are for publish/subscribe scenarios. Next, we create a topic session, a context where we can send and receive messages. The two arguments passed when creating the topic session specify a transactional flag and how our client will acknowledge receipt of messages. We?e going to just disable transactions and get the session to automatically send acknowledgements back to the broker on message receipt. The last step to getting our publisher is creating the publisher itself. From there we can start publishing messages up to the broker. At this point, it is important to distinguish between persistent messages and durable messages. JMS calls a message ?ersistent?if the messages received by the broker are persisted. This guarantees that senders know that the broker has received a message. It makes no guarantee that messages will actually be delivered to a final recipient. Durable subscribers are guaranteed to receive messages in the case that they temporarily drop their connection to the broker and reconnect at a later time. The JMS broker will uniquely identify subscriber clients with a combination of the client ID, username and password to uniquely identify clients and manage message queues for each client. Now we need to create the subscriber client. We?e going to write a standalone client to show that your code doesn? have to live in the application server to receive messages. The only trick we?e going to apply here is that while we can simply create an InitialContext with an empty constructor for code in the app server, code that exists outside of the application server must know where to find the JNDI naming service. Glassfish exposes the naming service via CORBA, the Common Object Request Broker Architecture. In short, we need to know a factory class name to create the context and we need to know the URL of where the object request broker is located. The listener client shown in Listing 14-45 can be run on the same host as the Glassfish server.

Listing 14-45. Creating a Subscriber Client for JMS

"""
This is a standalone client that listens for messages from JMS
"""
from javax.jms import TopicConnectionFactory, MessageListener, Session
from javax.naming import InitialContext, Context
import time

def get_context():
    props = {}
    props[Context.INITIAL_CONTEXT_FACTORY]="com.sun.appserv.naming.S1ASCtxFactory"
    props[Context.PROVIDER_URL]="iiop://127.0.0.1:3700"
    context = InitialContext(props)
    return context

class TopicListener(MessageListener):
    def go(self):
        context = get_context()
        tfactory = context.lookup("jms/MyConnectionFactory")
        tconnection = tfactory.createTopicConnection('recvuser', 'recvpass')
        tsession = tconnection.createTopicSession(False, Session.AUTO_ACKNOWLEDGE)
        subscriber = tsession.createDurableSubscriber(context.lookup("jms/MyTopic"), 'mysub')
        subscriber.setMessageListener(self)
        tconnection.start()
        while True:
            time.sleep(1)
        context.close()
        tconnection.close()

    def onMessage(self, message):
        print message.getText()

if __name__ == '__main__':
    TopicListener().go()

There are only a few key differences between the subscriber and publisher side of a JMS topic. First, the subscriber is created with a unique client id?n this case, it? ?ysub.?This is used by JMS to determine what pending messages to send to the client in the case that the client drops the JMS connections and rebinds at a later time. If we don? care to receive missed messages, we could have created a non-durable subscriber with ?reateSubscriber?instead of ?reateDurableSubscriber?and we would not have to pass in a client ID. Second, the listener employs a callback pattern for incoming messages. When a message is received, the onMessage will be called automatically by the subscriber object and the message object will be passed in. Now we need to create our sending user and receiving user on the broker. Drop to the command line and go to GLASSFISH_HOME/imq/bin. We are going to create two users: one sender and one receiver. See Listing 14-46.

Listing 14-46. Creating Two JMS Users

GLASSFISH_HOME/imq/bin $ imqusermgr add -u senduser -p sendpass
User repository for broker instance: imqbroker
User senduser successfully added.

GLASSFISH_HOME/imq/bin $ imqusermgr add -u recvuser -p recvpass
User repository for broker instance: imqbroker
User recvuser successfully added.

We now have two new users with username/password pairs of senduser/sendpass and recvuser/recvpass. You have enough code now to enable publish/subscribe messaging patterns in your code to signal applications that live outside of your application server. We can potentially have multiple listeners attached to the JMS broker and JMS will make sure that all subscribers get messages in a reliable way. Let? take a look now at sending message through a queue: this provides reliable point to point messaging and it adds guarantees that messages are persisted in a safe manner to safeguard against server crashes. This time, we?l build our send and receive clients as individual standalone clients that communicate with the JMS broker. See Listing 14-47.

Listing 14-47. Sending Messages Through a Queue

from javax.jms import Session
from javax.naming import InitialContext, Context
import time

def get_context():
    props = {}
    props[Context.INITIAL_CONTEXT_FACTORY]="com.sun.appserv.naming.S1ASCtxFactory"
    props[Context.PROVIDER_URL]="iiop://127.0.0.1:3700"
    context = InitialContext(props)
    return context

def send():
    context = get_context()
    qfactory = context.lookup("jms/MyConnectionFactory")
    # This assumes a user has been provisioned on the broker with
    # username/password of 'senduser/sendpass'
    qconnection = qfactory.createQueueConnection('senduser', 'sendpass')
    qsession = qconnection.createQueueSession(False, Session.AUTO_ACKNOWLEDGE)
    qsender = qsession.createSender(context.lookup("jms/MyQueue"))
    msg = qsession.createTextMessage()
    for i in range(20):
        msg.setText('this is msg [%d]' % i)
        qsender.send(msg)

def recv():
    context = get_context()
    qfactory = context.lookup("jms/MyConnectionFactory")
    # This assumes a user has been provisioned on the broker with
    # username/password of 'recvuser/recvpass'
    qconnection = qfactory.createQueueConnection('recvuser', 'recvpass')
    qsession = qconnection.createQueueSession(False, Session.AUTO_ACKNOWLEDGE)
    qreceiver = qsession.createReceiver(context.lookup("jms/MyQueue"))
    qconnection.start()  # start the receiver

    print "Starting to receive messages now:"
    while True:
        msg = qreceiver.receive(1)
        if msg is not None and isinstance(msg, TextMessage):
            print msg.getText()

send()와 recv() 함수는 주제를 관리하기 위한 발행/구독 코드와 거의 같다. 작은 차이점은 JMS 대기열 API는 메시지 전달 확인을 위해 콜백 개체를 사용하지 않는다는 것이다. 클라이언트 애플리케이션은 수동적인 구독자라기보다는, JMS 대기열에서 개체를 능동적으로 제거할 것이라고 전제한다. 이 JMS 코드는 메시지를 중개자에게 보내고 나서 서버에 문제가 생기더라도 메시지를 잃어버리지 않는다는 점에서 아름답다. 서버가 살아나서 상대편 클라이언트와 다시 접속되면, 밀려있던 메시지의 수신이 재개될 것이다. 우리는 이 예제를 더 발전시킬 수 있다. 앞에서 언급했듯이, Codehaus.org에는 문자열 흐름에 기반한 메시지 전송 규약(Streaming Text Orientated Messaging Protocol)의 줄임말인 STOMP라고 하는 메시지 프로젝트가 있다. STOMP는 JMS 메시지에 비해 단순한 반면 성능이 떨어지기는 하지만, 수많은 언어로 구현한 클라이언트가 존재한다는 장점이 있다. 또한 STOMP는 JMS 중개자를 STOMP 메시지 중개자로 바꾸어주는 stomp-connect라고 하는 어댑터도 제공한다. 이것은 우리의 애플리케이션이, 어떠한 언어로 쓰여진 애플리케이션과도 JMS를 통하여 통신할 수 있도록 해준다. 자이썬이나 자바에서 지원하지 않는 계산 처리를 위해, 다양한 C 라이브러리를 활용할 수 있는 Imagemagick 혹은 NumPy와 같은 기존의 CPython 코드를 사용하던 때도 있었다. stompconnect를 사용하여, 우리는 JMS를 통하여 작업 메시지를 보내고, 이러한 메시지들은 STOMP를 거쳐 CPython 클라이언트 프로세스가 요청을 처리하도록 할 수 있다. 완료된 작업은 다시 STOMP를 통해 보내어지고, JMS를 통해 자이썬 코드에서 받아볼 수 있다. 먼저, stomp-connect의 최신판을 codehaus.org에서 구하도록 하라. 이곳에서 stompconnect-1.0.zip를 내려받을 수 있다.

http://stomp.codehaus.org/Download

zip 파일을 푼 다음에는, STOMP가 JMS 클라이언트처럼 작동하도록 JNDI 속성 파일을 구성해야 한다. 그 구성은 우리의 자이썬 클라이언트와 동일하다. 여러분의 stompconnect 디렉토리에 jndi.properties라는 파일을 생성하도록 하자. 예제 14-48과 같은 두 줄이 있어야 한다.

예제 14-48. Jndi.properties Lines

java.naming.factory.initial=com.sun.appserv.naming.S1ASCtxFactory
java.naming.provider.url=iiop://127.0.0.1:3700

이제 여러분은 STOMP에서 필요로하는 JNDI, JMS 및 몇몇 로깅 클래스에 접근하기 위해 글래스피쉬에서 JAR 파일을 끌어내어야 한다. 다음의 JAR 파일들을 GLASSFISH_HOME/lib에서 STOMPCONNECT_HOME/lib으로 복사하도록 하자.

  • appserv-admin.jar
  • appserv-deployment-client.jar
  • appserv-ext.jar
  • appserv-rt.jar
  • j2ee.jar
  • javaee.jar

GLASSFISH_HOME/imq/lib/imqjmsra.jar 파일도 STOMPCONNECT_HOME/lib으로 복사한다.

이제 다음의 명령으로 접속자(connector)를 시작할 수 있다.

java -cp "lib\*;stompconnect-1.0.jar" \
    org.codehaus.stomp.jms.Main tcp://0.0.0.0:6666 \
    "jms/MyConnectionFactory"

올바로 작동한다면, 서버가 tcp://0.0.0.0:6666으로의 접속을 기다린다는 메시지로 끝나는 길다란 출력을 보게 될 것이다. 축하한다. 여러분은 OpenMQ JMS 중개자를 위한 양방향 프록시의 역할을 하는 STOMP 중개자를 갖게 되었다. 자이썬+JMS로부터 발생한 메시지를 CPython에서 수신하는 것은 예제 14-49와 같이 단순하다.

예제 14-49. STOMP 중개자를 통해 메시지를 수신

import stomp
serv = stomp.Stomp('localhost', 6666)
serv.connect({'client-id': 'reader_client', \
                  'login': 'recvuser', \
               'passcode': 'recvpass'})
serv.subscribe({'destination': '/queue/MyQueue', 'ack': 'client'})
frame = self.serv.receive_frame()
if frame.command == 'MESSAGE':
    # The message content will arrive in the STOMP frame's
    # body section
    print frame.body
    serv.ack(frame)

이렇듯 메시지를 보내는 것은 수월하다. CPython 코드에서는, stomp 클라이언트를 들여와서 자이썬 코드 쪽으로 메시지를 돌려보내주면 된다. 예제 14-50을 보자.

예제 14-50. STOMP 중개자를 통해 메시지를 전송

import stomp
serv = stomp.Stomp('localhost', 6666)
serv.connect({'client-id': 'sending_client', \
                  'login': 'senduser', \
               'passcode': 'sendpass'})
serv.send({'destination': '/queue/MyQueue', 'body': 'Hello world!'})

요약

여기에 많은 지면을 할애했다. 우리는 자이썬에서 장고를 구성하여 애플리케이션에서 데이터베이스 자원의 사용을 제한하기 위하여 접속 풀링을 사용하는 방법을 살펴보았다. JMS 대기열의 구성에 대하여, 그리고 자이썬 프로세스 간의 점대점 및 메시지의 발행/구독에 대하여 살펴보았다. 그런 다음에는 메시징 서비스를 이용하여 자이썬 코드와 자바가 아닌 코드를 함께 운용하는 방법을 살펴보았다. 필요한 기술을 섞어서 사용함으로해서 자이썬은 매우 강력해진다. 여러분은 수년의 경험을 바탕으로 결집된 JavaEE의 기술력과, 장고와 같이 보다 가볍고 현대적인 웹 애플리케이션 계층의 장점을 모두 취할 수 있다. 앞으로 애플리케이션 서버에서 자이썬 및 장고에 대한 지원은 전망이 아주 밝다. 웹스피어는 자이썬을 공식 스크립트 언어로 채택하였으며, 글래스피쉬 3판에서는 장고 애플리케이션에 대하여 우선적으로 지원한다. 여러분은 WAR 파일을 구축하지 않고도 웹 애플리케이션을 배포할 수 있다. 원본 디렉터리로부터 바로 배포해버리면 경기 끝이다.