크고 아름다운 Java 기반의 레거시 시스템. 하지만 매일 같이 반복되는 Java 코드를 찍어내기에 지쳤다면? 레거시 시스템에 Django를 들이밀어 한DB 두살림을 구축해보자. 아 그거 inspectdb 하나만 쓰면 되는 거 아닌가? 크고 작은 삽질들을 모아모아 공유합니다.
9. 2017년 가을, 저희 개발팀의 상황
• 주요 개발 언어인 Java, Javascript가 코드 베이스의 대부분이고
Python을 일부분 사용
• 테이블 수는 200여개
• 기존 Java(Spring)으로 만든 거대한 어드민 운영중
두 개의 어드민을 계속 유지보수 할 수 있을까?
9
15. DB 연결
• 두 개의 DB를 연결합니다.
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'myproj_django',
},
'myproj': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'myproj_development’,
},
}
DATABASE_ROUTERS = ['apps.router.DBRouter']
15
16. inspectdb 업데이트 스크립트
• 모델을 자주 업데이트하니 아예 스크립트를 만듭시다.
#!/bin/bash
set -e
TEMP_FILE=models.py.tmp
python manage.py inspectdb --database myproj | tee ${TEMP_FILE}
# 임시파일을 거쳐서 생성해야함.
mv -f ${TEMP_FILE} apps/core/models.py
16
17. 로그인 인증
• django의 custom backend를 활용
• DB가 연동되어 있으니 legacy의 ID/PW 정보로 로그인
• 사용자, 권한 정보 등을 그대로 가져다 쓸 수 있음!
# settings에서...
AUTHENTICATION_BACKENDS = [
'apps.core.backends.MyProjLoginBackend',
]
17
18. 로그인 인증
• 최초 로그인시 Django DB에도 staff user 생성
# backends.py
class MyProjLoginBackend(ModelBackend):
def authenticate(self, request=None, username=None, password=None):
myproj_user = MyProjUser.objects.filter(username=username).first()
# 암호 확인, 권한 확인 (생략)
user = User.objects.filter(username=username).first()
if not user:
user = User.objects.create_user(
username=myproj_user.username,
email=lendit_user.email,
is_staff=True,
)
user.save()
return user
18
19. 모델 어드민 등록
• 모델이 있으니, 모델 어드민만 등록하면 된대요!
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(LoanContract)
admin.site.register(User)
admin.site.register(FeatureFlag)
admin.site.register(AdminUser)
admin.site.register(EventBanner)
...
19
20. 모델 어드민 등록
• models.py에 있는 것을 모두 등록할 거니까요.
model_classes = [
x[1] for x in
inspect.getmembers(sys.modules["apps.core.models"],
inspect.isclass)
if models.Model in x[1].__bases__
]
for model_class in model_classes:
admin.site.register(model_class)
20
24. Django 어드민 커스터마이징
• Django의 손 쉬운 커스터마이징
• 하지만 200개 넘는 모델을 다 대응하려면...
@admin.register(LoanContract)
class LoanContractAdmin(admin.ModelAdmin):
search_fields = ('=cust_nm',)
list_display = ('id', 'cust_nm', 'status', 'created_at')
list_filter = ('status',)
form = LoanContractForm
24
25. Django 어드민 커스터마이징
• list_display 만이라도 전부 적용해봅시다.
def generate_default_model_admin(model):
return type(f'{model.__name__}Admin', (admin.ModelAdmin,), {
'list_display': [x.name for x in
model._meta.get_fields()],
})
# (생략......inspect로 model_class 불러오는 부분)
for model_class in model_classes:
if model_class not in admin.site._registry.keys():
admin.site.register(model_class,
generate_default_model_admin(model_class))
25
27. “필수 항목입니다.”
• inspectdb는 문자열 필드를 만들 때, 모두 필수 필드라고 가정
• 모두 필수 아님 필드로 만들어봅시다.
python manage.py inspectdb --database myproj > $TEMP_FILE
sed "
s/some_field = models.CharField(max_length=200)/some_field =
models.CharField(max_length=200, blank=True)/;
s/other_field = models.CharField(max_length=200)/other_field
= models.CharField(max_length=200, blank=True)/;
....
" $TEMP_FILE | tee $MODEL_FILE
27
28. inspectdb 확장하기
• sed 같은 외부 툴에 의존하지 않는 방법은 없을까?
• Github을 뒤적이던 도중....
🤔 흥미로운 파일명이군요.
28
29. inspectdb 확장하기
• Django문서 중 custom management commands
# apps/core/management/commands/inspectdb.py
from django.core.management.commands.inspectdb import (
Command as InspectDBCommand,
)
class Command(InspectDBCommand):
def get_field_type(self, connection, table_name, row):
field_type, field_params, field_notes =
super().get_field_type(connection, table_name, row)
if field_type == 'CharField':
field_params['blank'] = True
return field_type, field_params, field_notes
29
30. inspectdb 확장하기
• auto_now를 써서 생성, 수정 일자도 자동으로 넣어봅시다.
def get_field_type(self, connection, table_name, row):
field_type, field_params, field_notes =
super().get_field_type(connection, table_name, row)
if row.name == 'created_at':
field_params['auto_now_add'] = True
elif row.name == 'updated_at':
field_params['auto_now'] = True
if field_type == 'CharField':
field_params['blank'] = True
return field_type, field_params, field_notes
30
31. mysql, 그리고 BIT(1)
• 기존 시스템은 Mysql을 사용
• Boolean 값을 BIT(1)으로 표시
• inspectdb는 BIT(1)을 어떻게 생각할까?
old_bit_field = models.TextField() # This field type is a guess.
31
32. 커스텀 Boolean Field
• 사용자 필드 생성 매뉴얼을 정독하고, 만들어봅시다.
class LBooleanField(BooleanField):
def from_db_value(self, value, expression, connection,
context):
if value is None:
return False
return self.to_python(value)
def to_python(self, value):
# BIT(1)은 b'x00' b'x01'로 떨어짐. 변환필요.
if isinstance(value, bytes):
return bool(value[0])
return super(BooleanField, self).to_python(value)
32
33. 커스텀 Boolean Field
• inspectdb에서 불러다 씁시다.
• row.null_ok 를 사용하여 NullBooleanField도 확장하면 됩니다.
def get_field_type(self, connection, table_name, row):
field_type, field_params, field_notes =
super().get_field_type(connection, table_name, row)
if (row.type_code == FIELD_TYPE.TINY or row.type_code ==
FIELD_TYPE.BIT) and row.internal_size == 1:
field_type = 'LBooleanField'
field_notes = []
if row.name == 'created_at':
field_params['auto_now_add'] = True
...(생략)
33
34. 여기까지 쓴 Python 코드
• 로그인 처리: 10여줄
• 모델 별 admin 등록: 10여줄
• inspectdb 확장: 30여줄
• DB 라우터: 20여줄
34