첫 글에서 말씀드렸듯 프레임워크를 쓰는 방법 자체에 대한 설명은 많으니 그보다는 왜 DRF가 그렇게 생겼는지, 어떻게 더 잘 쓸지 등 좀 더 깊이있는 이야기를 풀어보고자 합니다.
전 회차에서는 python 가상환경을 만들고, dependency를 설치하고, django 앱을 생성하고, DRF를 얹고, migration을 돌린 후 DRF의 기본 auth 앱을 통해 로그인하는데까지 진행해봤습니다.
요번 회차에서는 유저 정보를 담을 수 있는 프로필 모델을 생성하면서 모델에 대해 알아보겠습니다.
python 모듈/패키지는 어떻게 작동하는지, 이를 통해서 첫번째 글에서 모듈화 한다고 한 부분들을 어떻게 바꿔야 할지 알아봅시다.
쉽게 생각하면 모든 python 파일은 모듈이고, 여러 파이썬 파일이 있는 폴더가 패키지입니다. 하지만 모듈이나 패키지나 import하면 똑같이 모듈로 취급됩니다. 파일시스템 레벨에서만 의미 있는 구분인거죠.
혹시 눈치채셨나요? 그 말인 즉, 어떤 모듈을 불러올 자리에 패키지를 넣을 수 있다는 이야기입니다. 모듈을 더 편하게 작성하기 위해 폴더와 하위 파일로 패키지화 할 수 있고, 이는 다른 코드에 전혀 영향을 주지 않습니다.
기본적으로 패키지는 폴더 구조를 따라갑니다. 원래 python에는 package를 초기화(init) 하는 __init__.py 라는 파일이 폴더에 있어야 하는데, python 3.3 부터는 implicit namespace package 라는 기능이 추가되어 init파일 없이 폴더구조 그대로 패키지처럼 사용할 수 있습니다. 아래와 같이 말이죠.
app.py
parent/
__init__.py (생략 가능)
child/
__init__.py (생략 가능)
something.py
# something.py
class Something:
pass
# app.py
from parent.child.something import Something
모델은 각 앱 하위의 models.py 파일 혹은 models 패키지에서 정의할 수 있습니다. 처음에 django startapp 을 통해 앱을 생성하게 되면 단일 models.py 파일로 되어 있어서 그냥 그렇게 관성적으로 작업을 하는 경우도 있지만, 프로젝트가 커지면 커질수록 코드 관리도, 리팩토링도 어려워지기 때문에 미리 패키지화 해 두는 것이 좋은 선택이라 생각합니다.
models.py를 삭제하고 models라는 python package를 추가합니다. (python package라고 해봐야, 해당 이름의 폴더에 __init__.py 파일이 하나 추가된 것 뿐입니다.) 그리고 요번에 사용할 프로필 모델을 담을 profile.py 파일도 생성합니다.
django.db.models.Model을 상속받아 모델을 구현하면 되고, 필요에 따라 모델 클래스 하위에 Meta 클래스라는 클래스에 abstract = True 라고 선언하여 abstract model을 만들고 이를 다시 타 클래스에서 상속받도록 구현할 수 있습니다.
단순하게 특정 컬럼이 다른 테이블의 어던 컬럼의 키값을 가져야 한다는 foreign key constraint를 이용하기 위해 models.ForeignKey()로 필드를 정의할 수도 있지만, ORM의 강력한 기능들을 활용하기 위해서는 구체적인 relationship에 따라 OneToOneField, OneToManyField, ManyToManyField를 이용하면 모델간의 관계를 더 명확하게 정의할 수 있고 그에 따른 편의기능을 사용할 수 있습니다.
모든 유저는 반드시 하나의 프로필을 갖도록 하기 위해 OneToOneField를 이용해보겠습니다. 첫 인자에는 모델의 string값 또는 클래스 둘 다 넘겨줄 수 있고, import를 피하기 위해 string을 넘겨주는 것이 편하지만 그렇게 했을 경우 리팩토링시 해당 string값에 대해서도 꼭 체크해줘야 한다는 점 잊으면 안됩니다.
여기서는 django contrib에 포함된 auth module의 user를 이용합니다. 본인의 목적에 따라 settings에 AUTH_USER_MODEL으로 다른 AbstractBaseUser 클래스를 상속받는 모델을 이용해도 좋습니다.
위와 같은 형태로 User와 1:1 관계를 갖는 Profile 모델을 생성했습니다. 일단 프로필 이미지 주소를 위한 필드 하나만 추가해봤습니다.
OneToOneField로 매핑된 경우, User instance와 Profile instance는 user.profile과 profile.user와 같이 양방향으로 불러올 수 있습니다.
이때 해당 인스턴스 정보를 가져오기 위해 db에 쿼리를 날리는 시점에 대해 유의할 필요가 있습니다.
django ORM은 lazy loading과 caching을 사용합니다. 실제로 데이터가 필요한 순간까지 실제 데이터 가져오기를 유보합니다. 그리고 가져온 데이터를 cache해 동일한 데이터에 대해 여러번 쿼리를 날리지 않습니다.
users = User.objects.all() # 쿼리 실행 x
...
user = users[0] # 비로소 쿼리 실행
users[0].email # users에 캐시된 값 이용
보편적인 명칭은 N+1 problem입니다. (순서상 1이 N보다 먼저 일어나기 때문에 저는 1+N problem이 더 직관적인 명칭이라고 생각합니다.) ORM을 통해 여러 instance를 가져와서 (1) 다시 각 instance의 related instance에 접근할 때, 이를 N번의 쿼리로 가져오게 되는 문제를 말합니다.
users = User.objects.all() # 1 query
for user in users:
user.profile # 1 query, executed N times
이를 피하기 위해 처음에 데이터를 가져올때 연관된 데이터를 미리 가져오도록 할 수 있습니다.
users = User.objects.prefetch_related('profile').all() # 1 query
for user in users:
user.profile # 0 query (cached)
공식문서 잘 쓸 일 없지만 알고 있으면 유용합니다. 쿼리를 만들게 해주는 클래스로, 모든 모델 클래스는 모델 매니저 인스턴스를 갖고 있습니다. 위 예시 코드에서 보이는 User.objects.all()의 objects가 매니저입니다. 기본 behavior를 변경하거나 새로운 기능을 추가하는 등 쿼리에 대한 기능을 제어할 수 있습니다.
class UserManager(Manager):
def get_queryset(self):
return super().get_queryset().filter(deleted_at__isnull=True)
class User:
objects = UserManager()
User.objects.all() # isnull filter 적용됨
# CREATE
user = User.objects.create(**kwargs)
user = User(**kwargs).save()
# READ
users = User.objects.filter(**kwargs)
# UPDATE
user.update(**kwargs)
user.field_to_update = updated_value
user.save()
## DELETE
user.delete()
어떤 모델에 변경이 일어났을 때 그 변경이 전파되어야 하는 경우가 빈번합니다. save()를 오버라이드하는 방법도 있겠지만, django signal을 이용하는 방법도 있습니다. 내부적으로 작동하는 event pub/sub 기능으로, synchronous하게 작동 한다는 특징이 있습니다.
위에서 Profile 모델을 추가하고 Users와 1:1 관계로 정의했지만, 그것만으로 Users가 생성될 때 Profile이 자동으로 생성되지는 않습니다. 아래와 같이 User의 post_save signal을 받아 Profile instance를 생성하는 코드를 추가하면 User가 생성될 때 Profile까지 생성됩니다.
# app/signals/user.py
# third party imports
from django.db.models.signals import post_save
from django.dispatch import receiver
# application imports
from app.models.profile import Profile
@receiver(post_save, sender='auth.User')
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
signals라는 모듈을 만들고 users.py에 대한 코드를 넣었지만 models와 같이 django에서 기본적으로 참조하는 모듈이 아니라 django에서 이를 불러올 수 있게 명시해주어야 합니다.
# app/apps.py
class AppConfig(AppConfig):
name = 'app'
def ready(self):
from app import signals
django의 model과 관련된 내용들을 간단하게 살펴봤습니다. 다음 글에서는 이렇게 생성한 모델에 대해 테스트를 해보고, 간단한 API와 테스트를 작성해보겠습니다.