본문 바로가기

Project/Shall We Django?

Shall We Django? [3]

1/ 모델

지난 시간까지는 URL과 앱 내부의 html을 연결하는 작업을 진행했다. 만들고자 하는 사이트가 위키 형태이므로, 어떠한 텍스트를 입력하고 저장한 뒤 수정도 가능하며 삭제도 할 수 있도록 만들고 싶다.

 

이러한 기능을 가리켜 CRUD라고 부른다. 한국어 위키피디아에서는 "대부분의 컴퓨터 소프트웨어가 가지는 기본적인 데이터 처리 기능인 Create(생성), Read(읽기), Update(갱신), Delete(삭제)를 묶어서 일컫는 말이다."라고 서술하고 있다.

 

Django에서도 CRUD를 담당하는 부분이 있다. models라고 한다. 여기서 각각의 model은 models.py에서 정의된 각각의 model들을 말하고, 하나의 model은 클래스(객체)다. 또한 하나의 model은 하나의 데이터베이스 테이블과 대응한다. 더 자세히 말하자면 model 내부의 속성(attribute)이 데이터베이스의 필드와 대응하도록 만든 것이다.

 

본래 프레임워크와 데이터베이스 사이에는 간극이 있어 직접 데이터베이스를 조작하는 SQL query를 통해 그 간극을 메웠다고 한다. 그러한 연결을 개발자가 아닌 프레임워크가 처리하도록 구성할 수도 있다. 위에서 언급한 것처럼 프레임워크 언어의 객체와 데이터베이스의 필드를 대응시키는 기능을 삽입하는 것이다. 이를 ORM(Object Relational Mapping)이라고 한다.(1) 말 그대로 객체(object)와 관계형(relational) 데이터베이스(2) 사이의 대응 관계(mapping)를 구성한 것.

 

그렇다면 위키 페이지에는 어떤 데이터들이 필요할까? 일단 특정한 주제에 대해 다뤄야 하니 subject라는 항목을 생각할 수 있겠다. 당연히 해당 주제에 대한 설명 내용이 있어야 하므로 content라는 항목도 필요하다. 해당 게시글이 처음 생성된 일자와 최근에 수정된 일자 또한 중요하다고 본다. 각각 created_at과 updated_at이라는 변수명을 사용하자.

 

apps 폴더의 models.py에 Wiki라는 클래스를 만들어 속성들을 다음과 같이 선언하였다:

from django.db import models

class Wiki(models.Model):
    subject = models.CharField(max_length=100)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)    # auto_now_add는 최초 저장 시에만 적용, ineditable
    updated_at = models.DateTimeField(auto_now=True)        # auto_now는 객체가 저장될 때마다 적용

 

models라는 모듈을 import하는 걸 볼 수 있다. 그리고 모든 줄에서 models를 사용하고 있다. models란 어떤 모듈일까?

 

Django를 설치한 (가상환경의) 디렉토리에서 Lib/site-packages/django/db로 들어가면 models라는 폴더가 있다.(혹은 VSCode에서 interpreter 경로 설정이 잘 되어 있다면 ctrl + left-click으로 해당 대상을 바로 열람할 수 있다)  __init__.py라는 파일이 있는 걸로 보아 models는 패키지(package)인 듯하다.(3) models 패키지 내부의 fields 패키지에서 CharField 따위의 클래스를 선언하고 있는 것을 확인할 수 있다. TextField를 정의하는 코드를 보자.

 

class TextField(Field):
    description = _("Text")

    def __init__(self, *args, db_collation=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.db_collation = db_collation

    def check(self, **kwargs):
        databases = kwargs.get("databases") or []
        return [
            *super().check(**kwargs),
            *self._check_db_collation(databases),
        ]

    def _check_db_collation(self, databases):
        errors = []
        for db in databases:
            if not router.allow_migrate_model(db, self.model):
                continue
            connection = connections[db]
            if not (
                self.db_collation is None
                or "supports_collation_on_textfield"
                in self.model._meta.required_db_features
                or connection.features.supports_collation_on_textfield
            ):
                errors.append(
                    checks.Error(
                        "%s does not support a database collation on "
                        "TextFields." % connection.display_name,
                        obj=self,
                        id="fields.E190",
                    ),
                )
        return errors

    def db_parameters(self, connection):
        db_params = super().db_parameters(connection)
        db_params["collation"] = self.db_collation
        return db_params

    def get_internal_type(self):
        return "TextField"

    def to_python(self, value):
        if isinstance(value, str) or value is None:
            return value
        return str(value)

    def get_prep_value(self, value):
        value = super().get_prep_value(value)
        return self.to_python(value)

    def formfield(self, **kwargs):
        # Passing max_length to forms.CharField means that the value's length
        # will be validated twice. This is considered acceptable since we want
        # the value in the form field (to pass into widget for example).
        return super().formfield(
            **{
                "max_length": self.max_length,
                **({} if self.choices is not None else {"widget": forms.Textarea}),
                **kwargs,
            }
        )

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        if self.db_collation:
            kwargs["db_collation"] = self.db_collation
        return name, path, args, kwargs

 

Field라는 클래스를 상속 받아서 이것저것 정의가 되어 있는데… 봐도 잘 모르겠다……. 나중에 뜯어보기로 하고(4) 지금은 넘어가자.

 

좀 더 얕은 수준으로 돌아와서, Django에서는 CharFieldTextField를 구분하고 있는데, 공식 문서에서 "For large amounts of text, use TextField."라고 언급하는 등 짧은 문자열에 대해서만 CharField를 쓸 것을 권하고 있다. 이건 관계형 데이터베이스 관리 시스템(RDBMS, Relational DataBase Management System)들이 필드의 자료형으로 varchar와 text를 구분하기 때문인 것 같다.(5) 

 

의문이 몇 가지 생겼지만 일단 당장 목표로 한 model은 완성했다. 이제 model과 데이터베이스를 연결할 시간이다.

 

Django에서 model과 데이터베이스를 연동하는 작업은 2단계로 이루어진다. 우선 makemigrations 명령어를 통해 model의 수정사항을 반영하는 작업이 필요하고, 이후에는 migrate 명령어를 통해 실제로 데이터베이스 테이블을 만들어 연동하는 작업이 진행되어야 한다.

 

$ python manage.py makemigrations
Migrations for 'apps':
  apps\migrations\0001_initial.py
    - Create model Wiki

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, apps, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying apps.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

 

이제 manage.py 파일이 있는 폴더의 db.sqlite3 파일을 조회하면 다음과 같은 내용을 볼 수 있다.

 

 

apps라는 앱의 Wiki라는 모델을 가리키는 테이블로 apps_wiki가 생성되었다. 해당 테이블에는 5개의 필드가 있는데, 개중 id는 우리가 Wiki 클래스에 입력하지 않은 항목이다. Django는 기본적으로 각각의 모델에 id라는 필드를 자동적으로 준다고 한다. id 필드는 기본적으로 primary key인데, 테이블 내부의 각 자료모음(row 혹은 tuple)을 구분짓는 식별자다. 개발자는 원한다면 primary key가 되는 필드를 직접 선언할 수 있다.

 

 

2/ Django shell과 관리자 페이지

우리는 아직 웹사이트를 통해서 글을 입력하는 기능을 만들지 않았다. 하지만 지금 상황에서도 데이터베이스에 자료를 입력할 수 있는 방법이 있는데, 바로 Django shell을 이용하는 것이다. shell 명령어를 이용하면 Django shell을 실행할 수 있다.

 

$ python manage.py shell
Python 3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

 

해당 shell은 line-by-line으로 실행되는데, 파이썬과 비슷한 형태다. 종료를 위해서는 ctrl+z를 이용하자.

>>> from apps.models import Wiki
>>> article = Wiki(subject='Django', content='Django는 신속한 개발과 깔끔하고 실용적인 디자인
을 돕는 고수준 Python 웹 프레임워크다.') 
>>> article.save()
>>> article.id
1
>>> article.subject
'Django'
>>> article.content
'Django는 신속한 개발과 깔끔하고 실용적인 디자인을 돕는 고수준 Python 웹 프레임워크다.'      
>>> article.created_at  
datetime.datetime(2024, 3, 17, 6, 15, 33, 794891, tzinfo=datetime.timezone.utc)
>>> article.updated_at
datetime.datetime(2024, 3, 17, 6, 15, 33, 794891, tzinfo=datetime.timezone.utc)
>>>

 

Django shell에서 article이라는 변수를 이용하여(변수명이 꼭 article일 필요는 없다) 데이터베이스에 새로운 내용을 등록해봤다. 실제로 데이터베이스에 적용이 되었을까?

 

 

입력이 잘 되었음을 확인할 수 있다! 

 

약간의 절차가 더 필요하긴 하지만, 데이터베이스에 자료를 입력하는 방법이 하나 더 있다. Django에서 지원하는 관리자(admin) 사이트로, 모델 데이터를 관리할 수 있는 인터페이스다.

 

우선 관리자 페이지에 접속하기 위해서 관리자 계정을 만들어야 한다. createsuperuser 명령어를 사용해 admin 계정을 만들자.

 

$ python manage.py createsuperuser
Username (leave blank to use 'gorankim'): admin
Email address:
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

 

Username은 admin, Email은 비워두고, Password는 1234로 설정하였다. 친절하게도 password의 보안이 부족하다고 경고해주고 있다. 일단 진행하자.

 

이후 다시 서버를 켜서(runserver) http://127.0.0.1:8000/admin/으로 접속하면 다음과 같은 화면을 볼 수 있다.

 

뭔가 정말 관리자 페이지 같다

 

하지만 우리가 목표로 했던 Wiki 모델에 대한 정보는 확인할 수 없다. 약간의 절차가 더 필요하다.

 

apps 폴더 내의 admin.py를 다음과 같이 수정하자.

 

from django.contrib import admin
from .models import Wiki

admin.site.register(Wiki)

 

그리고 관리자 페이지를 새로고침하면 다음과 같이 화면이 바뀌어 있는 게 보일 것이다.

 

 

Wikis라는 항목이 생겼다. 우리가 만든 model은 Wiki였는데 접미에 복수형을 뜻하는 -s가 붙었다. 이는 Django가 human-readable의 목적으로 model의 이름을 단수형(singular)으로 지을 것을 권고하고 있기 때문이다. Meta 옵션을 이용하여 단수형/복수형 이름을 지정할 수 있다.

 

 

Wikis를 클릭하면 상세 페이지로 들어가게 되고, 항목이 하나 있음을 확인할 수 있다.

 

 

해당 항목은 우리가 shell에서 입력했던 그 내용임을 확인할 수 있다. 간단하게 수정해보자. 나는 다음과 같이 내용을 고쳤다.

 

 

그럼 이제 SAVE 버튼을 눌러보자. 그러면 이전 상세 페이지로 돌아감과 동시에 "The wiki “Wiki object (1)” was changed successfully."라는 문구가 뜰 것이다. 데이터베이스를 다시 확인해보자.

 

 

content가 수정되었음을 확인할 수 있다.

 

 

+/ 240317 돌아보기

1) 진도가 너무너무너무 느리다. 건강 상태와는 별개로 기록을 하며 진행한다는 것 자체가 내겐 처음 있는 일이라 그런 것 같다. 생각의 파편들을 늘어놓은 다음 선형적으로 그러모으는 경험은 많았지만, 이렇게 긴 내용을 끊어가며 진행 및 작성을 번갈아 하는 작업은 겪어보지 않았다.

익숙지 않은 만큼 산만해지는 일이 잦은데 장점도 있었다. 보다 명시적으로 생각할 수 있는 기회가 된다는 점이 그렇다. 단순히 구현에만 집중했다면 구글링 이후 구현하고 넘겼을 지점들을 다시 한 번 고민해보게 된다. 질문 상자의 존재가 큰 효과를 발휘하는 중인 듯.

 

2) 글을 작성하며 Django의 공식 문서를 수시로 참고했는데 튜토리얼이 있더라. 내용이 꽤나 알찼다. 악기를 연주할 때 어려운 곡에 바로 도전하는 것보다 쉬운 곡부터 차근차근 연습해나가는 게 더 빠르다는 이야기를 들은 적이 있다. 어쩌면 프로그래밍도 비슷할 것이다. 잘 짜여진 길을 가면서 중요한 부분을 습득하고, 그것을 바탕으로 다시금 원하는 방향으로 나아가는 것이 총체적으로 낫지 않을까 하는. 이후의 프로젝트에서는 참고해봐야겠다.

 

 

*/ 질문 상자

1) ORM에 대해서 알아보자. 이게 어떤 맥락에서 등장했는지, 어떤 ORM들이 있는지, ORM이 없는 경우 어떻게 해야하는지, Python의 경우에는 ORM을 쓰지 않는 웹 프레임워크가 있는지 등등.

2) 관계형 데이터베이스(RDB)란 무엇인가?

3) 패키지의 정의는 무엇인가?

4) OOP에 대해서 제대로 공부할 시기가 다가온 듯하다. 이후 다시 models 패키지를 살펴보도록 하자.

5) 데이터베이스에 대한 공부가 필요하다. 무엇을 어디부터 공부해야 할까?

 

'Project > Shall We Django?' 카테고리의 다른 글

Shall We Django? [4]  (0) 2024.03.20
Shall We Django? [2]  (2) 2024.03.14
Shall We Django? [1]  (1) 2024.03.13
Shall We Django? [0] - 시작하기에 앞서  (0) 2024.03.13