이제는 과제를 해보자. 슬라이드 뒤편에 과제 슬라이드를 첨부해 주셨으니 하나씩 읽으면서 해 보자. 

 

✅ 과제 #1: 이해하기

슬라이드를 직접 캡처하기가 애매해서 말로 풀어서 써보겠다.

 

과제 #1에서는 테이블을 만들 때 '코멘트'를 넣는 작업을 하면 된다. 정확히는 django ORM에서 Model의 verbose_name, Field의 verbose_name 값으로 입력한 값이 필드의 '코멘트'가 되도록 하는 작업이다. 

 

사실 나는 '코멘트'가 뭔지 잘 몰랐다. 블로그 글을 보면서 이해한 바로는 '코멘트'는 SQL(mysql, oracle 등) db에서 제공하는 하나의 기능으로, 필드나 테이블의 의미를 나타내기 위한 기능이라고 한다. 

 

아래는 oracle db에서 코멘트를 추가하는 문법이다. 

COMMENT ON TABLE 테이블명 IS '코멘트 내용';

COMMENT ON COLUMN 테이블명.컬럼명 IS '코멘트 내용';

 

어쨌든, 이를 알았으니 과제를 이해해볼 수 있겠다. 내가 알기로는 기본적으로 django model, field에서 있는 verbose_name 값은 db의 comment와 아무런 관계가 없는 것이 기본값이다. 

 

관계를 만들고 싶다면 아마도 DatabaseSchemaEditor를 사용해야 한다고 이해했다. 과제에서 원하는 것은 verbose_name 속성이 create, update, delete 될 때 db의 comment도 같이 업데이트 되는 것이다. verbose_name 속성은 필드나 테이블의 속성을 수정하는 것이므로 migration의 영역이지 queryset의 영역은 아니다. 

 

그리고 일반 migration에서는 이러한 기능을 제공하지 않는다. 정확히 말하면 migration에서도 SQL.runpython(정확한 명령이 맞나 모르겠다)로 raw SQL을 실행시킬 수 있지만, 이러면 매번 필드나 테이블을 변경할 때마다 raw SQL 쿼리를 짜야 한다. 비효율적이라는 말이겠다.

 

그러므로 그 밑단에 있는 DatabaseSchemaEditor의 기능을 재정의하거나 일부 수정해서, 다음과 같은 일들을 하게 하면 되겠다. 

  1. create table/column 작업 시 verbose_name 속성이 있다면 comment 추가하기
  2. update table/column 작업 시 verbose_name 속성이 수정/삭제되었다면 comment 수정하거나 삭제하기

그렇다면 DatabaseSchemaEditor의 source code를 봐 보자.

 

보려고 했는데 source code가 2000줄이 넘는다... 일단은 문서를 보고 이해가 안 되면 코드를 보자. 다행히 이럴 줄 알았는지 SchemaEditor 문서에는 메소드들이 잘 설명되어 있었다. 여기서 필드나 모델을 생성하는 경우, 지우는 경우(지우려는 field나 table에 comment가 있다면 지워야 하므로), 수정하는 경우에 대한 메소드들만 정리해 보았다. 

  • create_model
  • delete_model
  • alter_db_table_comment
  • add_field
  • remove_field
  • alter_field

한 가지 의문은 왜 table의 경우 'alter_db_table_comment' 메소드가 따로 있는데 필드의 경우는 없는지 모르겠다. 정 없으면 alter_field 메소드에서 작업을 대신하면 되니 일단 넘어가자. 

 

그 다음에 할 일은 이 메소드들의 소스 코드만 보고, 작업을 아주 대략적으로 이해하는 것이다. 우선 'create_model' 메소드를 봐 봤다. table_sql이란 다른 메소드를 통해 실행할 SQL을 받아온 다음, 추가적인 작업을 더 하는 것으로 보였다. 그 중에서도 중요한 부분은 db_comment와 관련된 이 부분이었다. 

class BaseDatabaseSchemaEditor: 

    def create_model(self, model):
        """
        Create a table and any accompanying indexes or unique constraints for
        the given `model`.
        """
        sql, params = self.table_sql(model)
        # Prevent using [] as params, in the case a literal '%' is used in the
        # definition on backends that don't support parametrized DDL.
        self.execute(sql, params or None)

        if self.connection.features.supports_comments:
            # Add table comment.
            if model._meta.db_table_comment:
                self.alter_db_table_comment(model, None, model._meta.db_table_comment)
            # Add column comments.
            if not self.connection.features.supports_comments_inline:
                for field in model._meta.local_fields:
                    if field.db_comment:
                        field_db_params = field.db_parameters(
                            connection=self.connection
                        )
                        field_type = field_db_params["type"]
                        self.execute(
                            *self._alter_column_comment_sql(
                                model, field, field_type, field.db_comment
                            )
                        )
        # Add any field index (deferred as SQLite _remake_table needs it).
        self.deferred_sql.extend(self._model_indexes_sql(model))

        # Make M2M tables
        for field in model._meta.local_many_to_many:
            if field.remote_field.through._meta.auto_created:
                self.create_model(field.remote_field.through)

 

한 메소드만 가져와봤는데도 길다... 암튼 여기서 중요한 부분은 'alter_db_table_comment'와 'if field.db_comment' 부분이다. 이 부분에서는 각각 table comment와 column comment를 더해준다는 주석이 적혀있어서 금방 알아볼 수 있었다.

 

그럼 이 부분을 override 해 주면 되겠다. 그런데 migration 수정은 해 봤어도 DatabaseSchemaEditor 수정은 처음 해 본다. 이 부분은 잘 모르겠어서 GPT 찬스를 써 봤다. 

 

크게 세 단계를 거치면 되었다. 

  1. BaseDatabaseSchemaEditor를 상속받는 새 DatabaseSchemaEditor 클래스를 작성한다. 
  2. 각 DB(여기서는 SQLite)의 DatabaseWrapper 클래스를 상속받아서 1번에서 정의한 새 SchemaEditor를 사용함을 선언한다.
  3. 1번과 2번 파일이 들어있는 모듈을 만들고, 1번은 schema.py에, 2번은 base.py 파일에 위치시킨다. 
  4. settings.py의 DATABASES 변수 값 안의 'engine' 속성을 3번의 프로젝트 상대 경로로 지정한다.

그럼 이제 이 방식대로 코딩을 해 보자. 

 

우선 1번, 새 DatabaseSchemaEditor 클래스를 작성해 보자. 관건은 어떻게 하면 '적은 코드를 바꿔서 comment를 수정하도록 할 것인가'였다. 이를 위해서 'comment'로 전체검색을 해 보았다. 그랬더니 다음과 같은 코드가 나오는 게 아닌가. 

sql_alter_table_comment = "COMMENT ON TABLE %(table)s IS %(comment)s"
sql_alter_column_comment = "COMMENT ON COLUMN %(table)s.%(column)s IS %(comment)s"

 

이 변수들을 사용하는 코드를 찾아보았다. alter_table_comment 변수는 alter_db_table_comment에서 사용하고 있었다. 그리고 이 alter_db_table_comment는 create_model 메소드에서 호출되고 있었다. 그렇다면 모델을 만들 때 외에는 comment를 별도로 업데이트하지 않는 것인가? 일단 이 의문을 갖고 넘어가 보자. 

def alter_db_table_comment(self, model, old_db_table_comment, new_db_table_comment):
    if self.sql_alter_table_comment and self.connection.features.supports_comments:
        self.execute(
            self.sql_alter_table_comment
            % {
                "table": self.quote_name(model._meta.db_table),
                "comment": self.quote_value(new_db_table_comment or ""),
            }
        )

 

이번에는 sql_alter_column_comment 변수의 사용처를 찾아보았다. _alter_column_comment_sql에서 사용되고 있었고, 해당 함수는 create_model, add_field, _alter_column_type_sql 함수에서 사용되고 있었다. _alter_column_type_sql에서 사용된 것은 의외였다. column type을 바꾸는 데 comment가 관여할 여지가 있나 의문이다. 

def _alter_column_comment_sql(self, model, new_field, new_type, new_db_comment):
    return (
        self.sql_alter_column_comment
        % {
            "table": self.quote_name(model._meta.db_table),
            "column": self.quote_name(new_field.column),
            "comment": self._comment_sql(new_db_comment),
        },
        [],
    )

 

여기서 왜 코멘트 관련 문법이 이렇게 간단하지 싶었는데, 알고보니 코멘트의 create, update, delete 문법이 모두 같았다. 그래서 위의 두 가지 sql문으로 모두 커버가 되는 것이었다. 그러면 이제는 위에서 언급된, comment 관련 필드를 사용하는 메소드를 override 해 주면 되겠다. 

  • create_model
  • add_field
  • _alter_column_type_sql

 

✅ 과제 #1: 결과물

세 개의 메소드를 재정의하였고, 주 변경 내용은 필드를 바꾼 것이었다. 예를 들면 기존의 table._meta.db_comment로 되어있는 변수 대신 table._meta.verbose_name으로 바꿔 주는 작업이 주였다. 구현해 본 코드는 다음과 같다. (구현한 부분에다가만 주석을 달아보았다)

더보기

 

# custom_engine/schema.py

from django.db.backends.mysql.schema import DatabaseSchemaEditor
from django.db.backends.utils import split_identifier
from django.db.models import NOT_PROVIDED


class CustomDatabaseSchemaEditor(DatabaseSchemaEditor):
    
    def create_model(self, model):
        """
        Create a table and any accompanying indexes or unique constraints for
        the given `model`.
        """
        sql, params = self.table_sql(model)
        # Prevent using [] as params, in the case a literal '%' is used in the
        # definition on backends that don't support parametrized DDL.
        self.execute(sql, params or None)

        if self.connection.features.supports_comments:
            """
            추가한 부분 - 'db_table_comment' 대신 'verbose_name' 사용
            """
            if model._meta.verbose_name:
                self.alter_db_table_comment(model, None, model._meta.verbose_name)
            

            if model._meta.db_table_comment:
                self.alter_db_table_comment(model, None, model._meta.db_table_comment)

            """
            추가한 부분 - 'db_table_comment' 대신 'verbose_name' 사용
            """
            if not self.connection.features.supports_comments_inline:
                for field in model._meta.local_fields:
                    if field.verbose_name:
                        field_db_params = field.db_parameters(
                            connection = self.connection
                        )
                        field_type = field_db_params["type"]
                        self.execute(
                            *self._alter_column_comment_sql(
                                model, field, field_type, field.verbose_name
                            )
                        )
                        

            # Add column comments.
            if not self.connection.features.supports_comments_inline:
                for field in model._meta.local_fields:
                    if field.db_comment:
                        field_db_params = field.db_parameters(
                            connection=self.connection
                        )
                        field_type = field_db_params["type"]
                        self.execute(
                            *self._alter_column_comment_sql(
                                model, field, field_type, field.db_comment
                            )
                        )
        # Add any field index (deferred as SQLite _remake_table needs it).
        self.deferred_sql.extend(self._model_indexes_sql(model))

        # Make M2M tables
        for field in model._meta.local_many_to_many:
            if field.remote_field.through._meta.auto_created:
                self.create_model(field.remote_field.through)


    def add_field(self, model, field):
        """
        Create a field on a model. Usually involves adding a column, but may
        involve adding a table instead (for M2M fields).
        """
        # Special-case implicit M2M tables
        if field.many_to_many and field.remote_field.through._meta.auto_created:
            return self.create_model(field.remote_field.through)
        # Get the column's definition
        definition, params = self.column_sql(model, field, include_default=True)
        # It might not actually have a column behind it
        if definition is None:
            return
        if col_type_suffix := field.db_type_suffix(connection=self.connection):
            definition += f" {col_type_suffix}"
        # Check constraints can go on the column SQL here
        db_params = field.db_parameters(connection=self.connection)
        if db_params["check"]:
            definition += " " + self.sql_check_constraint % db_params
        if (
            field.remote_field
            and self.connection.features.supports_foreign_keys
            and field.db_constraint
        ):
            constraint_suffix = "_fk_%(to_table)s_%(to_column)s"
            # Add FK constraint inline, if supported.
            if self.sql_create_column_inline_fk:
                to_table = field.remote_field.model._meta.db_table
                to_column = field.remote_field.model._meta.get_field(
                    field.remote_field.field_name
                ).column
                namespace, _ = split_identifier(model._meta.db_table)
                definition += " " + self.sql_create_column_inline_fk % {
                    "name": self._fk_constraint_name(model, field, constraint_suffix),
                    "namespace": (
                        "%s." % self.quote_name(namespace) if namespace else ""
                    ),
                    "column": self.quote_name(field.column),
                    "to_table": self.quote_name(to_table),
                    "to_column": self.quote_name(to_column),
                    "deferrable": self.connection.ops.deferrable_sql(),
                }
            # Otherwise, add FK constraints later.
            else:
                self.deferred_sql.append(
                    self._create_fk_sql(model, field, constraint_suffix)
                )
        # Build the SQL and run it
        sql = self.sql_create_column % {
            "table": self.quote_name(model._meta.db_table),
            "column": self.quote_name(field.column),
            "definition": definition,
        }
        # Prevent using [] as params, in the case a literal '%' is used in the
        # definition on backends that don't support parametrized DDL.
        self.execute(sql, params or None)
        # Drop the default if we need to
        if (
            field.db_default is NOT_PROVIDED
            and not self.skip_default_on_alter(field)
            and self.effective_default(field) is not None
        ):
            changes_sql, params = self._alter_column_default_sql(
                model, None, field, drop=True
            )
            sql = self.sql_alter_column % {
                "table": self.quote_name(model._meta.db_table),
                "changes": changes_sql,
            }
            self.execute(sql, params)

        """
        추가한 부분 - 'db_table_comment' 대신 'verbose_name' 사용
        """
        if (
            field.verbose_name
            and self.connection.features.supports_comments
            and not self.connection.features.supports_comments_inline
        ):
            field_type = db_params["type"]
            self.execute(
                *self._alter_column_comment_sql(
                    model, field, field_type, field.verbose_name
                )
            )
            

        # Add field comment, if required.
        if (
            field.db_comment
            and self.connection.features.supports_comments
            and not self.connection.features.supports_comments_inline
        ):
            field_type = db_params["type"]
            self.execute(
                *self._alter_column_comment_sql(
                    model, field, field_type, field.db_comment
                )
            )
        # Add an index, if required
        self.deferred_sql.extend(self._field_indexes_sql(model, field))
        # Reset connection if required
        if self.connection.features.connection_persists_old_columns:
            self.connection.close()

    
    def _alter_column_type_sql(
        self, model, old_field, new_field, new_type, old_collation, new_collation
    ):
        """
        Hook to specialize column type alteration for different backends,
        for cases when a creation type is different to an alteration type
        (e.g. SERIAL in PostgreSQL, PostGIS fields).

        Return a 2-tuple of: an SQL fragment of (sql, params) to insert into
        an ALTER TABLE statement and a list of extra (sql, params) tuples to
        run once the field is altered.
        """
        other_actions = []
        if collate_sql := self._collate_sql(
            new_collation, old_collation, model._meta.db_table
        ):
            collate_sql = f" {collate_sql}"
        else:
            collate_sql = ""

        """
        추가한 부분 - 'db_table_comment' 대신 'verbose_name' 사용
        """
        comment_sql = ""
        if self.connection.features.supports_comments and not new_field.many_to_many:
            if old_field.verbose_name != new_field.verbose_name:
                sql, params = self._alter_column_comment_sql(
                    model, new_field, new_type, new_field.verbose_name
                )
                if sql:
                    other_actions.append((sql, params))


        # Comment change?
        if self.connection.features.supports_comments and not new_field.many_to_many:
            comment_sql = ""
            if old_field.db_comment != new_field.db_comment:
                # PostgreSQL and Oracle can't execute 'ALTER COLUMN ...' and
                # 'COMMENT ON ...' at the same time.
                sql, params = self._alter_column_comment_sql(
                    model, new_field, new_type, new_field.db_comment
                )
                if sql:
                    other_actions.append((sql, params))
            if new_field.db_comment:
                comment_sql = self._comment_sql(new_field.db_comment)
        return (
            (
                self.sql_alter_column_type
                % {
                    "column": self.quote_name(new_field.column),
                    "type": new_type,
                    "collation": collate_sql,
                    "comment": comment_sql,
                },
                [],
            ),
            other_actions,
        )

 

 

이렇게 구현해 보고, 2번 과정인 DatabaseWrapper 클래스를 선언해 주었다. 

# custom_engine/base.py
from custom_engine.schema import CustomDatabaseSchemaEditor
from django.db.backends.sqlite3.base import DatabaseWrapper as SQLite3DatabaseWrapper

class CustomSQLite3DatabaseWrapper(SQLite3DatabaseWrapper):
    SchemaEditorClass = CustomDatabaseSchemaEditor

 

이제는 settings.py의 DATABASES 속성에서 engine의 값을 바꿔보자. 

# settings.py
DATABASES = {
    "default": {
        "ENGINE": "orm.custom_engine",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

 

이렇게 해 주면 과제의 1차 설정은 끝났다. 이제는 과제를 구현한 대로 잘 되는지를 확인해 보자.

 

추가적인 질의를 통해 알게 된 사실인데 Sqlite는 comment 기능이 없다고 한다... 큰일날 뻔했다. comment 기능을 확인하려면 database를 바꿔 주어야 하겠다. 로컬에 설정되어 있는 mysql db로 일단 바꿔주었다.

# settings.py
DATABASES = {
    "default": {
        "ENGINE": "custom_engine",
        "NAME": "django_orm_tutorial",
        "USER": "myuser",
        "PASSWORD": "mypassword",
        "HOST": "127.0.0.1",
        "PORT": "3306",
    }
}

 

DatabaseWrapper 클래스도 mysql의 DatabaseWrapper를 사용하도록 바꿔주었다. 

# custom_engine/base.py
from django.db.backends.mysql.base import DatabaseWrapper
from .schema import CustomDatabaseSchemaEditor

class CustomMysqlDatabaseWrapper(DatabaseWrapper):
    SchemaEditorClass = CustomDatabaseSchemaEditor

 

잘 동작하는지까지 확인하였다. 이제 해야 할 일은 다음과 같다. 

  1. 새 모델 생성하고, verbose_name 값을 설정하기
  2. python manage.py makemigrations && python manage.py migrate
  3. mysql db에 들어가서 해당 모델의 comment 속성이 잘 추가되었는지 확인하기

그리고 이 방법을 따랐다... 그런데 추가가 되지 않았다..!!

 

무슨 일일까. 우선 아래의 명령어를 사용하면 table에 생성된 comment를 볼 수 있다고 해서 그대로 따라해 보았다. 

SELECT 
    table_name, table_comment
FROM
    information_schema.tables
WHERE
    table_schema = 'DB 이름' AND table_name = '테이블 이름';

 

왜 없을까 생각해보니 짚이는 점은 두 가지였다. 

  1. custom backend가 제대로 동작(호출)되지 않았다. 
  2. custom backend의 코드가 잘못되었다. 

우선 1번의 가능성을 보기 위해서 새 모델에 임의의 필드를 추가하여 마이그레이션을 해보고, 그 상황에서 해당 custom backend의 코드가 호출되었는지를 디버거 등으로 살펴봐야 하겠다. 

 

우선 myapp의 모델에서 변경 사항이 실행될 수 있도록 마이그레이션을 되돌려 보자(revert migrations). 해당 명령어를 실행하니 마이그레이션이 잘 되돌려졌다. 

python manage.py migrate myapp 0004

 

마이그레이션을 한 단계 이전으로 되돌렸다. 이제는 다시 migrate 명령어를 실행해서 revert했다가 다시 실행하려는 그 migration이 잘 되는지를 테스트 할 차례인데, 그 전에 우선 custom backend에서 verbose_name 관련해서 새로 작성한 코드들에 전부 breakpoint를 걸어놓았다. 

 

디버깅을 하다 보니 이상한 점이 있었다. custom_engine 디렉토리의 base.py 파일까지는 breakpoint에 걸리는데, 그 안에서 'SchemaEditorClass' 변수 값으로 정의한 CustomDatabaseSchemaEditor 클래스의 세부 메소드는 breakpoint에 걸리지 않았다. 이유가 무엇일까? 일단은 위에서 짚이는 방법 중 1번 원인이 유력해 보였다. 코드가 잘못된 게 아니라 호출되지 않아서 반영되지 않은 것 같았다. 하지만 왜일까. 

 

다른 방법으로 현재 모듈에서 backend engine으로 위에서 정의해 준 backend wrapper를 사용하고 있는지를 알아보았다. 

python manage.py shell
from django.db import connection

print(type(connection))
with connection.schema_editor() as editor:
    print(type(editor))

 

그랬더니 이런 결과가 나왔다. 즉 CustomDatabaseSchemaEditor 클래스가 사용되지 않고 있었다. 이유가 무엇일까?

 

일단 여기서 막힌 상태이다. 좀 더 이유를 찾아봐야겠다...!!

 

✅ 과제 #1: 궁금한 점

  1. 왜 table에서 사용할 수 있는 'alter_db_table_comment' 메소드는 있는데 field에서 사용할 수 있는 전용 메소드를 따로 만들어 주지 않고 다른 방식으로 사용했을까? 
  2. DatabaseWrapper는 왜 필요할까? Django는 왜 SchemaEditor를 그대로 사용하는 대신 DatabaseWrapper에 한번 더 감싸는 구조를 사용했을까? 

 

'server-side > Django' 카테고리의 다른 글

Django ORM 톺아보기 세션: 이해한 내용 정리하기  (1) 2024.12.01
django customizing user  (0) 2024.01.07
django customizing authentication  (0) 2024.01.07
django sessions  (0) 2024.01.06
django migrations  (0) 2023.12.31

지난 달 11월 9일에 Django ORM 톺아보기 세션이 열렸어서 가서 재밌고 유익하게 들었었다. ORM을 쓰는 방법은 그래도 대강은 알고 있다고 생각했는데, 쓰는 방법 말고 동작 원리에 대해서는 모르는 부분이 많았다. 특히 Queryset보다 더 밑단의 동작 원리(SQLCompiler, SQL과 QueySet 사이의 계층)에 대해서는 거의 처음 들어봤다. 역시 배워도 배워도 새롭고 신기한 것들 투성이였다. 

 

다행히 슬라이드는 계속 열어 두신다고 하셔서 지금이라도 슬라이드를 훑어보면서 그때의 기억을 되살려 보았다. 예전에 듣기로는 학습을 하는 좋은 방법 중 하나는 내용을 보면서 무엇을 어떻게 이해했고, 무엇을 모르겠는지를 스스로 점검하는 거라고 했다. 오늘 그걸 해 볼 예정이다. 그러면서 과제도 해 보고, 궁금한 것들을 정리해 보려고 한다. 

 

✅ 흐름 이해

ORM의 정의에 대해서 간단히 다룬 뒤, 처음에는 간단한 유저 모델을 생성하고 유저를 조회하는 API를 만들고 이를 실행시켜 보면서 튜토리얼이 진행되었다. 이후 DDL과 DML을 다루면서 Django에서 ORM이 동작하는 대표적인 두 경우를 봤다. 

 

첫 번째는 DDL(data definition language)으로 ORM을 통해 migration을 쓰는 경우였다. 이 경우는 데이터를 CRUD하는 것이 아니라, 테이블 스키마 자체를 정의하고 바꾸는 작업이다. 두 번째는 DML(data manipulation language)로 ORM을 통해 queryset을 쓰는 경우였다. 데이터를 CRUD하는 경우 쿼리셋이 실행된다고 이해했다. 

 

내가 아는 것은 여기까지였다. 그리고 첫 번째와 두 번째 경우에서, migration과 queryset 아래에 또 동작하는 django의 계층이 있음을 새롭게 알았다. 

 

migration의 경우는 migration과 SQL 사이에 DatabaseSchemaEditor 클래스가 동작해서 python 언어로 쓰여진 migration 파일을 SQL로 바꿔 준다고 이해했다. (사실 깊게 파 보면 그게 다가 아니긴 할 것이다. 일단은 넘어가자!) queryset의 경우는 queryset과 SQL 사이에 SQLCompiler 클래스가 동작해서, python 언어로 쓰여진 queryset 조회 코드를 SQL로 바꿔 준다고 이해했다. 

 

그리고 나는 migration과 queryset이 django ORM하면 거론되는 대표적인 기능들이라서 그 밑단에서는 정보가 많이 없을 것이라고 생각했었는데, DatabaseSchemaEditor 공식 문서가 있었다. SQLCompiler는 'django sql compiler'로 검색했을 때는 정보가 안 나왔는데, 분명 다른 이름으로 뭔가 있을 것이라는 추측을 해 본다. 

 

공식문서 피셜, (Database)SchemaEditor는 db를 추상화하는 계층이란다. 그리고 django에서 지원하는 모든 database backend 별로 SchemaEditor가 있고, 그 모든 SchemaEditor 클래스들은 BaseDatabaseSchemaEditor를 상속받는다고 한다.

 

다른 부분은 그래도 잘 읽혔는데, 아래 부분은 잘 안 읽혔다. 'context manager와 같이 사용해야만 transaction, foreign key constraint 등의 상황에서 사용할 수 있으니, context manager와 같이 사용되어야 한다'는 말이었다. 잘 모르겠어서 GPT에게도 물어봤다. 

 

'왜 context manager가 transaction과 deferred SQL(외래키 관련 migration 등이 있을 때, 해당 SQL을 지연시키는 것)을 지원하는지, 이 context manager는 또 뭔지'가 궁금했었다. GPT 녀석은 질문의 의도를 잘 캐치하고 context manager가 무슨 역할을 하는지를 설명해 주었다. 그런데도 모호한 부분이 있어서, context manager에 대해 질문했다. 

 

알고보니 context manager라는 녀석은 django가 아니라 python 자체에서 지원하는 기능이었다. 설명을 보고선 'with statement와 같이 쓰이는 많은 공통 과제들을 좀 더 편리하게 선언/처리할 수 있게 도와주는 util 함수' 라고 이해했다. 그런데 나는 with statement도 사실 잘 몰랐다(어떻게 쓰는지는 아는데 그게 구체적으로 어떨 때 쓰고, 어떤 원리로 동작하는지는 몰랐다).

 

그래서 조금 삼천포로 빠진 것 같지만 with statement의 정의와 대략적인 동작 원리까지만 찾아보기로 했다. 공식문서를 찾아보니 더 구체적인 정의가 나왔다. with statement로 코드를 실행할 때, 어떤 runtime context를 사용할 것인지를 정의하는 객체가 context manager라고 했다. 

 

context manager는 with문이 실행되는 상황에서 context에 진입하고 종료할 때 부가적인 작업을 해 준다고 이해했다. 즉 with 구문은 일종의 문법이고, 그 문법을 사용해서 코드를 실행할 때는 해당 runtime context의 진입과 종료 작업을 관리하기 위해 context manager가 개입한다고 이해했다. 휴 복잡하다. 

 

암튼 context manager를 이렇게 이해하면 위에서의 말도 이해할 수 있겠다. DatabaseSchemaEditor가 사용되는 raw django code에서는 with문으로 deferred SQL이나 transaction을 처리하는 부분이 있나보다. 그래서 context manager와 같이 사용해야 한다는 설명을 덧붙인 것이겠다. 

 

⚠️

아래 공식문서를 읽고 인증 관련된 내용 중 일부만 발췌해서 정리했습니다.

자세한 내용이 궁금하시다면 공식문서를 참고하시는 것을 추천드립니다. 

https://docs.djangoproject.com/en/5.0/topics/auth/customizing/

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

✅custom user model 설정하기

 

settings.py 파일의 AUTH_USER_MODEL 변수의 값을 "app이름.Model이름" 으로 설정하자. 

AUTH_USER_MODEL = "account.CustomUser"

 

또한 공식문서에서는 인증 모델의 정보에 접근할 때는 쿼리셋을 작성할 때 흔히 사용하는 방식(CustomUser.objects....) 보다는 django.contrib.auth 모듈의 get_user_model() 사용을 권장한다. 

 

이유는 다른 auth user model을 사용하는 다른 프로젝트에서 코드를 사용할 때 충돌이 날 수 있어서라고 한다. 

 

확장성 있는 코드를 작성하고 싶다면 공식문서대로 하는 것이 좋겠고, 다른 프로젝트에서 코드를 사용할 일이 없다면 크게 상관없지 않을까 싶다. 

 

✅인증 정보 구분하기: profile model, proxy model

공식 문서에서는 인증 관련 정보는 user 모델에, 유저와 관련된 그 외 다른 정보는 다른 모델(profile model)에 저장하는 것을 권장한다. 

 

예를 들어 profile model이 Employee라고 하면, Employee와 User 모델의 관계를 1:1 또는 외래키로 설정하는 것이다. 

class Employee(models.Model):
	user = models.OneToOneField(User)
	...

 

반면 인증 관련 정보 외에 추가로 정보를 저장할 필요는 없지만 User 모델이 동작하는 방식을 바꾸고 싶다면 proxy model을 사용하는 것도 좋다. 

 

프록시 모델(proxy model)을 사용하면 유저 모델에서 사용 가능한 메소드나 쿼리셋의 정렬 등을 유저 모델과 다르게 설정할 수 있다. 

class Member(CustomUser):

	class Meta:
		proxy = True

 

✅User model Customizing

공식문서에서는 유저 모델을 정의할 때 django.contrib.auth.models 모듈의 AbstractBaseUser를 상속해서 사용하는 것을 권장한다. 

 

AbstractUser는 장고의 기본 유저 모델인 만큼 인증에서 사용할 수 있는 여러 메소드가 정의되어 있기 때문이다. 

 

물론 AbstractUser를 상속하지 않고도 유저 모델을 새로 정의해서 사용하는 것도 가능하다. 

 

하지만 이 경우 AbstractUser에 있던 기본 메소드들을 따로 정의해 주어야 하기 때문에 불편할 수도 있다. 

 

AbstractUser 모델을 상속해서 커스터마이징 할 경우, 다음과 같은 필드를 설정할 때는 주의해야 한다. 

 

USERNAME_FIELD

각 유저를 unique하게 인식하기 위해 필요한 필드이다. 이 값으로 선언한 필드는 반드시 unique 해야 한다. 

 

REQUIRED_FIELDS

createsuperuser 커맨드를 호출할 때는 이 필드들의 값을 모두 입력해야 한다. 이 필드값들은 unique 할 필요는 없지만 blank=False 이어야 한다. 

class CustomUser(AbstractUser):
	...
	email = models.CharField(max_length=40, unique=True)
    
	USERNAME_FIELD = "email"
	REQUIRED_FIELDS = ["email", "address"]

 

☑️blank와 null의 차이

null은 DB에 값을 저장할 때 null을 허용할지를 결정하고, blank은 DB와는 관련 없는 validation의 영역이다. null=True이어도 blank=False라면 DB에 넣을 값을 검사할 때 허용되지 않는다. 

 

그 외에도 AbstractBaseUser에는 세션이나 비밀번호 설정 등과 관련한 여러 메소드를 제공한다. 

 

is_authenticated

요청을 한 유저가 인증된 유저인지를 boolean 값으로 리턴한다. 

다만 이 값이 True라고 해도 이 유저가 어떤 특정 권한이 있거나 이 유저에 대한 세션이 있는지는 보장하지 않는다. 

 

set_password()

장고에서는 유저의 비밀번호 값을 그대로 저장하지 않고 암호화하여 저장하기 때문에 password의 값을 그대로 바꾸면 로그인이 되지 않는다. 

그 대신 set_password() 함수를 사용하면 해당 값이 암호화되어 저장된다. 

 

set_unusable_password()

장고에서 비밀번호를 사용하지 않을 때도 이 메소드를 사용하면 좋다. 

비밀번호에 공백 값을 넣는 것과 비밀번호를 사용하지 않는 것은 다른데, 이 메소드는 후자일 경우에 사용하자. 

 

get_session_auth_hash()

유저의 비밀번호를 HMAC 방식으로 암호화한 값을 리턴한다. (정확히 어떤 데 쓰이는지는 아직 모르겠다...)

 

 

☑️AbstractUser

만약 유저 모델의 특성을 거의 바꿀 것 같지 않다면 AbstractBaseUser를 상속한 AbstractUser 모델을 그대로 사용하는 것도 가능하다. 

 

이 모델에서는 USERNAME_FIELDREQUIRED_FIELDS 옵션도 이미 정의되어 있다.

 

AbstractBaseUser의 설정을 대부분 따르되 그 중 일부만 변경하고 싶다면 AbstractUser를 사용하면 코드를 간결하게 작성할 수 있다. 

class CustomUser(AbstractUser):
	pass

 

✅User manager Customizing

기본적으로, 각 모델에는 objects라는 이름을 가진 manager 클래스가 있다. 

 

이 manager 클래스는 모델이 어떤 쿼리를 통해서 데이터베이스와 어떻게 상호작용할지를 결정하는 일종의 인터페이스 역할을 한다. 

 

 

☑️BaseManager

 

유저 모델의 manager 클래스는 django.contrib.auth.models 모듈의 BaseManager 클래스를 상속한다. 

 

get_by_natural_key()

BaseManager에서 유저를 찾는 데 사용하는 메소드이다. 

createsuperuser 등의 커맨드에서도 호출된다. 

유저에서 USERNAME_FIELD로 선언한 필드의 값을 사용해서 유저를 찾는다.

 

 

☑️CustomManager

 

이 클래스를 상속해서 커스텀 manager 클래스를 만들고 싶다면 두 가지 메소드를 재정의해야 한다. 

 

create_user()

유저를 생성할 때 호출되는 메소드이다.

유저 모델에서 USERNAME_FIELDREQUIRED_FIELDS로 선언한 필드들은 모두 매개변수로 받아야 한다. 

 

create_superuser()

createsuperuser 커맨드를 사용할 때 호출된다. 

마찬가지로 USERNAME_FIELDREQUIRED_FIELDS로 선언한 필드들은 모두 매개변수로 받아야 한다. 

 

 

참고한 포스트

https://docs.djangoproject.com/en/5.0/ref/models/fields/#:~:text=null%20is%20purely%20database%2Drelated,the%20field%20will%20be%20required.

https://docs.djangoproject.com/en/5.0/topics/db/managers/

 

⚠️

아래 공식문서를 읽고 인증 관련된 내용 중 일부만 발췌해서 정리했습니다.

자세한 내용이 궁금하시다면 공식문서를 참고하시는 것을 추천드립니다. 

https://docs.djangoproject.com/en/5.0/topics/auth/customizing/

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

✅django authentication

장고에서는 인증을 수행하기 위한 여러 백엔드 클래스(authentication backends)들이 있다. 

 

settings.py 설정 파일에서 AUTHENTICATION_BACKENDS 라는 변수 값으로 이 인증 백엔드 클래스를 어떻게 설정할지를 지정할 수 있다. 

 

인증 백엔드가 여러 개일 경우, 맨 위에 선언한 클래스부터 순차적으로 인증이 실행된다. 상위 클래스에서 인증이 완료되면 하위 클래스는 실행되지 않는다. 

 

기본값은 ModelBackend이다. 

AUTHENTICATION_BACKENDS = [
	"django.contrib.auth.backends.ModelBackend"
]

 

인증 백엔드는 세션과도 연관이 있다. 하나의 세션에서는 인증에 사용하는 백엔드를 캐싱해 두고 인증할 때 그 클래스를 사용한다. 

 

django.contrib.auth 모듈의 authenticate()를 호출하면 장고의 모든 인증 백엔드에서 순차적으로 인증을 시도한다. 

 

☑️login() vs authenticate()

인증을 수행한다는 공통점이 있다.

authenticate(): 인증 정보가 유효한지를 한 번 확인한다.

login(): 인증 정보가 유효한지를 확인하고 유효하다면 세션을 만드는 등의 후처리를 한다. 그 결과 한 번 인증된 유저를 여러 번 인증할 필요가 없게 한다. 

 

✅Custom Authentication

기본 인증을 사용하지 않고 새로운 인증 클래스를 만들어서 사용해야 할 때도 있다.

 

예를 들면 서로 다른 두 앱(django app)의 유저를 통합하거나 할 때, 두 앱에서 모두 사용할 수 있는 인증 클래스가 필요할 것이다. 

 

새로운 인증 백엔드 클래스(Authentication Backend)를 만들기 위해서는 크게 두 가지가 필요하다. 

  1. django.contrib.auth.backends 모듈의 BaseBackend 클래스를 상속하는 새 클래스 만들기
  2. 해당 클래스에서 두 가지 메소드 구현하기
  • authenticate: 요청 및 필요시 추가 정보(credentials)를 받아서 인증 되면 user, 아니면 None을 리턴한다. 
  • get_user: user 인스턴스의 pk 값을 받아서 user를 리턴한다. 
from django.contrib.auth.backends import BaseBackend

class CustomBackend(BaseBackend):

	def authenticate(self, request, credentials=None):
		...
        
	def get_user(self, user_id):
		...
        
   ...

 

 

✅Authentication Backend에서 권한(Permissions) 부여하기

BaseBackend 클래스에서는 User 모델 및 UserManager 클래스에서 정의하는 여러 권한 조회 함수들(permission lookup functions)을 사용할 수 있다. 

  • get_user_permissions: 해당 유저가 직접적으로 가진 권한들을 리턴한다. 
  • get_group_permissions: 해당 유저가 속한 그룹이 가진 권한들을 리턴한다. 
  • get_all_permissions: 위의 두 메소드의 합집합을 리턴한다. 
  • has_perm(perm): 유저가 해당 권한을 가지고 있는지를 boolean 값으로 리턴한다. 
  • has_module_perms(module_name): 유저가 해당 모듈(django app)에 해당하는 권한을 하나라도 갖고 있다면 True를 리턴한다. 

만약 시행 중 Permission Denied 에러가 리턴되면, 미들웨어처럼 이후의 인증 백엔드를 거치지 않고 에러를 리턴한다. 

 

✅특정 모델 인스턴스에 대해 권한 생성하기

모델 클래스의 Meta 옵션에 permissions 속성을 추가하고, 그 값으로 추가하고 싶은 권한들을 작성한다. 

권한들도 DB를 사용하는 경우 하나의 테이블로 맵핑되기 때문에, 이렇게 추가한 권한 값들은 migrate 명령어를 실행한 이후에 생성되므로 그 이후에 사용할 수 있다. 

class Task(models.Model):
	...
	class Meta:
		permissions = [
			("change_task_status", "Can change the status of tasks"),
			("close_task", "Can close the status of tasks"),
		]

 

 

참고한 포스트

https://stackoverflow.com/questions/28249276/whats-the-difference-between-authenticate-and-login

 

'server-side > Django' 카테고리의 다른 글

Django ORM 톺아보기 세션: 이해한 내용 정리하기  (1) 2024.12.01
django customizing user  (0) 2024.01.07
django sessions  (0) 2024.01.06
django migrations  (0) 2023.12.31
django routers  (1) 2023.12.21

아래 공식문서를 읽고 세션에 관련된 기본 내용만 발췌하여 정리했습니다. 자세한 내용이 궁금하시다면 공식문서를 참고하시는 것을 추천드립니다. 

https://docs.djangoproject.com/en/5.0/topics/http/sessions/

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

Session

  • 세션은 인증에서 사용되는 방법 중 하나로, 서버에 접속한 개별 클라이언트마다 인증에 필요한 정보를 서버에 저장해 두고 관리하는 방식이다.
  • 인증 정보를 서버 측에서 관리하기 때문에 보안 면에서 더 안전하고 정보가 탈취될 가능성이 더 적다.
  • 반면 사용자가 많아질수록 그 많은 사용자의 정보를 서버에서 모두 관리해야 하기 때문에 서버의 부하가 커질 수 있고, DB 등 정보를 저장할 곳이 필요하기 때문에 확장성이 떨어진다는 단점도 있다. 

 

Django Session

장고에서는 세션을 미들웨어(middleware)로 구현한다.

장고의 설정 파일(settings.py)의 middleware 변수에는 django.contrib.sessions.middleware.SessionMiddleware 클래스가 기본으로 추가되어 있는데, 이 미들웨어가 장고에서 기본 세션을 구현하는 역할을 한다. 

만약 장고에서 세션을 사용하고 싶지 않다면 이 미들웨어를 middleware 변수에서 빼 주자. 

 

앞서 세션은 인증에 필요한 정보를 서버 측에 저장하는 방식이라고 했다. 

장고에서는 이 정보를 어디에 저장하는지에 따라서 다양한 세션 엔진(session engine)의 종류가 있다. 

 

DB

  • 장고 세션에서 사용하는 기본 설정이다.
  • 사용하는 DB에 세션 정보를 저장하는 전용 테이블을 만들어서 그 안에 세션 정보를 저장한다.
  • 이 경우 Session 객체를 통해 개별 세션의 정보에 접근할 수 있다. 

Cache

  • 공식문서에서는 Redis나 MemCached 같은 인메모리 데이터 저장소를 사용하는 경우에만 사용하는 것을 추천한다. 
  • 로컬 메모리 캐시를 사용할 경우 데이터를 오랫동안 저장해두지 못할 수 있고, 로컬 메모리 캐시는 멀티 프로세스 환경에 적합하지 않기 때문이다(not multi-process safe, 멀티 프로세스 환경에서 발생하는 동시성 에러 등으로 데이터가 변경될 수 있다). 
  • 인메모리 데이터 저장소에서는 데이터를 key-value 쌍으로 저장하기 때문에, 이 경우 key는 각 세션을 구분하는 session key가 될 것이고 세션에 저장할 데이터가 value 값이 된다. 

Cookie

  • http 통신에 사용하는 쿠키 형태로 세션 데이터를 저장하는 방식이다. 
  • 다만 쿠키 데이터를 cryptography 방식으로 암호화하여 저장한다. 

File

  • 파일 형태로 로컬 디렉토리에 저장한다.
  • 기본 값으로는 /tmp 디렉토리에 저장되고, 기본 디렉토리를 바꾸고 싶다면 설정 파일의 SESSION_FILE_PATH 변수 값을 따로 지정하면 된다. 

 

Session object 특징

  • session_key: 이 값으로 각 세션을 구분한다. session key는 40글자 이내의 문자열이다. 
  • expiry_date:  각 세션이 언제 만료되는지를 초 값으로 저장한다. 
  • session_data: dictionary 형태로 인증에 필요한 정보를 저장한다.

 

Sesssion 특징

서버에서 브라우저와 통신할 때는 쿠키를 사용하는데, 이 쿠키에 현재 세션을 unique하게 구분하기 위한 session key 값이 들어 있다. 만약 session key 값이 잘못되었거나 해당 세션의 만료 시점이 지났다면 인증이 정상적으로 시행되지 않는다. 

 

세션 데이터는 사용하는 세션 엔진에 따라서, 또는 커스텀 세션의 경우는 직접 정의한 encoder 및 decoder에 따라서 값 그대로 저장되지 않고 인코딩 되어 저장된다. 

 

세션 데이터의 값이 수정될 때마다 세션 값이 새로 저장되는 것이 기본적이다. 만약 그렇게 하지 않고 매번 요청할 때만 값을 새로 저장하고 싶다면, 설정 파일에서 SESSION_SAVE_EVERY_REQUEST 변수 값을 True로 바꿔주자. 

 

세션은 세션 정보를 저장하는 쿠키의 만료 시점까지 지속되는 것이 기본 설정이다. 그러나 브라우저를 닫을 때 관련 세션이 닫히게 하고 싶다면, 설정 파일의 SESSION_EXPIRE_AT_BROWSER_CLOSE 변수 값을 True로 바꿔주자. 

 

세션이 만료된 경우에도 DB 등에 저장된 세션 데이터는 자동으로 사라지지 않고 남아있다. 이 경우 clearsessions 명령어를 통해 만료된 세션 데이터를 주기적으로 지워 주어야 한다. 

 

 

그 외 참고한 포스트들

https://docs.djangoproject.com/en/5.0/topics/http/sessions/

https://zangzangs.tistory.com/72

https://superfastpython.com/process-safe-in-python/

 

'server-side > Django' 카테고리의 다른 글

django customizing user  (0) 2024.01.07
django customizing authentication  (0) 2024.01.07
django migrations  (0) 2023.12.31
django routers  (1) 2023.12.21
django apps  (0) 2023.12.20

migration(마이그레이션)이란

장고에서 모델과 연결된 DB의 버전을 관리하는 방법이다. migration은 파이썬 파일 형식으로 관리된다. 

 

migration의 특징

  1. 실제 DB에서 변경되는 내용이 없어도 migration 파일이 만들어질 수 있다. 
  2. 어떤 DB에는 적용 가능한 내용이 다른 DB에는 적용 불가능할 수 있다. (MySQL의 경우 PostgreSQL보다 컬럼에 사용하는 max_length 속성의 최댓값이 작다)
  3. 직접 migration 파일을 수정할 수 있고 빈 migration 파일을 만들 수도 있다. 
  4. transaction 개념이 있는 DB(postgresql, sqlite)의 경우, migration은 하나의 transaction 안에서 실행된다. 그렇지 않은 경우는 transaction 없이 실행되고, 이 경우 migration 도중 failure가 발생하면 rollback이 되지 않는다. 
  5. 맨 처음 만들어진 migration(initial migration)이 아닌 경우, migration은 다른 migration에 의존한다. 

 

migration이 만들어질 때

python manage.py makemigrations 커맨드를 입력하면 migration 파일이 만들어진다. 

migration 파일에는 여러 속성들이 있지만, 그 중에서도 dependencies와 operations가 대표적이다. 

dependencies는 해당 migration이 다른 어떤 migration에 의존하고 있는지를 나타낸다. 

operations는 해당 migration이 해당 앱의 모델을 어떻게 변형할지를 나타낸다. 

 

makemigrations 명령어에 따라 migration 파일이 만들어지는 원리

  1. 장고는 이전에 있던 migration들을 순서대로 실행한다. 
  2. 현재 모델의 상태와 1번의 과정에서 얻은 모델의 상태를 비교한다. 만약 변화된 점이 없다면 'detected no changes'와 유사한 문구가 뜬다. 
  3. 2번에서 현재 모델과 migration 파일들을 순차적으로 실행하면서 얻은 모델의 상태에 차이가 있다면, 그 차이점을 바탕으로 migration 파일을 만든다. 

migration 관련 명령어들

  • makemigrations: 위의 과정으로 migration 파일을 만든다. 
  • migrate: makemigrations으로 만들어진 파일이 있는 경우 해당 내용을 실행해서 DB 스키마를 바꾼다. 만약 모델에 변화가 있는데 makemigrations 없이 migrate를 실행한 경우, migration 파일이 만들어지고 그 파일에 대해서 바로 migrate 명령어가 이어서 실행된다. 
  • sqlmigrate: migrate와 똑같이 동작하는데, 다만 동작 과정에서 어떤 SQL문이 실행되는지를 보여준다. 
  • showmigrations: 현재 어떤 migration들이 있는지를 보여준다. migration 파일은 있는데 DB에 반영되지 않은 경우에는 체크 표시가 없고, migration 파일도 있고 DB에 정상적으로 반영된 경우 체크 표시가 있다. 
  • squashmigrations: 기존에 있던 여러 개의 migration 파일을 압축하는 커맨드이다. 

 

apply migration

python manage.py migrate {app_label}

특정 앱에 대해서만 migration을 적용하고 싶을 때 사용한다. 

 

reverse migration 

python manage.py migrate {app_label} {number/migration name}

특정 앱에 대해서 이전 migration으로 진행 상태를 돌리고 싶을 때 사용한다. migration의 번호를 입력하면 해당 번호까지의 migration이 실행된 상태로 돌려 준다. 

물론 항상 가능한 것은 아니고, 되돌리지 못하는 migration의 경우는 IrreversibleError가 발생할 수 있다. 

 

squash migration

python manage.py squashmigrations {app_label} {number/migration name}

기존 여러 개의 migration 파일을 1개 혹은 몇 개로 줄이는 작업이다. 최초 migration부터 입력한 번호까지의 migration을 압축한다고 볼 수 있다. 기존 migration보다 파일의 개수나 operation의 개수는 줄어들지만 DB에 적용되는 내용은 똑같다. 

 

작업 원리

  1. squash 대상이 되는 모든 migration에서 operation을 추출해서 순서대로 정렬한다. 
  2. 1번 결과에 대해 optimizer를 실행해서, 중복되는 operation은 생략하는 등 operation의 개수를 줄인다. (ex. CreateModel과 DeleteModel이 같이 있다면 두 operation 모두 삭제하기)
  3. 2번의 결과를 migration 파일로 만든다. 

 

squashed migration -> normal migration

squashed migration(squashmigration의 결과로 생성된 파일)은 해당 파일의 replace 속성이 있어서 일반 migration과는 달리 "기존의 migration을 대체한" 파일이다. 

이 파일이 일반 migration처럼 동작하게 하려면 추가 작업이 필요하다. 

  1. squashmigrations 명령어 실행하기
  2. 1번의 migration 파일이 대체한 기존 migration 파일들 삭제하기
  3. 2번에서 삭제한 migration 파일에 의존하는 다른 파일들의 dependencies 속성을 1번에서 새로 생성된 squashed migration 파일로 수정하기
  4. 1번 파일에 표시된 replaces 속성을 삭제하기

 

fake migration

이미 DB에는 SQL문이 실행되어 테이블과 컬럼이 있으나 장고 migration에서는 반영되지 않았을 때 --fake 옵션을 사용한다. 

즉 장고에서 관리하는 migration의 진행 상태만 진행한 것으로 변화시키고, 실제로 DB에 SQL문은 실행하지 않는 것을 fake migration이라고 한다. 

 

initial migration

맨 처음 실행되는 mgiration으로, 다른 migration에 의존하지 않는다. 

보통 initial migration은 1개이지만 2개 이상의 initial migration을 만들 수도 있다. 

--fake-initial 옵션을 붙이면 이미 DB에서 테이블과 컬럼이 만들어진 경우 초기 migration을 적용하지 않고 스킵할 수 있다. 다만 이 옵션은 적용하려고 했던 initial migration에 선언된 모델이 DB에 존재하는지가 확인되어야 적용할 수 있다. (적용하려는 모델이 없는데 --fake-initial 옵션을 쓸 수는 없다.)

 

RunPython

기존의 migration 파일을 작성하는 방법이 아니라, python 코드로 DB 내부의 내용을 변경할 수 있다. 

(RunPython과 비슷하게는 RunSQL도 있다. 이 경우 SQL문을 직접 장고에서 실행할 수 있다.)

 

주의 사항

1. RunPython으로 현재 앱이 아닌 다른 앱에 있는 모델에 접근할 경우, migration 파일의 dependencies 속성에다 다음 2가지를 꼭 추가해야 한다. 

  • 현재 app의 initial migration
  • 접근하는 다른 앱의 가장 최신 migration

그렇지 않으면 LookupError 에러가 발생한다. 

 

2. RunPython으로 data migration 작업을 할 수 있다. 

  1. python manage.py migrate {app_label} --empty 커맨드로 빈 migration 파일을 만든다. 
  2. 해당 파일의 dependencies 속성에는 해당 앱의 initial migration만 추가한다. 
  3. operations에는 migrations.RunPython()을 명시하고, 괄호 안에는 callable(실행 가능한 함수의 이름)을 적는다. data migration 작업을 하는 함수를 만들고 그 함수의 이름을 callable로 적으면 된다. 

 

'server-side > Django' 카테고리의 다른 글

django customizing authentication  (0) 2024.01.07
django sessions  (0) 2024.01.06
django routers  (1) 2023.12.21
django apps  (0) 2023.12.20
models and databases  (0) 2023.09.16

Routing(라우팅)

서버로 들어온 요청에 맞는 리소스나 페이지로 요청을 이동시키는 것이다. 

 

Routing 방법들

1. path 사용

path("경로", Viewset)

from django.urls import path

urlpatterns = [
	path("/path", ViewSet.as_view()),
]

 

2. router 사용

router.register("경로", Viewset)

from rest_framework import routers

router = routers.SimpleRouter()
router.register(r"path", ViewSet)
urlpatterns = router.urls

 

3. @action decorator 사용

path, router과는 조금 다른 방법으로, 기본적인 url mapping을 따르지 않거나, view의 역할이 기본적인 CRUD가 아닐 때, 또는 추가적인 permission 등을 지정하고 싶을 때 사용한다. 

class TempViewSet(ModelViewSet):
	
    @action(methods=['post'], detail=True, permission_classes=[IsAdminOnly], url_path="tempapi")
    def temp_view(self, request):
		...

methods: 해당 view에 접근 가능한 http method를 리스트 형식으로 지정한다. 

detail: 해당 view가 모델 개별 인스턴스에 대한 정보를 다루는지, 모델 전체 인스턴스들에 대한 정보를 다루는지를 boolean 값으로 표현한다. 

permission_classes: 해당 view에 어떤 permission 클래스에 해당해야 접근할 수 있는지를 리스트 형식으로 지정한다. 

url_path: @action을 사용하는 경우 보통 url 경로는 메소드의 이름으로 정해진다. 만약 url 경로를 메소드의 이름과 다르게 설정하고 싶다면 설정해주면 된다. 

 

Router: SimpleRouter와 DefaultRouter

1. 차이점

- DefaultRouter는 프로젝트의 모든 url이 전부 명시되어 있는 기본 페이지를 제공한다. 

- DefaultRouter는 api를 제공할 때 .json 형식으로도 제공한다. 

 

2. url path, http method, action에 따라 url을 mapping 하는 방식

url style http method action
기본 path/ GET LIST
기본 path/ POST CREATE
기본 path/해당 url path/ 지정 X @action(detail=False)
기본 path/pk/ GET RETRIEVE
기본 path/pk/ PUT UPDATE
기본 path/pk/ PATCH PARTIAL-UPDATE
기본 path/pk/ DELETE DESTROY
기본 path/pk/해당 url path/ 지정 X @action(detail=True)

 

+

또한 django에서 제공하는 router들은 기본적으로 모든 url 뒤에 '/'를 붙이는 것이 기본이다. 

그러나 router를 선언할 때 trailing_slash 옵션을 False로 선언하면 url의 맨 뒤에 '/'가 붙지 않도록 할 수 있다. 

router = routers.SimpleRouter(trailing_slash=False)

 

'server-side > Django' 카테고리의 다른 글

django sessions  (0) 2024.01.06
django migrations  (0) 2023.12.31
django apps  (0) 2023.12.20
models and databases  (0) 2023.09.16
python - poetry 사용하기  (0) 2023.07.12

Application(앱)이란

django-admin startproject myproject

이 커맨드로 장고 프로젝트(project)를 처음 만들면 기본 디렉토리에 asgi, wsgi, settings 등의 파일이 생성된다.

이 파일들은 프로젝트 전역에서 사용되는 파일로, 하나의 앱(application)에 소속되어 있지 않다. 

 

django-admin startapp myapp

여기서 이 커맨드로 myproject에 소속된 myapp 앱을 만들면 프로젝트 디렉토리 기준으로 /myapp 이라는 디렉토리가 생기고, 이 디렉토리 안에는 apps, models, views, tests 등 여러 파일이 생긴다. 이 파일들은 해당 myapp 앱 안에 소속되어 있다. 

 

장고 앱은 하나의 프로젝트 안에서도 여러 역할이나 기능이 나누어질 수 있기 때문에, 그 기능이나 역할별로 코드를 효율적으로 작성하는 데 도움이 된다. 

 

App 선언하고 사용하기

startapp 커맨드로 앱을 만든 뒤에는 이 앱을 장고가 인식할 수 있게 해야 한다. 

프로젝트 디렉토리의 settings.py의 INSTALLED_APPS라는 리스트 형식의 변수에다가 해당 앱 디렉토리 까지의 경로를 추가해 준다. 

 

이때 앱 디렉토리까지의 경로만 입력해도 장고가 앱을 인식할 수 있는 이유는, 앱 안의 apps.py에 기본적으로 AppConfig 클래스가 정의되어 있기 때문이다. 

이 클래스는 AppConfig라는 클래스를 상속하여야 하고, 한 앱당 최소 1개 이상은 있어야 한다. 

다만 꼭 apps.py 파일 안에 있을 필요는 없다. AppConfig 클래스는 해당 앱의 바깥에다가 정의해도 상관없다. 대신에 이 경우 INSTALLED_APPS에는 해당 AppConfig 클래스까지의 경로를 정확히 명시해 주어야 한다. 

 

AppConfig는 각 장고 앱에 관련된 메타 데이터를 저장하는 클래스이다. 장고에서는 각 앱마다 있는 AppConfig 클래스를 상속한 이 AppConfig 객체에 메타 데이터를 저장한다. 

반면 Application 객체는 없다. 그냥 AppConfig 객체에 각 앱의 실행과 관리에 필요한 여러 정보를 저장할 뿐이다. 

 

AppConfig class

name: 프로젝트 디렉토리에서 해당 앱까지의 경로이며, 모든 앱은 이 경로가 서로 달라야 한다. 

label: 각 앱을 구분하는 이름이고, 이 이름도 서로 달라야 한다. 

verbose_name: human-readable 이름이다. 이 이름은 서로 겹칠 수도 있다. 

path: 파일 시스템에서 해당 앱까지의 경로이다. 

default: 하나의 앱에 여러 AppConfig 클래스가 있을 때, 어떤 것을 기본으로 사용할지 지정하는 boolean 필드이다. 

default_auto_field: 만약 앱 내에서 모델을 생성한다면, 그 모델의 AutoField를 어떤 타입으로 할지를 지정한다. 기본값은 BigAutoField이다. 

 

앱이 로딩되는 순서

1. 맨 처음 장고가 시작된다. (즉 django.setup() 이 실행된다.)

2. settings.py 파일에 선언된 변수들을 import 한다. 

3. 이때 INSTALLED_APPS 안에 선언된 앱들을 위에서 아래로 차례대로 import 한다. 이 과정에서 각 앱의 AppConfig 인스턴스가 생성된다. 

4. 각 앱에 있는 모델이 있다면 그 모델들을 import 한다. 

5. 각 앱의 AppConfig의 메소드인 ready()를 호출한다. 이 ready()가 호출되면 비로소 앱은 실행을 위해 준비된 상태가 된다. ready() 메소드는 override해서 각 AppConfig 클래스마다 새로 정의할 수 있다. 이 메소드가 실행 중일 때는 앱이 아직 준비되지 않은 상태이기 때문에, DB에 접근하는 코드 등은 지양해야 한다. 이 경우 AppRegistryNotReady 에러가 발생할 수 있다. 

 

'server-side > Django' 카테고리의 다른 글

django migrations  (0) 2023.12.31
django routers  (1) 2023.12.21
models and databases  (0) 2023.09.16
python - poetry 사용하기  (0) 2023.07.12
signals  (0) 2023.06.21

+ Recent posts