레거시 시스템에
Django 들이밀기
정지용
발표자 소개
• P2P금융 렌딧에서 일합니다.
• 새로운 언어와 프레임워크를 좋아합니다.
• 읽기 편한 코드를 좋아합니다.
• 가장 좋은 코드는 아예 만들 필요가 없는 코드
2
Django로 향하는 길을 함께 열어준
Sam.Jo에게 감사를 전합니다.
3
Java 세상 이야기
“우리 시스템에 OO한 기능을 추가하고 싶어요.”
“일단 이.......만큼 코드를 쓰시고요.”
MyExampleRepository.java
MyExampleService.java
MyExampleServiceImpl.java
MyExampleController.java
MyExampleAdminController.java
MyExampleList.vue
MyExampleDetail.vue
4
자연스러운 수순
5
이어질 만한 수순
"이 이벤트 배너 종료일자는 어떻게 고치나요?"
"아.. 입력 폼 복붙하다가 필드를 하나 빼먹었네요."
6
막장 수순
빨리 고쳐야하는데...
급하니까 일단 터미널을 열고...
mysql에 접속해서...
UPDATE event_banner SET ends_at = '2018-08-19 13:35:00';
7
계기
• 높은 생산성을 갖는 어드민을 새로 만들고 싶다...
...
8
2017년 가을, 저희 개발팀의 상황
• 주요 개발 언어인 Java, Javascript가 코드 베이스의 대부분이고
Python을 일부분 사용
• 테이블 수는 200여개
• 기존 Java(Spring)으로 만든 거대한 어드민 운영중
 두 개의 어드민을 계속 유지보수 할 수 있을까?
9
inspectdb
• Integrating Django with a legacy database
$ python manage.py inspectdb > models.py
10
inspectdb
• DB 스키마를 읽어서 models.py를 생성해주는 기능
• legacy 시스템에 Django를 쉽게 도입할 수 있도록 도와줌
• 본격적으로 한 DB 두 살림을 운영해보자!
11
목표
1. 최소한의 코딩으로 최대한의 효과(어드민 기능)를!
2. 최대한 기존 DB를 활용하지만, legacy 프로젝트에는 전혀 영향
을 주지 않도록
12
발표 목차
• DB 연결
• 로그인 인증
• 모델 어드민 등록 및 커스터마이징
• inspectdb 확장하기
• BIT(1), BOOLEAN 문제
13
주의: 전방에 코드가 있습니다.
14
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
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
로그인 인증
• django의 custom backend를 활용
• DB가 연동되어 있으니 legacy의 ID/PW 정보로 로그인
• 사용자, 권한 정보 등을 그대로 가져다 쓸 수 있음!
# settings에서...
AUTHENTICATION_BACKENDS = [
'apps.core.backends.MyProjLoginBackend',
]
17
로그인 인증
• 최초 로그인시 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
모델 어드민 등록
• 모델이 있으니, 모델 어드민만 등록하면 된대요!
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
모델 어드민 등록
• 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
완성! 참 쉽죠?
21
짜잔!!!!!!!!!!!!!!!!!!!!!!!!
22
짜잔!!!!!!!!!!!!!!!!!!!!!!!!
23
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
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
Django 어드민 커스터마이징
26
“필수 항목입니다.”
• 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
inspectdb 확장하기
• sed 같은 외부 툴에 의존하지 않는 방법은 없을까?
• Github을 뒤적이던 도중....
🤔 흥미로운 파일명이군요.
28
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
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
mysql, 그리고 BIT(1)
• 기존 시스템은 Mysql을 사용
• Boolean 값을 BIT(1)으로 표시
• inspectdb는 BIT(1)을 어떻게 생각할까?
old_bit_field = models.TextField() # This field type is a guess.
31
커스텀 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
커스텀 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
여기까지 쓴 Python 코드
• 로그인 처리: 10여줄
• 모델 별 admin 등록: 10여줄
• inspectdb 확장: 30여줄
• DB 라우터: 20여줄
34
돌아보면...
• Legacy와의 공존은 성공
• Django기반 시스템을 발전시킬 수 있는 기반을 마련함
• 가장 힘들었던 부분: 배포 환경 설정
35
돌아보면...
• 날로 먹으면 기분이 좋다.
• 거의 모든 부분이 확장 가능한 Django.
• 지금 복사 붙여넣기를 하고 있다면, 분명 더 나은 방법이 있다.
36

레거시 시스템에 Django 들이밀기

  • 1.
  • 2.
    발표자 소개 • P2P금융렌딧에서 일합니다. • 새로운 언어와 프레임워크를 좋아합니다. • 읽기 편한 코드를 좋아합니다. • 가장 좋은 코드는 아예 만들 필요가 없는 코드 2
  • 3.
    Django로 향하는 길을함께 열어준 Sam.Jo에게 감사를 전합니다. 3
  • 4.
    Java 세상 이야기 “우리시스템에 OO한 기능을 추가하고 싶어요.” “일단 이.......만큼 코드를 쓰시고요.” MyExampleRepository.java MyExampleService.java MyExampleServiceImpl.java MyExampleController.java MyExampleAdminController.java MyExampleList.vue MyExampleDetail.vue 4
  • 5.
  • 6.
    이어질 만한 수순 "이이벤트 배너 종료일자는 어떻게 고치나요?" "아.. 입력 폼 복붙하다가 필드를 하나 빼먹었네요." 6
  • 7.
    막장 수순 빨리 고쳐야하는데... 급하니까일단 터미널을 열고... mysql에 접속해서... UPDATE event_banner SET ends_at = '2018-08-19 13:35:00'; 7
  • 8.
    계기 • 높은 생산성을갖는 어드민을 새로 만들고 싶다... ... 8
  • 9.
    2017년 가을, 저희개발팀의 상황 • 주요 개발 언어인 Java, Javascript가 코드 베이스의 대부분이고 Python을 일부분 사용 • 테이블 수는 200여개 • 기존 Java(Spring)으로 만든 거대한 어드민 운영중  두 개의 어드민을 계속 유지보수 할 수 있을까? 9
  • 10.
    inspectdb • Integrating Djangowith a legacy database $ python manage.py inspectdb > models.py 10
  • 11.
    inspectdb • DB 스키마를읽어서 models.py를 생성해주는 기능 • legacy 시스템에 Django를 쉽게 도입할 수 있도록 도와줌 • 본격적으로 한 DB 두 살림을 운영해보자! 11
  • 12.
    목표 1. 최소한의 코딩으로최대한의 효과(어드민 기능)를! 2. 최대한 기존 DB를 활용하지만, legacy 프로젝트에는 전혀 영향 을 주지 않도록 12
  • 13.
    발표 목차 • DB연결 • 로그인 인증 • 모델 어드민 등록 및 커스터마이징 • inspectdb 확장하기 • BIT(1), BOOLEAN 문제 13
  • 14.
  • 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
  • 21.
  • 22.
  • 23.
  • 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
  • 26.
  • 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
  • 35.
    돌아보면... • Legacy와의 공존은성공 • Django기반 시스템을 발전시킬 수 있는 기반을 마련함 • 가장 힘들었던 부분: 배포 환경 설정 35
  • 36.
    돌아보면... • 날로 먹으면기분이 좋다. • 거의 모든 부분이 확장 가능한 Django. • 지금 복사 붙여넣기를 하고 있다면, 분명 더 나은 방법이 있다. 36

Editor's Notes

  • #16 - 기존 DB에 전혀 영향이 없도록 django는 DB를 분리 - router에서는 model이 속한 app을 보고 라우팅
  • #21 Inspect라는 단어가 좋아지려고 하네요. ㅎㅎ Getmembers는 (name, value) pair list를 반환. apps.core.models 모듈에서 클래스인 것 중, models.Model을 상속받은 클래스 전체를 등록
  • #24 참고로 Django 2.0에서는 User object(42) 같이 id 정보가 더 나옵니다.
  • #26 type()에 대한 설명
  • #29 t -> inspectdb 쳤는데!!
  • #30 경로와 파일명을 정확히 맞추어야합니다.
  • #31 경로와 파일명을 정확히 맞추어야합니다.
  • #32 - 심지어는 b’\x00’, b’\x01’ 값이 브라우저에 따라 다르게 표시되고 혼돈이 밀려옴
  • #34 - 참고) `Django-mysql` 패키지의 Bit1BooleanField 를 사용해도 좋을 것 같습니다. - Django 2.1에서는 NullBooleanField말고 BooleanField 하나로 모두 처리해야겠네요!
  • #36 - pip로 모듈이 안 깔릴 때는 easy_install을 해보세요.
  • #37 더 낫다는 것은 상황에 따라 다를 수 있지만...