프로젝트 루트 디렉토리에 위치하는 manage.py는 사이트 관리를 위한 백엔드 콘솔이라고 설명을 했었습니다. 테스트 서버 데몬을 구동하기 위해 실행했던 runserver도 여기에 내장되어 있는 기본 명령어입니다.
Django프로젝트를 생성하면 기본적으로 제공되는 명령어들은 다음과 같습니다. (좀 많은것 같죠..?^^)
몇 가지 명령어는 튜토리얼에서 사용해 봤겠지만, 아직 사용해보지 않은 명령이 더 많을 것입니다. 여기에 정의되어 있는 명령어는 주로 DB에 관련된 것들이 많습니다. 또한, shell 명령을 실행하면 Django 구성요소를 Import한 파이썬 쉘이 나타나는데, 여기서는 직접 파이썬 명령을 쳐서 사이트 관리 작업을 할 수 있습니다.
이번 글에서는 위에 나타난 목록에 Custom Command를 등록하고 사용하는 방법에 대해 다루도록 하겠습니다. 본문 예제에서는 지난번까지 작성했던 예제에 덧붙여서 DB에 튜플을 삽입하는 'insert-book' 명령을 정의합니다.
Custom Command 추가를 위한 디렉토리 구조 만들기
우선, 다음 명령을 실행해서 Custom Command를 등록하기 위한 디렉토리 구조를 만들고, 파이썬에서 모듈로 인식할 수 있도록 __init__.py파일을 위치시키도록 합니다.
(venv)$ mkdir -p myapp/management/commands (venv)$ touch myapp/management/__init__.py (venv)$ touch myapp/management/commands/__init__.py
이제 myapp/management/commands 디렉토리 안에 {Command 이름}.py 파일을 추가하면 다음과 같이 백엔드 콘솔에서 manage.py를 통해 정의한 명령을 실행할 수 있습니다.
(venv)$ ./manage.py {Command 이름} [Args...]
정의할 insert-book명령은 세 개의 Argument(isbn, title, memo)를 입력받아서 Book 튜플을 생성한 뒤 DB에 삽입하는 동작을 수행합니다. myapp/management/commands/insert-book.py 파일을 다음과 같이 작성합니다.
import argparse import re from django.core.management.base import BaseCommand, CommandError from myapp.models import Book class Command(BaseCommand): help = 'Insert book tuple into database' def CheckIsbn(self, string): if not re.match("^[0-9]{13}$", string): raise argparse.ArgumentTypeError("'{0}' is not a valid ISBN"\ .format(string)) return int(string) def add_arguments(self, parser): parser.add_argument('isbn', nargs=1, type=self.CheckIsbn, help='ISBN number') parser.add_argument('title', nargs=1, type=str, help='Book title') parser.add_argument('memo', nargs=1, type=str, help='Memo') def handle(self, *args, **options): isbn = options['isbn'][0] title = options['title'][0] memo = options['memo'][0] try: Book(isbn=isbn, title=title, memo={'content':memo}).save() except: raise CommandError("Failed inserting book tuple into DB") self.stdout.write("Succeed inserting book tuple into DB")
Custom Command를 정의하려면 BaseCommand를 상속받은 Command Class를 정의하고, 그 안에 handle메소드를 구현해야 합니다. 인수(Argument)를 입력받으려면 add_arguments 메소드도 함께 구현해야 합니다. 멤버 변수 'help'는 '-h' 옵션을 입력해서 명령에 대한 도움말을 표시할 때 나타낼 문구를 입력합니다.
내부적으로 파이썬의 Commandline Argument Parser(argparse)를 사용하기 때문에 argparse를 사용해 봤다면, 위 코드를 쉽게 이해할 수 있을 것입니다.
[add_arguments] 메소드:
명령에 사용할 세 개의 인수를 정의하여 parser에 추가합니다. 세 개의 인수는 모두 입력 순서에 따라 정해지는 Positional Argument로, isbn-title-memo의 순서로 입력 받습니다. 각 인수의 갯수는 모두 1개(nargs=1)입니다.
type에는 인수의 형식을 지정하며, title과 memo는 기본형인 string형식을 사용하였고, isbn은 13자리 숫자가 맞는지 검사하기 위해 CheckIsbn이라는 이름의 Custom Argument Validator와 연결시켰습니다.
[CheckIsbn] 메소드:
add_argument의 첫 번째 인수인 isbn의 유효성을 검사합니다. 정규식을 사용하여 전달된 문자열이 13자리 숫자인지 여부를 검사하여 형식에 맞지 않는 경우에는 ArgumentTypeError예외를 발생시킵니다. 예외가 발생하면 오류 메시지를 표시하면서 명령 실행을 중단합니다. 인수가 형식에 맞는 경우에는 정수형으로 변환해서 반환합니다. (Custom Argument Validator는 인수의 유효성 검사 뿐만 아니라 필요한 형식으로 변환해서 반환하는 기능도 함께 수행합니다.)
필수 인수들이 모두 입력되었고 형식에 맞는지 검사하여 통과된 경우에만 비로소 handle메소드가 호출됩니다. 파싱된 인수들은 options 파라미터에서 찾을 수 있으며, 이미 유효성 검사가 끝난 상태이므로 예외 처리 없이 그대로 사용할 수 있습니다.
30-32줄에서 전달된 인수들을 전달받습니다. 이 때 주의할 점이 있는데, 전달되는 인수들은 모두 목록(List)형식이라는 점입니다. 이는 하나의 인수에 여러 개의 값을 포함할 경우에 대비하기 위함입니다. 여기서는 인수들이 모두 단일 값이므로 모두 목록의 첫 번째 항목([0])을 지정하였습니다.
34줄에서는 가져온 인수들로 Book 튜플을 만들어서 DB에 튜플을 삽입합니다. try~except구문으로 예외처리를 하였고, DB작업중에 오류가 발생하면 CommandError예외를 발생시킵니다. CommandError는 '명령이 올바르게 실행되지 못했음'을 나타내는 예외입니다. 이 예외를 rasie하면 오류메시지를 출력하면서 명령 실행을 중단하며, 최종적으로 명령을 실행한 Shell에 Error Code 1을 반환합니다.
참고로, Argument Parser에 관한 자세한 기술문서는 다음 파이썬 공식 매뉴얼을 참조해 주세요.
https://docs.python.org/3.4/library/argparse.html
Custom Command 실행하기
여기까지 과정을 잘 따라온 경우, manage.py에 다음과 같이 새로 정의한 insert-book 명령이 등록되어 있을 것입니다.
다음과 같이 인수와 함께 명령을 실행하면 DB에 튜플이 성공적으로 삽입되었다는 메시지가 나타납니다.
(venv)$ ./manage.py insert-book 1234567890123 전자회로 Razavi
잘못된 형식의 인수를 입력하면 오류메시지가 나타나면서 바로 종료됨을 알 수 있습니다. (ISBN에 13자리가 아닌 10자리 숫자를 입력해서 오류가 발생한 경우입니다.)
-h 옵션을 줘서 명령을 실행하면 다음과 같이 도움말 화면이 표시됩니다.
[심화] save()메소드의 동작 방식
다음과 같이 위에서 입력했던 명령을 반복 실행해서 같은 튜플을 DB에 삽입하려고 시도하면 아무 오류가 발생하지 않는 것을 알 수 있습니다. 분명 Model에서 ISBN이 Primary Key로 설정했기 때문에 이미 존재하는 ISBN의 Book 튜플을 DB에 삽입하려고 시도하면 오류가 발생해야 할 것입니다.
이렇게 같은 튜플을 중복해서 삽입하려 시도하는데도 오류가 발생하지 않는 이유는 save()메소드가 내부적으로 INSERT와 UPDATE 동작을 적절히 선택해서 수행하기 때문입니다. 즉, Primary Key로 지정된 Field를 갖는 튜플이 이미 존재하면 UPDATE를 수행하고, 그렇지 않은 경우 INSERT를 수행합니다.
위에서 같은 튜플을 삽입하는 명령을 3번 반복해서 수행했지만, 실제로 튜플이 삽입된 것은 첫 번째 명령을 실행할때 뿐이고, 나머지는 업데이트가 수행된 것입니다.
save()메소드를 호출할 때 'force_insert=True'나 'force_update=True'옵션을 지정해서 강제로 INSERT또는 UPDATE를 수행하도록 할 수도 있습니다. 'force_insert=True'옵션을 지정하고 이미 존재하는 PK를 갖는 튜플을 삽입하려고 시도하면 오류가 발생합니다.
실험을 위해 위의 예제의 34번째 줄의 save메소드에 다음과 같이 'force_insert=True'옵션을 주고 같은 튜플을 삽입하는 명령을 실행하면...
Book(isbn=isbn, title=title, memo={'content':memo}).save(force_insert=True)
위와 같이 튜플 삽입에 실패했다는 오류메시지가 뜨면서 종료가 될 것입니다.
이번 글에서는 사이트 관리를 위한 백엔드 콘솔에 사용자 정의 명령어를 추가하는 방법에 대해 살펴보았습니다. 파이썬의 편리한 Commandline Parser를 그대로 가져다가 쓸 수 있으므로 귀찮은 예외처리 검사 구문을 작성하지 않고도 손쉽게 새로운 명령을 추가할 수 있습니다.
다음 글부터는 완성한 Django 프로젝트를 실제 운영 서버에 올려서 WSGI를 통해 웹 서버와 연동하는 방법 대해 다루도록 하겠습니다. 구체적으로는 Apache+mod_wsgi를 활용하는 방법과, Nginx+uWSGI를 활용하는 방법 두 가지 모두를 다룰 것입니다.
- [Django Tutorial] 9. Production - uWSGI를 통해 Nginx 웹 서버와 연동하기 (11348) *1
- [Django Tutorial] 8. Production - setting.py설정, Static파일 모으기 (5477)
- [Django Tutorial] 6. Database 연동하기 - Model설계, Migration (30157)
- [Django Tutorial] 5. Static 파일 사용하고 관리하기 (9267)
- [Django Tutorial] 4. URL Config, Template 및 View의 동작에 대한 이해 (9196)
- [Django Tutorial] 3. 프로젝트 및 App 생성, settings.py수정(DB연동, Migration), Runserver (12314)
- [Django Tutorial] 2. 개발 환경 세팅하기 - pyenv 및 virtualenv 활용 (6603)
- [Django Tutorial] 1. 파이썬 기반 웹 프레임워크 Django에 대한 소개 (10945) *2
- VirtualEnv를 통한 Python Sandbox 개발환경 구축하기 (4428)
- pyenv를 이용하여 여러 버전의 Python 동시에 사용하기 (14703) *3