Django 커스텀 manage.py 명령어 만들기

2020. 10. 05 IT/컴퓨터 > Django

Picture by Safar Safarov from unsplash.com

 

장고 프로젝트에서 manage.py 를 이용해 프로젝트 관련 명령을 실행할 때가 있다.

$ python manage.py runserver
$ python manage.py collectstatic
$ python manage.py migrate

등등 테스트용 서버를 띄우거나 models.py 내의 모델을 수정하고 새로 마이그레이션 하는 등의 작업을 manage.py 라는 파일을 통해 터미널의 커맨드 라인에서 입력해서 하도록 되어 있다.

 

(참고로 sub 커맨드 없이 그냥 python manage.py 만 실행해 보면 실행 가능한 sub커맨드들을 볼 수 있고, 각각에 대해서 help를 쳐서 사용법을 살펴볼수 있다.)

$ python manage.py 

Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:

[auth]
    changepassword
    createsuperuser

[contenttypes]
    remove_stale_contenttypes

... 중략 ...

[staticfiles]
    collectstatic
    findstatic
    runserver

 

이렇게 장고에서 기본적으로 제공하는 커맨드들 이외에, 원하는 동작을 수행하는 sub 커맨드를 직접 작성할 수도 있다.

어떤 작업이 필요할 때 꼭 manage.py 를 통하지 않아도 파이썬 자체 명령으로 실행되는 별도의 파일을 만들어 실행할 수도 있지만, 이렇게 하면 기본적인 장고 설정들이 로딩되지 않기 때문에 프로젝트의 models.py 에서 사용하던 모델들을 불러와 데이터베이스와 연동된 작업을 하는것이 어렵다. 

manage.py 를 통해 실행되는 커맨드들은 장고 환경을 자동으로 로딩해 주고 실행되기 때문에 Django shell 에서 작업하는것과 같이 프로젝트에서 사용하던 model 들을 별도의 복잡한 설정 없이 바로 import 해서 데이터베이스와 관련된 작업을 할 수 있어 편리한 점이 많다. (Django shell 을 띄우는 것은 python manage.py shell 명령으로 가능)

그래서 주로 데이터베이스에 항목들을 일괄 입력/수정하는 등의 작업이 반복적으로 필요할 때 이런 커스텀 커맨드들을 등록해 놓으면 편리하다. 이렇게 커스텀 커맨드를 등록해 놓으면 리눅스 환경에서 cron 을 이용해 반복적으로 해야 하는 일이 있을때도 커맨드 한 줄로 처리할수 있다.

 

[ 기본 형식 ] 

커맨드를 만들려면 우선 등록된 app 디렉토리 아래에 management/commands 라는 디렉토리를 만들어 주어야 한다.

manage.py 가 있는 프로젝트의 루트 디렉토리를 my_project/ 라고 하면 

my_project/ 디렉토리 아래에 기존에 my_app/ 이라는 앱 디렉토리가 있고 그 디렉토리 안에 views.py, models.py, urls.py 등의 기본 파일들이 있을 것인데 그 아래에 management/commands 디렉토리를 만들어 주고, commands 디렉토리에 실행하고 싶은 커맨드를 my_command.py 식으로 만들어 주면 된다.

~/my_project/my_app/management/commands$

안에 my_command.py 가 들어가는것이고, 이렇게 등록해주면 프로젝트의 root 디렉토리에서 

~/my_project$ python manage.py my_command

와 같이 실행할 수 있다.

 

커맨드의 기본 형식은 다음과 같다.

[/my_project/my_app/management/commands/my_command.py]
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = '커맨드에 대한 도움말을 적어줌'

    def handle(self, *args, **kwargs):
        ''' 실행할 동작을 정의해줌'''
        print('my_command.py is running!')

 

BaseCommand 클래스를 임포트해주고 이를 상속받는 class Command 를 정의해준다.

커맨드 사용법을 문자열로 help 변수에 저장해주면 

~/my_project$ python manage.py help my_command

를 실행했을 때 해당 내용이 출력된다.

 

클래스 내의 handle() 메서드에 실행하고 싶은 동작을 일반 파이썬 함수로 작성해 주면 된다.

위의 경우 

~/my_project$ python manage.py my_command
​my_command.py is running!

와 같이 실행되는 것을 볼 수 있다.

 

 

[ 옵션 설정 ]

커맨드에 동작 옵션을 추가해줄 수 있다. 클래스 내에 add_arguments() 메서드를 정의해주면 된다.

[/my_project/my_app/management/commands/my_command.py]
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = '커맨드에 대한 도움말을 적어줌'

    def add_arguments(self, parser):
        # 위치 인자 (positional arguments)
        parser.add_argument('option1', type=int, help='위치 인자를 전달')

        # 키워드 인자 (named arguments)
        parser.add_argument('-o', '--option2', type=str, help='키워드 인자를 전달', )

    def handle(self, *args, **kwargs):
        ''' 실행할 동작을 정의해줌'''
        print('my_command.py is running!')

 

저렇게만 써 놓으니 좀 막연한데, 간단한 예제를 작성해본다.

my_app 앱 안의 models.py 에 사용자들끼리 주고받는 쪽지를 만들어주는 UserMessage 라는 모델이 정의되어 있고, 해당 모델로 생성된 사용자 쪽지들이 데이터베이스에 저장되어 있다고 가정하고 예시를 작성해 보겠다.

쪽지들은 읽으면 read 상태로 되고 따로 보관함에 보관할수 있는데, 보관함에 보관하지 않으면 unarchived 상태라고 가정한다. 

이 중 읽은 쪽지를 일괄적으로 삭제하거나, 아니면 보관함에 보관되지 않은 쪽지들을 일괄적으로 삭제하는 커맨드를 작성하고 싶다고 해 본다.

우선 

[/my_project/my_app/management/commands/delete_user_messages.py]
from django.core.management.base import BaseCommand
from my_app.models import UserMessage

class Command(BaseCommand):
    help = '지정된 쪽지를 삭제해주는 기능'

    def add_arguments(self, parser):
        # 위치 인자 (positional arguments)
        parser.add_argument('recent_range', type=int, 
                            help='최근 며칠간의 쪽지에 대해 작업할것인지 날 수를 입력')

        # 키워드 인자 (named arguments)
        parser.add_argument('-i', '--including', type=str, 
                            help='포함할 쪽지의 종류를 지정해줌. "read", "unarchived"')

    def handle(self, *args, **kwargs):
        recent_range = kwargs['recent_range']  # 최근 며칠간 쪽지를 삭제할것인지
        including = kwargs['including']  # 어떤 쪽지들을 포함할 것인지를 지정
 
        if not including:  # 만약 including 옵션이 입력되지 않은 경우 디폴트값을 정해줌
            including = 'read'  # 일단 여기서는 디폴트값을 'read' 로 지정,
                                # 즉 별도 옵션이 없으면 읽은 쪽지를 삭제하도록 지시
        
        messages = UserMessage.objects.all()

        if recent_range:  # 만약 recent_range 옵션이 주어진 경우 
                          # (옵션이 입력되지 않으면 모든 쪽지를 대상으로 작업)
            messages = 쪽지를 recent_range 에 지정된 일수만큼 걸러내 주는 logic 작성

        if including == 'read':
            이미 읽은 쪽지를 삭제해주는 logic
        elif including == 'unarchived':
            보관함에 보관되지 않은 쪽지를 전부 삭제해주는 logic

 

위의 커맨드는 

~/my_project$ python manage.py delete_user_messages

와 같이 실행할 수 있고, 아무 옵션을 주지 않은 경우 특정 기간이 아닌 모든 쪽지에 대해 read 상태인 (즉 이미 읽은) 쪽지를 삭제하게 된다.

 

~/my_project$ python manage.py delete_user_messages 30

add_argument() 내에서 recent_range 로 지정된 인자는 위치 인자이므로 플래그 없이 바로 옵션의 값을 입력하면 된다. 이 경우 전체 쪽지 중 최근 30일간 작성된 쪽지들을 추려서 작업하게 된다.

 

~/my_project$ python manage.py delete_user_messages --including 'unarchived'

혹은

~/my_project$ python manage.py delete_user_messages -i 'unarchived'

이런 식으로 옵션과 그 값을 입력해 주면 unarchived, 즉 보관함에 보관되지 않은 쪽지는 모두 삭제한다.

 

특정한 여러 개의 인자들을 리스트로 전달해서 작업하는것도 가능하다.

[/my_project/my_app/management/commands/delete_user_messages.py]
from django.core.management.base import BaseCommand
from my_app.models import UserMessage

class Command(BaseCommand):
    help = '지정된 쪽지를 삭제해주는 기능'

    def add_arguments(self, parser):
        # 위치 인자 (positional arguments) 를 리스트로 전달
        parser.add_argument('message_ids', nargs='+', type=int, 
                            help='여러 개의 message 의 번호를 지정해 삭제')

    def handle(self, *args, **kwargs):
        message_ids = kwargs['message_ids']

        for message_id in message_ids:
            try:
                message = UserMessage.objects.get(id=message_id)
                message.delete()
                print(message_id, '번 쪽지를 삭제했습니다.')

            except UserMessage.DoesNotExist:
                print(message_id, ': 해당하는 번호의 쪽지는 존재하지 않습니다.')

 

이렇게 작성하면 지우고 싶은 메세지의 ID 를 지정해서 삭제해줄 수 있다 (여기서 ID는 데이터베이스에 저장된 순서대로 자동으로 붙는 번호이므로 사실 message의 ID를 알아내 지운다는것이 약간 현실적인 사용 례는 아닌것같지만 개념상 딱히 좋은 예가 당장 떠오르지 않아 이렇게 해 보았다. 특정 유저나 특정 단어가 들어간 쪽지를 삭제한다든지 하는 식으로 응용할수 있을듯)

~/my_project$ python manage.py delete_user_messages 1 2 3
1 번 쪽지를 삭제했습니다.
2 번 쪽지를 삭제했습니다.
3 : 해당하는 번호의 쪽지는 존재하지 않습니다.

 

이런식으로 여러 개의 숫자를 리스트로 전달해 일괄 처리하는것도 가능하다.

 

옵션들은 add_arguments() 메서드 안에 parser.add_arguments()를 통해 제한 없이 추가 가능하다. 

 

다른 프로젝트에서 매주 업데이트되는 정보를 웹에서 크롤링해 와서 데이터베이스에 입력하는 작업을 하고 있는데, 이런식으로 커스텀 커맨드를 작성한 후 리눅스의 cron 으로 매주 한번씩 작업이 되게 하고 있다. 

 

기타 자세한 내용은 Django 의 공식 문서를 참조하면 좋을 듯한데 아쉽게 아직 한글 번역은 되지 않은것같다. 

공식 문서:

https://docs.djangoproject.com/ko/3.1/howto/custom-management-commands/



최근 글 목록