4. Photo 모델로 Admin 영역에서 데이터 다루기

이번 편에서는 Django framework이 제공하는 Admin 기능을 이용하여 Photo 모델로 데이터를 추가하거나 내용을 고치거나 삭제해 보겠습니다.

1. Photo 모델로 데이터 넣기

(1) Admin에서 Photo 모델에 데이터 넣기

Photo 모델을 이용하여 데이터베이스를 넣겠습니다. View에 관련 기능을 구현해도 되지만, Django의 장점 중 하나인 Admin 기능을 이용해서 자료를 관리해 보겠습니다. photos 앱에 있는 admin.py 파일에 관련 코드를 작성해 넣으면 됩니다.

from django.contrib import admin

from .models import Photo


admin.site.register(Photo)

Django framework에는 Admin 기능이 admin이라는 형태로 제공되는데, contrib 패키지 안에 admin 패키지로 존재합니다. admin.site.registeradmin 패키지에 있는 sites 모듈에서 AdminSite 클래스를 site라는 이름을 갖는 인스턴스로 만들고, 이 site 객체의 인스턴스 메서드인 register로 지정한 모델을 Admin 영역에서 관리하도록 등록합니다.

photo앱의 admin.py를 저장하고 나면 Django의 개발용 내장 웹서버(이하 내장 웹서버)가 자동으로 재실행 됩니다. 재실행이 되고 나면 웹 브라우저에서 http://127.0.0.1:8000/admin/로 접속해 보세요. 로그인에 필요한 ID와 비밀번호를 묻는데, 지난 3편에서 manage.py로 만든 계정 정보로 접속하면 됩니다. 비밀번호가 기억이 나질 않는다면 manage.pychangepassword 명령어로 비밀번호를 새로 생성하면 됩니다.

로그인을 했다면 PHOTOS라는 영역이 있고 그 아래에 Photos라는 항목이 보입니다. 그 항목이 바로 Photo 모델입니다. Photo 항목 오른쪽에 Add를 눌러보세요. Photo 모델에 데이터를 넣는 Form이 나타납니다.

어차피 싹 지우고 다시 데이터는 채워 넣을 거니까 아무 자료나 넣어보세요. 사진이 아닌 파일도 지정해보고 본문(content) 입력란에 아무 내용도 넣지 말고 저장도 해보세요. 또 본문 입력란에 500글자가 넘는 글자를 넣어 보세요. 우리가 뭔가 따로 조치를 취한 게 없는데도 파일이 이미지 파일인지 아닌지, 본문이 채워져 있는지를 검사하고 본문에 500자 이상 입력이 안 되게 제한됩니다.

Django의 forms 기능(패키지)이 이런 처리를 하며, 이미지 파일이어야 하고 본문은 반드시 내용이 있어야 한다거나 본문 길이와 같은 검사 항목과 정보를 우리가 만든 Photo 모델에서 참조합니다. image 모델 속성을 ImageField라는 필드 타입으로 지정해서 업로드 되는 파일이 이미지 파일인지 검사하는 것이며, content 모델 속성을 최대 길이 500자로 지정한 TextField 필드 타입으로 지정해서 문자열 길이가 500자 이하인지 검사합니다. 생성일시인 created_at은 자동으로 값이 저장되는 옵션을 주어서 입력란으로 등장하지 않았습니다.

몇 가지 실험해보죠. photos 앱의 Photo 모델에서 created_at을 고치겠습니다. auto_now_addauto_now 필드 옵션을 모두 제거하겠습니다. 그리고 content의 필드 타입에 blank라는 필드 옵션을 True로 추가 지정하겠습니다. 코드로 보면 이렇습니다.

class Photo(models.Model):
    image = models.ImageField()
    filtered_image = models.ImageField()
    content = models.TextField(max_length=500, blank=True)
    created_at = models.DateTimeField()

모델 모듈(파일)을 저장하여 내장 웹서버가 재실행되게 한 후, Photo 모델에 데이터를 추가하는 입력란 영역으로 다시 가보거나 열어보세요. Created at이라는 입력란이 추가 됐습니다. 이제 Save 버튼을 눌러보세요.

뭔가 달라졌지요? 본문란에 아무 내용을 넣지 않았는데도 무섭게 시뻘건 경고 안내가 나타나지 않습니다. 그리고, 생성일시 정보를 넣지 않았다고 경고합니다.

blank 필드 옵션은 이름 그대로 빈칸을 뜻합니다. 즉 blank=True는 빈칸을 허용하겠다는 뜻입니다. 이와 비슷한 옵션으로 null이 있는데, null은 Python의 None 자료형 객체를 뜻합니다. null=TrueNone 자료형을 허용하겠다는 뜻입니다. 빈칸과 None(null)은 의미가 완전히 다른데, 빈칸은 내용이 비어있는 문자형 객체입니다. 데이터베이스의 테이블 구성(schema)도 전혀 달라서, null=True이라고 하면 해당 컬럼(column)은 NULL을 허용하도록 지정되고, blank=True만 있으면 null=True가 없어서 기본값인 null=False로 지정되어 데이터베이스 테이블의 컬럼도 NULL이 허용되지 않는 NOT NULL로 지정됩니다. 그래서 contentblank=True 옵션만 설정한 상태에서 빈칸인 문자형 객체 조차 넣지 않으면 데이터베이스에 자료를 넣는 중에 오류가 발생합니다. 정리하면 null=True는 데이터베이스 테이블에 대한 것, blank=True는 Django Form에 대한 설정입니다.

사진 게시물 본문을 꼭 넣지 않아도 되도록 변경하겠습니다.

class Photo(models.Model):
    image = models.ImageField()
    filtered_image = models.ImageField()
    content = models.TextField(max_length=500, null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

데이터베이스에 반영하는 방법은 manage.pymakemigrationsmigrate 명령어를 이용하면 됩니다.

$ python manage.py makemigrations
Migrations for 'photos':
  photos/migrations/0002_auto_20170129_1211.py:
    - Alter field content on photo

0001은 Photo 모델을 처음 데이터베이스에 반영할 때 만들었으니 0002라는 일련번호가 붙은 마이그레이션 파일이 생성됩니다. photocontent 필드를 변경(alter)하는 내용이라고 나오네요. python manage.py migrate를 실행하면 makemigrations으로 만들어진 마이그레이션 파일을 실제로 반영합니다. 번거롭게 데이터베이스 테이블을 우리가 변경하지 않아도 되니 참 편합니다.

자, Admin 영역에서 이제 실제로 이미지 파일을 지정하여 Photo 모델에 데이터를 실제로 넣어 보세요.

(2) 파일 업로드 경로 지정

Photo 모델에 데이터를 추가하면 업로드한 이미지 파일은 manage.py 파일이 있는 곳에 저장됩니다.

관리하기 편하게 업로드 되는 파일을 uploads/연도/월/일/종류에 저장하겠습니다. Photo 모델에서 ImageField 필드 타입에 필드 옵션인 upload_to를 이용하면 됩니다. 코드부터 보지요.

image = models.ImageField(upload_to='uploads/%Y/%m/%d/orig')
filtered_image = models.ImageField(upload_to='uploads/%Y/%m/%d/filtered')

위와 같이 Photo 모델을 고쳐서 저장한 후 Admin 영역에서 Photo 모델에 데이터를 추가해 보세요. 그리고 파일이 저장된 경로를 확인해 보세요. 파일이 upload_to로 지정한 경로에 저장됩니다.

경로에 저장하는 연도, 월, 일이 포함되는 것도 확인하셨나요? %Y, %m, %d가 그런 역할을 하는데, 이 문자열은 Python의 strftime의 포맷팅(formatting)에 사용되는 형태잡기 문자열(format string) 중에서 날짜와 시간과 같은 규칙을 따릅니다.

모델 필드 내용이 바뀌었으니 마이그레이션을 수행합니다.

업로드 경로를 중간에 변경해도 괜찮을까?

여기서 잠깐. 우리는 중간 중간 업로드 경로를 바꾸면서 이미지 파일을 업로드 했습니다. 이러면 혹시 이전 업로드 경로로 올린 이미지 파일에 접근하지 못하는 문제가 발생하지 않을까요? 발생하지 않습니다. upload_to는 업로드 된 파일을 지정한 경로에 저장할 때 참조합니다. 그래서 해당 데이터 객체의 경로는 이전 업로드 경로를 포함하여 지정됩니다.

(3) 첨부 파일 삭제하기

혹시 Admin 영역에서 추가한 Photo 모델의 객체를 지워보셨나요? Admin 영역에서는 모델 객체를 추가하는 것 뿐만 아니라 기존 모델 객체를 수정하거나 지우는 기능을 기본 제공합니다. 한 번 모델 객체를 지워 보세요.

이상한 점 발견하셨나요? 모델 객체를 지우면 객체 자체는 지워지는데 그 객체에 연결된 파일들, 그러니까 업로드한 두 개 파일은 지워지지 않고 여전히 남아 있습니다. Django의 모델 기능은 모델 객체가 삭제되어도 그 모델 객체의 파일 필드에 연결된 파일을 지우지 않습니다. 그래서 삭제할 모델 객체를 먼져 가져와서 연결된 파일을 일일이 지워준 후에 모델 객체를 지워야 합니다.

모델 객체가 삭제될 때 그 모델 객체에 연결된 파일도 자동으로 함께 지우는 기능은 따로 구현해야 합니다. 몇 가지 방법이 있습니다.

  1. 모델을 삭제하는 기능이 호출되면 파일 삭제 기능도 실행
  2. 모델이 삭제되는 신호가 감지되면 파일 삭제 기능도 실행

2번은 나중에 알아보기로 하고, 이번 편에서는 1번 방법을 구현해 보겠습니다.

Django framework은 delete라는 인스턴스 메서드를 호출하여 모델 객체를 지웁니다. Admin 영역에 있는 삭제 기능도 이 메서드를 호출하는 겁니다. 이 메서드는 Model 클래스에 정의되어 있습니다. 우리가 Django 모델을 만들 때 클래스에 models.Model을 상속받도록 지정했기 때문에 우리가 만든 모델에 delete 메서드를 따로 만들지 않아도 됐던 것이지요. 그렇다면 우리가 만든 모델에 delete 인스턴스 메서드를 만들고 이 메서드가 호출되면 업로드 파일을 지우고 나서 모델 객체를 지우는 원래 delete 메서드 기능을 수행하면 되겠군요. 그런 기능을 구현한 코드부터 보겠습니다.

class Photo(models.Model):
    # 중략
    
    def delete(self, *args, **kwargs):
        self.image.delete()
        self.filtered_image.delete()
        super(Photo, self).delete(*args, **kwargs)

먼저 def delete(self, *args, **kwargs):는 특별한 내용은 없습니다. delete 함수는 인스턴스 메서드이므로 첫 번째 인자로 객체 자신을 self라는 이름으로 넘겨 받습니다. *args**kwargs는 함수가 넘겨받는 인자를 미리 알지 못하는 경우에 함수가 넘겨받는 인자를 담는 객체입니다. delete 메서드로 뭘 인자로 넘길 지는 모르겠지만 어쨌든 넘겨받은 그대로 Model클래스의 delete 메서드로 넘겨줘야 해서 저렇게 받습니다.

self.image.delete()에서 self.imageimage 모델 필드를 뜻합니다. Python 클래스의 인스턴스 메서드 안에서 속성(attribute)에 접근하려면 self.속성이름으로 접근하지요. selfdelete 인스턴스 메서드에서 첫 번째 인자로 넘겨 받았고요. 인스턴스 밖에서 접근하려면 photo.image 이렇게 접근하겠고요. 이 image 모델 필드는 Django의 모델 필드인 ImageField 클래스의 인스턴스입니다. ImageField 클래스로 만든 인스턴스는 delete라는 인스턴스 메서드를 제공하며, 이름에서 알 수 있듯이 해당 모델 필드에 연결된 파일을 삭제합니다. self.filtered_image.delete()는 무슨 코드인지 예측되지요? 필터가 적용된 이미지 파일을 지우는 겁니다.

맨 마지막 줄인 super(Photo, self).delete(*args, **kwargs)Photo 모델이 상속받은 부모 클래스의 delete 인스턴스 메서드를 호출합니다. 넘겨받은 인자를 그대로 전달하려고 *args, **kwargs로 인자를 보내지요. 이 코드가 없으면 첨부된 업로드 파일만 삭제되고 모델 객체는 삭제되지 않습니다. 모델 객체를 지우는 건 Model 클래스에 있는 delete 메서드거든요. 만약 Model 클래스의 delete 메서드를 사용해서 모델 객체를 삭제하지 않고 여러분이 독자 구현한 코드로 모델 객체를 지우고자 한다면 super(...) 이 부분을 지우고 직접 구현하면 됩니다.

2. 부록

(1) Django Admin 주소

Django에서 제공하는 Admin 기능은 settings.py에 설정되어 있습니다. INSTALLED_APPS라는 변수를 찾아 보시면 django.contrib.admin이라는 줄이 보입니다. 우리가 만든 photos 앱도 이곳에 추가했지요.

그럼 http://127.0.0.1:8000/admin 주소(URL)에서 admin 부분도 어딘가에 미리 설정되어 있는 걸까요? 맞습니다. urls.py에 기본으로 설정되어 있습니다. pystagram 패키지(디렉터리)에 있는 urls.py을 열어 보시면 url(r'^admin/', include(admin.site.urls)),이라는 내용이 보일 겁니다. 경로 맨 앞에 admin이 있는 모든 경로를 admin.site.urls에 설정되어 있는 경로에 연결(matching)하겠다는 내용입니다. 이건 django.contrib.admin 패키지에서 sites.py 파일에 보면 AdminSite 클래스가 있는데, 그 클래스의 get_urls라는 인스턴스 메서드를 호출하는 겁니다. 메서드를 프로퍼티화 하는 @property 장식자(decorator)를 이용하여 urls를 호출하면 get_urls 인스턴스 메서드가 반환하는 정보를 던져주는 것이지요.

http://127.0.0.1:8000/admin 이 주소 대신 /_admin으로 접근하고 싶다면 ^admin/ 부분을 ^_admin/으로 고치면 됩니다.

(2) Django Admin 필요성

Django Admin은 이용자가 꽤 유연하게 변경하도록 만들어져 있습니다. 서비스는 고객이 사용하는 제품부 뿐만 아니라 운영에 필요한 관리 영역을 만드는 데에도 상당한 노고가 필요한데, Django Admin을 쓰면 그런 노고가 줄어 듭니다. Django Admin은 그 자체만으로도 확장성 있게 잘 만들어져 있고, Django의 모델이나 미들웨어 체계와 강하게 연계되어 있어서 직접 구현하려면 번거로운 기능을 쉽고 편하게 구현하도록 합니다.

저는 이 강좌에서 Django Admin 부분만 따로 할당하지 않고, 그때 그때 필요한 내용을 설명하도록 하겠습니다.


이것으로 강좌 4편을 마칩니다. 늦어서 죄송합니다. 요즘 많이 바빠서 연재하기 힘드네요. ㅜㅜ 한 편에 너무 많은 내용을 담느라 연재 주기가 늘어지지 않도록 해보겠습니다.


3. Photo 앱과 모델 만들기

1. Django Project와 App

(1) 개념

Python 코드가 담긴 파일을 Python 모듈이라고 하며, Python package는 Python module을 묶어놓은 단위입니다. 파일 체계로 보면 디렉터리지요. Python 패키지는 반드시 초기화 모듈인 __init__.py이 필요합니다.

Django는 Django project 단위로 만드는데, Python 체계로 보면 Python 패키지를 뜻합니다. Django로 만드는 프로젝트에 사용되는 코드와 Django 설정값이 Python 모듈로 존재하고 모두를 포함하는 Python 패키지로 묶은 것이지요.

우리가 Pystagram 프로젝트를 Django로 만든다는 건 Pystagram이라는 Python 패키지를 만들고, Pystagram에 들어가는 기능은 Python 모듈로 만든다는 뜻입니다. 그럼 Django를 써서 Pystagram을 만들려면 먼저 Pystagram 디렉터리를 만들어야 겠지요. 이 디렉터리는 Python 패키지니까 초기화 파일인 __init__.py이 필요합니다. 그리고 Django framework이 참조할 프로젝트 설정 항목은 settings라는 모듈이므로 settings.py라는 파일로 필요합니다. 웹 주소(URL)로 서비스에 접근하므로 각 접근 주소에 연결될 기능을 설정하는 urls.py라는 파일도 필요합니다. 이 중에서 settings.py는 필수 모듈입니다.

(2) Django project 만들기

django-admin.py

Python 패키지인 Pystagram 디렉터리를 만들고, 여기에 필수 모듈인 settings.py__init__.py를 만드는 과정을 간편하게 처리하는 프로그램이 django-admin.py입니다. 이 파일로 Pystagram 프로젝트를 개설하겠습니다. 그 전에 지난 편에서 꾸려놓은 개발 환경을 먼저 써볼까요?

$ source env_pystagram/bin/activate

이번엔 Pystagram 프로젝트를 개설합니다. 여러분이 원하는 아무 곳(디렉터리)에 만들어도 됩니다. 저는 ~/Workspace 안에다 프로그래밍 프로젝트를 넣어두니 이 안에 만들겠습니다.

$ django-admin startproject pystagram

django-admin 프로그램(script file)에 첫 번째 인자로 startproject라는 명령어를 넣고 두 번째 인자에 만들 프로젝트 이름을 넣습니다. 그러면 두 번째 인자로 넣은 프로젝트 이름으로 디렉터리가 하나 만들어 집니다.

Django Project와 Python 패키지

앞으로 만들 Pystagram 소스 파일이나 각종 매체(media) 파일은 이곳에 담는데, 이 디렉터리 자체는 Python 패키지는 아닙니다. Python 패키지가 아니므로 Python으로 불러들일 수 없고(import), 그러므로 이 디렉터리 이름은 Pystagram으로 하든 HelloWorld로 하든 아무 상관 없습니다. 만약 Pystagram이라는 이름이나 기본 기능은 그대로 쓰지만, 몇 가지 실험성 기능을 넣어서 PystagramStory라는 걸 운영하고 싶다면 이 디렉터리를 복사하면 그만입니다. Django계에서 이 디렉터리를 뜻하는 별도 용어가 있진 않고 그냥 Django project를 포함하는 뿌리 디렉터리(root directory)라고 부릅니다.

실제로 Django project는 pystagram 디렉터리 안에 있는 pystagram이라는 디렉터리입니다. 그러니까 django-admin.py가 만든 디렉터리는 이렇게 구성됩니다.

pystagram/
├── manage.py
└── pystagram
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

pystagram 디렉터리 안에 있는 pystagram 디렉터리가 실제로 사용되는 Python 패키지입니다. 이 디렉터리는 Pystagram 프로젝트에서 사용할 시작 패키지라고 보면 됩니다.

manage.py와 개발용 내장 웹 서버

manage.py 파일은 Django로 돌아가는 프로젝트를 다양하게 다루는 도구입니다. Database를 만들거나 개발용 내장 웹서버로 우리가 만드는 프로젝트를 서버로 구동하는 기능 등 여러 편의 요소를 제공합니다. 말이 나온 김에 Django project가 잘 만들어졌는지 manage.py로 확인해 볼까요?

$ python manage.py runserver

(중략)
Django version 1.10.5, using settings 'pystagram.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

manage.py 파일에 첫 번째 인자로 runserver 명령을 주어 서버를 실행하자, 쉘 프롬프트가 사라지고 Django framework이 제공하는 개발용 내장 웹 서버가 실행된 상태로 대기하고 있습니다. pystagram package 안에 있는 settings 모듈을 참조하여 127.0.0.1 주소와 8000번 포트로 개발용 서버를 띄웠다는 내용, 그리고 Control+C 글쇠를 누르면 이 서버를 종료한다는 내용이 나와 있습니다.

Django 내장 개발용 웹서버로 띄운 서비스에 접속할 수 있는 주소를 알게 됐으니 웹 브라우저를 열어서 확인합시다. http://localhost:8000이나 http://127.0.0.1:8000로 접속하면 됩니다.

It worked!

우린 그냥 Django project를 생성한 것 뿐인데 깔끔하게 디자인 된 환영 페이지가 뜨네요. 저도 동참하겠습니다.

여러분, Django 웹 프레임워크 세상에 오신 걸 환영합니다.

내장 웹서버 접속 주소 바꾸기

아무 전달인자를 주지 않으면 내장 웹서버는 호스트는 127.0.0.1를, 포트는 8000를 기본으로 사용합니다. 이 주소를 바꾸려면 사용할 주소를 지정하면 됩니다.

$ python manage.py runserver 127.0.0.1:8080

(3) Pystagram Project 초기/사전 작업

django-admin.py로 Pystagram project를 만들고 나면 데이터베이스를 동기화하는 과정을 거칩니다. 데이터베이스를 전혀 사용하지 않는다면 생략하기도 합니다. 데이터 자체는 외부에서 매번 요청하여 가져오고, 이 데이터를 적절히 가공하여 바로 출력하면 굳이 데이터베이스를 쓰지 않아도 됩니다. 가령 REST API1 데이터를 JSON 형식으로 가져온 뒤 출력 양식만 바꾸거나 데이터에 접근하는 방식을 바꾸어 사용자 경험을 제공하는 식이지요.

하지만 Pystagram은 데이터베이스를 사용합니다. 또한 Django에서 제공하는 여러 미들웨어나 앱을 사용하는데, 이러한 도구도 데이터베이스를 씁니다. 그래서 동기화 과정이 필요합니다. 방법은 간단합니다.

$ python manage.py migrate

manage.pymigrate 명령어를 주면 Django framework에서 제공하는 도구가 사용하는 데이터베이스 관련 작업을 자동으로 진행합니다. 최고 권한 이용자도 만들겠습니다. 곧 필요하거든요.

$ python manage.py createsuperuser
Username (leave blank to use 'hannal'): hannal
Email address: 
Password:
Password (again):
Superuser created successfully.

비밀번호는 언제든지 changepassword 명령어로 바꿀 수 있습니다. 바꿀 대상(username)을 지정하면 되지요.

$ python manage.py changepassword hannal

이렇게 수행한 데이터베이스 작업은 db.sqlite3라는 파일에 저장됩니다. 이 파일은 sqlite3이라는 데이터베이스 엔진이 다루는 데이터베이스 파일입니다. Django framework은 따로 settings.py에 설정하지 않으면 기본으로 sqlite3 데이터베이스를 사용하도록 되어 있습니다. 서비스용으로는 쓰기엔 기능과 성능이 부족하지만, 가볍고 간단해서 개발용으로 쓰기엔 더할 나위없이 좋습니다. 더구나 Linux나 Mac OS는 운영체제에 기본 포함하며, Python도 sqlite의 데이터베이스를 다루는 API를 기본 내장하고 있습니다.

Django framework에서 제공하는 도구가 사용할 데이터베이스 관련 작업과 최고 권한 이용자를 만들었으니 이제 본격 Pystagram을 만들 차례입니다.

(4) Photo App 초기 작업

Django App

Pystagram 기획에서 가장 먼저 기획한 기능이 무엇일까요? 퀴즈 아니니 기억 안 나시면 기획 내용 보고 오셔도 됩니다. 제가 가장 먼저 기획한 기능은 사진 관련 기능이었습니다. 그 다음이 사용자와 회원 기능, 그 다음이 사진 모아보는 기능이었지요. 이 각각은 사진 올리기, 사진 보기와 같이 세부 기능이 묶여 있지요. 이렇게 목적을 가진 뭔가를 수행하는 애플리케이션(application)을 Django계에선 Django App이라고 부릅니다. Django project는 이러한 App이 하나 이상 조합물입니다.

보통은 Django App은 해당 App으로 분리된 Python 패키지 형식입니다. modelsviews 모듈과 같이 각 App에 필요한 모듈로 구성합니다. 필수 모듈은 아니지만, “어떤 목적을 수행하는 애플리케이션”이라는 정의를 따른다면 자연스레 이런 모듈이 필요하게 됩니다. 그리고 Django project와 마찬가지로 Django app도 자동화 도구로 편하게 생성할 수 있습니다.

manage.py로 Photo App 만들기

manage.py가 Django project로 돌아가는 프로젝트를 지원하는 도구라는 것 기억하시죠? 이 파일로 App을 만들면 편합니다.

$ python manage.py startapp photos

django-admin.py과 마찬가지로 별다른 안내 없이 생성됩니다. 뭔가 안내가 나타났다면 문제가 있는 거고요. manage.py로 만든 Django app인 Photo app은 다음과 같이 구성되어 있습니다.

pystagram/
├── db.sqlite3
├── manage.py
├── photos
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── pystagram
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

photos package가 Django App입니다.

admin.py는 관리자 영역에서 이 App을 다루는 코드를 담는 모듈입니다. Django의 강점이자 매력 요소인데, 다음 편에서 다룹니다.

models.py은 모델을 정의하는 모듈인데 모델(model)은 데이터(data)를 구성하는 항목 자체(field)와 데이터를 다루는 행위(behaviour)를 포함한 것입니다. 사진은 찍은 날짜, 사진 그 자체, 인화지, 이름과 같은 사진 구성 정보, 그리고 사진을 크게 확대해서 보거나 특정 부분을 오려내거나 복사하거나 종이로 접어 날려보내는 것과 같이 관련 행위로 구성됩니다. 이런 사진 관련 정보나 행위를 사진이라는 객체로 표현하는 것이 모델입니다. 우리가 다루고자 하는 대상 그 자체인데, 사람이 인지(recognition)하는 표현물은 아닙니다. 비유를 들자면, 하드 디스크에 있는 사진이라는 이미지 파일인 겁니다. 이미지 데이터일 뿐, 그게 어떤 사진인지는 알지 못 합니다.

views.py는 특정 주소(URL)에 접근하면 화면에 내용을 표시하는 Python 함수를 호출하는 내용을 담습니다. 이름만 보면 인화되거나 출력된 표현물 같지만, 정확한 용도는 우리가 인지하는 표현물로 안내하는 역할을 합니다. 비유를 들자면, 이미지 파일 뷰어나 프린터입니다. 그럼 대체 표현물은 뭘까요? MVC 패턴에서는 View가 표현물이지만, Django계에서는 template이 표현물입니다. Django에서 View는 데이터(모델)를 표현(템플릿)하는 연결자이자 안내자입니다. MVC 패턴으로 보면 Controller와 유사합니다.

마지막으로 tests.pyUnit test 내용을 담는 모듈입니다. 나중에 직접 겪어 보도록 하겠습니다.

(4) Photo App 만들기

사진을 다루는 App인 Photo App을 만들려면 무엇부터 해야 할까요? 뭔가를 만드는 시작은 Hello world류이니 템플릿으로 화면에 뭔가를 출력하는 게 미덕이겠지만, 우리가 직접 입력한 Python 코드만 없을 뿐 Django 템플릿을 이용해 “It worked!”를 이미 출력해 봤습니다. 그러니 모델을 실제로 만들어 보도록 하겠습니다.

Photo model 만들기

Django project에서 모델은 db package의 models 모듈에 있는 Model 클래스(class) 사용하여 만듭니다. 말이 복잡한데 간단히 말해서 models 모듈이 django의 db package 안에 있으니 from django.db import models 이렇게 접근해서 불러온다는 뜻입니다. Django로 만드는 애플리케이션의 모델은 바로 이 Model 클래스의 자식클래스(하위클래스, subclass)이므로 Model 클래스를 상속 받습니다.

그럼 Model 클래스를 사용하여 photos App에 Photo 모델을 만들어 보겠습니다. 이 모델은 사진을 다루는 기본 데이터를 다룹니다. 모델이니 models.py를 고쳐야겠지요?

from django.db import models

class Photo(models.Model):
    id = '개별 사진을 구분하는 색인값'
    image = '원본 사진 파일'
    filtered_image = '필터 적용된 사진 파일'
    content = '사진에 대한 설명문'
    created_at = '생성일시'

Django에서 모델의 속성(attribute)은 데이터베이스 필드(field)로 나타냅니다. Python 클래스로 놓고 보면 속성이지만, Django 모델의 데이터 요소로 다루고자 할 경우 Django 모델이 제공하는 별도 자료형(type)으로 값을 다루는데, 이 자료형인 값을 모델 필드라고 하지요. 그래서 위 모델에서 image, content 등은 아직은 그냥 Python 클래스 속성입니다. 각각이 무엇인지는 코드로 표현해놨으니 따로 설명드리진 않겠습니다.

이제 이 속성들을 Django 모델 필드로 바꾸겠습니다. 그래야 비로소 Django model이 제공하는 기능을 쓸 수 있습니다.

image은 이용자가 올리는 원본 이미지 파일을 담습니다. Django의 model에는 파일을 다루는 필드는 FileField가 있는데, 이런 필드 종류를 Django에서는 필드 타입(field type)이라고 부릅니다. 이 필드로 파일을 건내면 저장소(storage)에 파일을 저장하고 이 파일에 접근하는 연결자 역할을 하며, 파일 관련 기능이나 정보를 제공합니다. 이 필드 자체에 파일을 직접 저장하는 건 아닙니다. 그리고 파일 중에서도 이미지 파일을 대상으로 하는 ImageField도 있습니다. ImageField의 기본 바탕은 FileField인데, 실제로도 FileField를 상속 받은 클래스입니다2. 이미지 면적(dimension)이나 길이(width, height)같은 정보를 제공해서 이미지 파일만 다룬다면 ImageField를 쓰는 게 더 편합니다.

모델 필드에는 몇 가지 선택 항목을 지정하곤 하는데, 이런 선택 항목을 필드 옵션(field option)이라고 합니다. ImageField 필드엔 upload_to, height_field, width_field, max_length, storage 등이 있는데, height_field, width_fieldImageField 전용 옵션이며, 나머지는 FileField에서 상속받은 항목입니다. 이 항목은 필수 지정 사항은 아니며, 따로 설정하지 않으면 Django에서 기본 설정된 내용을 따릅니다. 자세한 건 나중에 다시 설명하겠습니다.

filtered_image는 원본 이미지 파일에 필터(filter)를 적용한, 즉 가공을 거친 파일입니다. 원본 이미지 파일을 저장하지 않는다면 필요없는 필드인데, 여러분을 좀 괴롭히는 기능을 만들 거라서 원본 이미지 파일과 가공을 거친 이미지 파일을 구분해서 모두 저장하겠습니다. 어쨌든 가공을 거친 파일도 이미지 파일이므로 ImageField 필드 타입을 쓰겠습니다.

content는 사진에 사진글 작성자가 기입한 내용입니다. 그냥 글자만 넣으면 되는데, 글자를 입력하는 필드 타입은 CharFieldTextField가 있습니다. 물론 ImageFieldFileField를 상속 받아서 확장된 기능을 제공하는 것처럼 CharField 필드 타입을 상속받아서 특별한 임무를 수행하는 클래스도 있습니다. SlugFieldURLField, EmailField, CommaSeperatedIntegerField 같은 거죠. 필드 타입명만으로 무슨 역할을 할 지 대략 예상이 가는군요. CommaSeperatedIntegerField는 정수(Integer)를 다루는 필드처럼 보이는데 CharField를 상속하는 문자형 필드인 이유는 쉼표(comma) 자체가 문자이기 때문입니다.

문자열을 다룬다는 점에서 CharFieldTextField는 같지만, 실은 전혀 다릅니다. CharField는 데이터베이스의 VARCHAR에 대응합니다. Django는 통상 250자 정도를 보장합니다. “보장”이라는 표현을 쓴 이유는 데이터베이스 시스템에 따라 VARCHAR 제한 길이가 다르기 때문입니다. 그에 반해 TextField는 이보다 훨씬 긴 문자열을 다룹니다. 이것도 데이터베이스 시스템에 따라 길이 제한이 다른데, SQLite3는 약 1기가 바이트까지 저장하는 text, PostgreSQL은 길이 제한이 없는 text, MySQL은 약 4기가 바이트까지 담는 longtext, Oracle은 약 8~12테라 바이트까지 담는 NCLOB에 대응합니다34. 보통은 긴 문자열을 담는 저러한 필드형에는 데이터베이스 인덱스가 걸리지 않으므로 Django의 TextField 필드에도 데이터베이스의 인덱스가(필드 옵션 : db_index) 걸리지 않습니다.

어쨌든 데이터베이스 시스템에 따라서 TextField 필드의 길이제한 단위가 무시무시한데, 굳이 저렇게 긴 문자열을 저장하진 않을 겁니다. 안 예쁘잖아요. 최대 길이를 500자로 제한하겠습니다. CharFieldTextField 둘 다 max_length라는 필드 옵션으로 최대 문자열 길이를 제한하며, CharFieldmax_length 필드 옵션을 반드시 넣어야 합니다.

created_at은 Photo 모델이 생성되어 데이터베이스에 저장되는 시각을 담는데, Django에는 날짜를 다루는 DateField, 시간을 다루는 TimeField, 그리고 날짜와 시간을 같이 다루는 DateTimeField가 있습니다. 생성일시 정보를 다루니 DateTimeField를 쓰겠습니다. 이 필드에는 auto_now 옵션과 auto_now_add 옵션이 있는데, 자동으로 현재 시간 정보를 담을 지 여부를 TrueFalse로 지정합니다. auto_now_add는 객체가 처음 생성될 때, auto_now는 객체가 저장될 때 자동으로 시간 정보를 담습니다. auto_now_addTrue로 설정한다면, 데이터가 처음 저장되는 시간 정보만 잡히고, 이후에 그 데이터를 수정하여 저장하더라도 자동으로 시간 정보가 담기진 않겠지요. 의사코드로 표현한다면 이런 모습일 겁니다.

from datetime import datetime

the_photo.save()

if the_photo.is_created is True:
    the_photo.created_at = datetime.now()
    the_photo.save()

저 두 옵션을 활용하면 이런 코드를 생략하는 것이니 깔끔하고 편하지요.

주의할 점은 Django 1.8부터는 이 두 필드 옵션을 함께 사용하면 안 됩니다. 1.8판 이전에는 다음과 같이 시간 필드 옵션을 설정했습니다.

  • 최초 생성 일시 : auto_now_add=True, auto_now_add=False
  • 매 변경 일시 : auto_now_add=True, auto_now=True

Django 1.8판부터는 다음과 같이 사용합니다.

  • 최초 생성 일시 : auto_now_add=True
  • 매 변경 일시 : auto_now=True

마지막으로 id는 값이 겹치지 않는 색인이며, 이 값이 각 사진을 구분 짓는 고유값입니다. Django에서는 id라는 필드를 따로 정하지 않으면 관례에(conventional) 따라 AutoFieldid를 Django framework가 알아서(자동으로) 만들어 다룹니다. 그러니 우리는 굳이 id를 정의하지 않아도 됩니다.

자, 이제 그냥 클래스 속성으로 구성된 기존 Photo 모델을 Django 모델로 바꿔 보겠습니다. 아참, 이 필드들은 models 모듈에 있습니다.

class Photo(models.Model):
    image = models.ImageField()
    filtered_image = models.ImageField()
    content = models.TextField(max_length=500)
    created_at = models.DateTimeField(auto_now_add=True)
데이터베이스에 반영 (migration)

Django 1.7 이전 판에서는 manage.pysyncdb 명령으로 우리가 만든 모델을 데이터베이스에 반영했지만, 1.7판부터는 makemigrationsmigrate 명령어를 이용합니다. Photo 모델을 마이그레이션 하려면 settings.pyphoto 앱을 추가해야 하니 settings.py에서 INSTALLED_APPS 항목을 찾아서 다음과 같이 photos를 추가합니다.

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'photos',
)

그 다음에 makemigrations으로 마이그레이션 작업 내용을 만듭니다. 그런데 오류가 발생합니다.

SystemCheckError: System check identified some issues:

ERRORS:
photos.Photo.filtered_image: (fields.E210) Cannot use ImageField because Pillow is not installed.
        HINT: Get Pillow at https://pypi.python.org/pypi/Pillow or run command "pip install Pillow".
photos.Photo.image: (fields.E210) Cannot use ImageField because Pillow is not installed.
        HINT: Get Pillow at https://pypi.python.org/pypi/Pillow or run command "pip install Pillow".

imagefiltered_image 모델 필드는 ImageField인데, 이 모델 필드는 이미지 파일을 처리하는 도구가 필요합니다. 이미지 처리 도구인 Pillow를 설치하면 해결됩니다.

$ pip install pillow

설치를 마치면 다시 마이그레이션 스크립트를 만듭니다.

$ python manage.py makemigrations 
Migrations for 'photos':
  0001_initial.py:
    - Create model Photo

마이그레이션 작업 내용은 0001_initial.py에 있으며, Photo 모델을 생성하는 것만 있네요. 실제 코드는 photos 디렉터리의 migrations 디렉터리 안에 0001_initial.py 있습니다. 이 작업을 실제로 DB에 반영하려면 migrate 명령을 내리면 됩니다.

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, photos, contenttypes, auth, sessions
Running migrations:
  Applying photos.0001_initial... OK

이렇게 해서 Photo 모델을 데이터베이스에 반영하여 연결하였습니다. 이제부터는 Photo 모델로 데이터를 데이터베이스에 저장하고 찾아 꺼내며 다룰 수 있습니다. 정리하면 migrate는 실제 DB에 반영하는 명령어이고, 이 명령어는 마이그레이션 스크립트를 실행하는데 makemigrations 명령어로 마이그레이션 스크립트를 만듭니다.

2. 부록

(1) MTV와 MVC

Django는 Model, Template, View의 앞자를 따서 MTV 패턴을 따릅니다. Model-View-Controller인 MVC 패턴과 유사한데, 실제로 많은 사람은 이 패턴의 개념에 별 차이를 두지 않습니다. Django의 View는 MVC 패턴의 Controller, Template은 MVC 패턴의 View로 적당히 퉁쳐서 이해합니다. 역할로 보면 그다지 틀린 말도 아니지만, Django framework가 지향하는 철학면에서 보면 MTV 패턴과 MVC 패턴엔 미묘한 차이가 있습니다.

재료를 가공하여 손에 닿는 결과물로 만드는 상황을 가정하지요. 여기서 재료란 Data, 즉 Model이고, 재료로 만들어 낸 결과물이 View입니다. 가공하는 행위자가 바로 Controller지요. 이게 MVC 패턴이라면, Django 소프트웨어 재단에서는 MVC 패턴의 Controller 역할은 Django framework 그 자체가 하고 있다고 봅니다.

무슨 말인지 알 듯 하기도 하고 모를 듯 하기도 하네요. :)

(2) 한날이 아이디어를 구체화 하는 과정

저는 막연한 아이디어를 구현하고자 할 땐 표현물부터 만듭니다. 가령 고객을 대상으로 하는 제품 설명서를 작성하거나 정해놓은 흐름대로만(시나리오) 작동하는 견본(sample)을 만듭니다.

표현물을 먼저 만드는 이유는 사용 경험 수단을 만들어서 겪어보고, 그러면서 저 스스로 만들 대상을 실체화하는 데 좋기 때문입니다. 왜(why) 만드는지 고민하는 것이지요. 실체화(구체화)가 되면 모델을 구상합니다. 모델도 행위를 중심으로 구상합니다. 어떤 정보(데이터)가 있는지는 정보를 어떻게 보일 것인지 고민하면 얼개를 짤 수 있습니다. 그리고 보여진 데이터로 어떤 행위를 할 수 있는지 생각하면 데이터 간 관계, 연관 데이터를 예상할 수 있습니다. 어떻게(how) 목표에 달성하는지 고민한 것입니다. 그런 과정을 얼추 마치면 방법(how)에 필요한 재료를 제한합니다. 물론 만들면서 그때 그때 필요한 재료를 추가하거나 필요없는 재료를 빼기도 합니다.

이렇게 하는 이유는 이런 개발 과정이 재밌어서 그렇습니다. 취향이자 성향이지요. 이 강좌를 보시는 여러분의 취향이나 성향, 철학은 각양각색이니 제 취향과 성향대로 개발하고 설명해 나가겠습니다. ^^ 템플릿 컨텍스트 처리기(TEMPLATE_CONTEXT_PROCESSORS) 설정이 OPTIONScontext_processors로 이동하였고, 템플릿 파일 경로는 TEMPLATE_DIRS에서 DIRS로 이동하였습니다.


이것으로 강좌 3편을 마칩니다. 모델을 만들고 데이터를 실제로 저장하는 부분까지 하려 했는데, 분량이 많아져서 3편을 여기서 끊네요. 4편에서는 꼭 데이터를 저장하고 꺼내 봅시다! :)


  1. REST API 설계 발표 자료에서 한국어로 잘 설명 해놨습니다. 

  2. django/db/models/fields/files.py 모듈에 정의되어 있는 ImageField 클래스를 참조하세요. 

  3. django/db/backends/에서 mysql, oracle, sqlite3 패키지 안에 있는 creation.py 모듈을 참조하세요. 

  4. 글자 수가 아니라 바이트입니다.