장고를 쓰는 이유중 하나는 빠른 생산성이다. 보통 웹 프로젝트에서 실제 클라이언트에게 서비스되는 공간을 설계하는 것 만큼 어려운 것이 백오피스 즉 관리자 페이지 설계이다. 백 오피스는 이용자들이 접근할 수 없지만 내부 관리자들로 하여금 이용자들의 통계나 데이터들을 확인할 수 있어야 하며 프로그램의 중요한 정보들을 이곳에서 관리할 수 있다.
백 오피스 설계가 어려운 이유는 벤치마킹 대상을 찾기 어려움에 있다. 실제 서비스들에 연결되는 기능 설계보다 후 순위의 개발 작업이기에 더욱 개발 리소스를 얻기 어렵다.
장고는 무거운 프레임워크답게 Admin을 쉽게 제작할 수 있도록 제작 툴을 제공한다.
내가 설계하는 장고 백엔드의 모델은 다음과 같은 구조를 가진다.
모델 이름은 보안 상 임의로 변경해서 적었다. 현재 외래키로 one to N인 경우에만 탭에서 접근을 허용해줄 것이다.
즉 Master, Child, ChildInfo이다.
장고 Admin클래스는 Inline기능을 제공한다. Master에 Inline으로 Child를 전달하여 Master에서 Child 정보를 나타낸다. 실제로 담당하는 앱 기능의 많은 데이터는 ChildHistory쪽으로 접근해야 한다.
Django 모델 설계 구조가 깊은 이유
데이터 일관성 유지
: 모델은 데이터베이스에 저장되는 구조를 정의한다. 적절한 모델 설계를 통해 데이터의 일관성을 유지할 수 있으며, 일관성 있는 데이터는 애플리케이션의 신뢰성과 안정성을 높여준다.성능 최적화
: 잘 설계된 모델은 데이터베이스에 대한 효율적인 쿼리를 가능하게 한다. 쿼리를 최적화하고 인덱싱을 올바르게 구성함으로써 성능을 향상시킬 수 있다.유지 보수 용이성
: 모델은 애플리케이션의 데이터 구조를 정의한다. 모델을 잘 설계하면 코드의 가독성이 향상되고 유지 보수가 쉬워진다. 또한, 새로운 기능을 추가하거나 변경할 때 기존 모델을 수정하는 작업이 간단해진다.확장성
: 잘 설계된 모델은 애플리케이션이 성장함에 따라 유연하게 대응할 수 있도록 한다. 새로운 기능을 추가하거나 데이터 구조를 변경할 때 모델을 확장하거나 수정함으로써 애플리케이션을 쉽게 확장할 수 있다.
Master의 inline으로 Master 레코드에 들어가면, 많은 Child들을 띄운다. 이 Child들은 각자의 ChildHistory를 가진다. Child는 inline으로 ChildHistory를 가진다. 각각의 inline모델에 다음 자식으로 연결되는 링크를 넘겨준다.
class ChildInline(PermissionControlMixin, admin.TabularInline):
model = Child
extra = 1
fields = ('child_reg_no', 'child_info','link')
readonly_fields = ('child_info','link')
inlines = [ChildInfo,]
def link(self, obj):
if obj.pk:
url = reverse('admin:master_child_change', args=[obj.pk])
return format_html('<a href="{}">{}</a>', url, obj.pk)
else:
return None
class MasterInfoInline(PermissionControlMixin, admin.TabularInline):
model = MasterInfo
extra = 1
fields = ('master', 'name')
@admin.register(Master)
class Master(PermissionControlMixin, SoftDeletableAdmin):
list_display = ('account', 'omitted_name')
# search_fields = ['name']
# search_help_text = '이름으로 검색'
inlines = [ChildInline, MasterInfoInline]
def account(self, obj):
return f'{mask_account(obj.user.account)}'
def omitted_name(self, obj):
nickname = mask_korean_name(obj.doctor_info.name)
return f"{nickname}"
Master는 실제로 id만 가진 모델이다. Master의 정보는 MasterInfo에 담긴다. MasterInfoInline와 ChildInline를 MasterAdmin의 inline으로 전달한다.
Master admin 페이지에서 원하는 Master 레코드를 선택하면 다음과 같은 inline정보들이 나타난다.
Child로 이동하면 ChildAdmin은 ChildHistoryInline을 가지게 되며 ChildHistoryInline은 해당 pk의 ChildHistory로의 link를 부여한다.
ChildHistory는 다른 모든 모델들과 one-to-one관계를 가진다. 더 깊을지언정 차원이 늘어나지는 않는다는 뜻이다. 그러므로 ChildHistory의 개별 레코드 정보를 조회할 때 나머지 모두의 정보를 주면 완성이다.
ChildHistory는 7개의 모델의 정보들을 주어야 한다. 그러므로 코드가 길어질 것이라 admin_method.py에 ChildHistory작성을 위한 코드를 짜주었다.
from core.constants import CLASS_NAME_DISEASES
class HistoryChecker:
_SHORT_DESCRIPTIONS = {
'check_1': "memo",
'check_2': "ffile1",
'check_3': "ffile2",
'check_4': "map1",
'check_5': "map2",
'check_6': "f1_info",
'check_7': "f2_info"
}
_HISTORY_CHILD = [
'check_1',
'check_2',
'check_3',
'check_4',
'check_5',
'check_6',
'check_7'
]
@classmethod
def get_short_descriptions(cls):
return cls._SHORT_DESCRIPTIONS
@classmethod
def get_history_child(cls):
return cls._HISTORY_CHILD
@classmethod
def check_data1(cls, data):
if data:
return "Exists"
return "Does not exist"
@classmethod
def check_data2(cls, data):
if not data:
return "Does not exist"
if any(getattr(data, val) is None for val in CLASS_NAME_DISEASES):
return "Does not exist"
if sum(int(getattr(data, val)) for val in CLASS_NAME_DISEASES) == 1:
return "Exists"
return "Does not exist"
@classmethod
def set_short_descriptions(cls, fields):
for field in fields:
setattr(field, 'short_description', cls.SHORT_DESCRIPTIONS[field])
class PermissionControlMixin:
def has_change_permission(self, request, obj=None):
return False
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
## ======= admin.py ========= ##
@admin.register(MedicalHistory)
class MedicalHistory(PermissionControlMixin, SoftDeletableAdmin):
list_display = HistoryChecker.get_history_child()
fields = HistoryChecker.get_history_child()
readonly_fields = HistoryChecker.get_history_child()
def check_1(self, obj):
return HistoryChecker.check_data1(obj.~)
def check_2(self, obj):
return HistoryChecker.check_data1(obj.~)
def check_3(self, obj):
return HistoryChecker.check_data1(obj.~)
def check_4(self, obj):
return HistoryChecker.check_data1(obj.~)
def check_5(self, obj):
return HistoryChecker.check_data2(obj.~)
def check_6(self, obj):
return HistoryChecker.check_data2(obj.~)
def check_7(self, obj):
if not obj.memo_history:
return "Does not exist(no needed)"
if obj.memo_history.field1 or obj.memo_history.field2:
return "Exists"
return "Does not exist(no needed)"
for field, short_description in HistoryChecker.get_short_descriptions().items():
setattr(ChildHistory, field, short_description)
클래스로 History에 관련됨을 알릴 수 있고 그 안에 사용될 메서드들을 상수처럼 주고 불변을 위해 get으로만 외부에서 접근할 수 있도록 한다.
이렇게 되면 ChildHistory 레코드 접근시 데이터 존재성을 알 수 있다.
내부 관리자여도 데이터들을 관리자페이지에서 CRUD할 수 없다. 이용자는 당연히, 내부 개발자에게도 접근성을 최대한 제한 해야한다. 그렇기에 이용자의 데이터가 문제가 있을 때 존재성정도 체크할 수 있게 만들었다.(보안이 중요한 프로젝트이다보니.)