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편을 마칩니다. 늦어서 죄송합니다. 요즘 많이 바빠서 연재하기 힘드네요. ㅜㅜ 한 편에 너무 많은 내용을 담느라 연재 주기가 늘어지지 않도록 해보겠습니다.