티스토리 뷰

파이썬/장고

[Django] [Multiple DB] DB 한개로는 부족해!

글을 쓰는 개발자 2021. 12. 19. 15:15
반응형

이 주제에 대해서 알아보게 된 것은 최근 제가 다니고 있는 회사에서 하나의 DB로 데이터들을 처리할려고 하니 이에 대한 해결책이 뭐가 있을 까 하고 고민을 해봤습니다.

처음에 내가 생각한 것은 다음과 같습니다.

1. MSA (MIcro Soft Arhitecture) :  마 ~ 우리가 자존심이 있지 쪼개보자!!
2. DB 분산 작업:  기존 애플리케이션 내부 DB를 나누게 되면 이에 대한 쓰기 작업을 분산 시킬 수 있지 않을까?

1번은 우리 회사에서 하기에는 인원도 부족하고, 장기적으로 바라보는 작업이기에 현실적으로 바로 접근하기에는 문 턱이 높기 때문에 저는 2번을 먼저 해보는 게 좋지 않을까 해서  조사하게 되었습니다.

 

 

제가 찾아본 공식문서는 다음과 같습니다.

https://docs.djangoproject.com/en/4.0/topics/db/multi-db/

 

Multiple databases | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

멀티 DB로 쪼개는 것에 대한 저의 예제를 보여드리도록 하겠습니다.

 

프로젝트의 디렉토리는 다음과 같습니다.

 

 

DB - docker-compose.yml

version: '3.3'

services:
  postgres1:
    container_name: primary-db
    image: postgres:13.4
    restart: unless-stopped
    ports:
      - "35432:5432"
    environment:
      POSTGRES_DB: "primary_db"
      POSTGRES_HOST_AUTH_METHOD: "md5"
      POSTGRES_PASSWORD: "primarydb"
      POSTGRES_USER: "primary_user"
    volumes:
      - default-db:/var/lib/postgresql/data
  postgres2:
    container_name: log-db
    image: postgres:13.4
    restart: unless-stopped
    ports:
      - "45432:5432"
    environment:
      POSTGRES_DB: "log_db"
      POSTGRES_HOST_AUTH_METHOD: "md5"
      POSTGRES_PASSWORD: "logdb"
      POSTGRES_USER: "log_user"
    volumes:
      - log-db:/var/lib/postgresql/data

volumes:
  default-db:
  log-db:

DB 구성은 다음 도커 컴포즈로 실행해주세요

deocker-compose up -d --build

 

account.models.py

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.base_user import BaseUserManager


class UserManger(BaseUserManager):

    def create_user(self, email, username, password, **extra_fields):
        if not email:
            raise ValueError('The email must be set.')
        email = self.normalize_email(email)
        user = Account()
        user.set_email(email)
        user.username = username
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, username, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True')
        return self.create_user(email, username, password, **extra_fields)


class Account(AbstractUser):
    email = models.EmailField(unique=True)
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    objects = UserManger()

    def set_email(self, email):
        self.email = email

    def __repr__(self):
        return f'아이디: {self.id}, 이름 : {self.username}, 이메일: {self.email}'

해당 모델은 간단한 유저 모델입니다. 

 

 

blog.models.py

from django.db import models

from account.models import Account


class Blog(models.Model):
    account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='blogs')
    title = models.CharField(max_length=25)
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)

    objects = models.Manager()

    def __repr__(self):
        return f'title : {self.title}'

 

 

log.models.py

from django.db import models
from django.utils.translation import gettext_lazy as _
from account.models import Account


class LogType(models.TextChoices):
    LOGIN = 'login', _('LOGIN'),    # NAME / VALUE / LABEL
    LOGOUT = 'logout', _('LOGOUT')

    @staticmethod
    def index_to_type(index):
        if index == 0:
            return LogType.LOGIN
        else:
            return LogType.LOGOUT


class Log(models.Model):
    account = models.ForeignKey(Account, related_name='logs', null=True, on_delete=models.CASCADE)
    model_type = models.CharField(max_length=20, choices=LogType.choices, default=LogType.LOGIN)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    objects = models.Manager()

    def __repr__(self):
        return f'model_type: {self.model_type}'

 

모델들은 다음과 같이 간단하게 구성하였습니다.

 

이제 중요한 settings.py를 보도록 할까요?

 

settings.py (✌゚∀゚)☞

 

INSTALLED_APPS

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 추가된 항목
    'rest_framework',
    'log.apps.LogConfig',
    'blog.apps.BlogConfig',
    'account.apps.AccountConfig'
]

 

DATABASES

DATABASES = {
    'default': {},
    'primary_db': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'primary_db',
        'USER': 'primary_user',
        'PASSWORD': 'primarydb',
        'HOST': '127.0.0.1',
        "PORT": "35432",
    },
    'log_db': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'log_db',
        'USER': 'log_user',
        'PASSWORD': 'logdb',
        'HOST': '127.0.0.1',
        "PORT": "45432",
    },
}

 

🌟🌟🌟DATABASE_ROUTES 🌟🌟🌟

DATABASE_ROUTERS = ['config.defaultRouter.DefaultRouter', 'config.logRouter.LogRouter']

해당 설정은 각 모델에 따른 DB를 어떤 DB로 라우팅 할 것인지 설정하는 환경변수입니다. 

정말 중요한 설정이니 Multiple Database을 할 때에는 필수적입니다.

 

AUTH_USER_MODEL

AUTH_USER_MODEL = 'account.Account'

 

⭐⭐- Router -⭐⭐

config.defaultRouter.py

 

class DefaultRouter:
    route_app_labels = {'account', 'blog'}
    db_name = 'primary_db'

    def db_for_read(self, model, **hints):
        """
        Attempts to read logs and others models go to primary_db.
        """
        if model._meta.app_label in self.route_app_labels:
            return self.db_name
        return None

    def db_for_write(self, model, **hints):
        """
        Attempts to write logs and others models go to primary_db.
        """
        if model._meta.app_label in self.route_app_labels:
            return self.db_name
        return None

    def allow_relation(self, obj1, obj2, **hints):
        """
        Allow relations if a model in the obj1 or obj2 apps is
        involved.
        """
        if (
                obj1._meta.app_label in self.route_app_labels or
                obj2._meta.app_label in self.route_app_labels
        ):
            return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        Make sure the auth and contenttypes apps only appear in the
        'primary_db' database.
        """
        if app_label in self.route_app_labels:
            return self.db_name
        return None

 

저는 기본적인 구성 테이블들을 defaultRouter 에 설정해놨습니다.

 

route_app_labels = {'account', 'blog'}

이 부분에 독자들이 원하는 앱들을 적어주시면 됩니다.

db_name = 'primary_db'

이 부분에는 db 이름을 적어주세요

 

config.logRouter.py

 

class LogRouter:
    route_app_labels = {'log'}
    db_name = 'log_db'

    def db_for_read(self, model, **hints):
        """
        Attempts to read logs and others models go to log_db.
        """
        if model._meta.app_label in self.route_app_labels:
            return self.db_name
        return None

    def db_for_write(self, model, **hints):
        """
        Attempts to write logs and others models go to log_db.
        """
        if model._meta.app_label in self.route_app_labels:
            return self.db_name
        return None

    def allow_relation(self, obj1, obj2, **hints):
        """
        Allow relations if a model in the obj1 or obj2 apps is
        involved.
        """
        if (
                obj1._meta.app_label in self.route_app_labels or
                obj2._meta.app_label in self.route_app_labels
        ):
            return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        Make sure the auth and contenttypes apps only appear in the
        'log_db' database.
        """
        if app_label in self.route_app_labels:
            return self.db_name
        return None

같은 설명이니 생략하겠습니다.

 

이제 실제로 한 번 사용해볼까요?

 

아 잠시 실제로 따라할 때 migrate 부분은 다음과 같이 해주세요( 마이그레이트 진행 전에 makemigrations는 잊지 마세요~)

./manage.py migrate --database=primary_db
./manage.py migrate --database=log_db

 

 

shell로 확인해보기

 

이렇게 저장했을 때 primary_db의 상황

log_db는 ?

보시다시피 텅 비어 있다.

 

하지만 강제로 log_db에 데이터에 넣어줄 수 있습니다. 어떻게 하냐구요? 바로 보여드리겠습니다 ㅎㅎ

 

보시다시피 log_db에 저장되는 것을 보실 수 있습니다.

하지만 유의해야 할 점은 save()를 할 때 default도 건들게 되므로 굳이 db_manager()를 통해 변경할려는 시도를 하지는 말자!

default는 라우터로 설정한 것으로 된다는 것을 아시면 됩니다.

그러면 all() 하면 몇개 가져올까? 볼까요?

우려와 달리 default의 갯수만 가져옵니다.

 

그러면 log도 한 번 볼까요?

 

위의 경우는 위 테스트에서 db_manager를 통해 log_db에 account 데이터가 저장되어 있기 때문에 된 걸로 확인되었습니다. 

다시 확인해본 결과 다음과 같은 결과가 나왔습니다. 결론적으로 db를 쪼개서 관리하는 것은 아예 독립적인 테이블일 때만 가능한 것으로 확인되었습니다. ㅜㅜㅜㅜ

primary_db : log_log

 

log_db: log_log

우리 생각대로 라우터 방향으로 알맞게 들어갔다.

 

 

마지막으로 blog를 확인해보자!

log_db : blog_blog

 

primary_db: blog_blog

 

 

코드 첨부 : https://github.com/VIXXPARK/django-remind/tree/main/django_multiple_db

 

GitHub - VIXXPARK/django-remind

Contribute to VIXXPARK/django-remind development by creating an account on GitHub.

github.com

 

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함