django 모델 상속 ( Model Inheritance )
django의 ORM ( Object-Relational Mapping ) 기능은 데이터베이스 구조를 머리속 그림대로 직관적으로 구현하는 데에 매우 편리하다. 만약 당신이 SQL에 숙련되지 못했다면 엄두를 내지 못할 DB 구조를 django에서는 파이썬 코드를 통해 비교적 쉽게 해결할 수 있는 것이다. 이번에는 수 많은 django의 ORM 기능 중에서 상속 기능만 간단히, 그리고 부분적으로 설명해 보고자 한다.
django는 세 가지의 모델 상속 타입을 제공하는데 abstract base classes, multi-table inheritance, proxy model 이라는 이름으로 django doc에서 설명한다. 객체 지향적인 프로그래밍을 요즘은 모두들 아시기 때문에 상속에 대해서 설명하진 않을 것이다. 기본적으로 상속이라는 단어의 사전적 의미에서 알 수 있듯이 뭔가 타겟을 정해서 그 녀석을 통째로 받아서 이용해 먹는거라고 생각하면 된다.
1. Abstract base classes
가장 특별하지 않은 상속 방법이다. 말보다는 코드를 먼저 보겠다. 운송수단에 관한 모델을 구성하려고 한다.
class Vehicle( models.Model ):
name = models.CharField( max_length = 100 )
birthday = models.DateField( )
created = models.DateTimeField( auto_now_add = True )
class Meta:
abstract = True
class Car( Vehicle ):
type = models.CharField( max_length = 100 )
class Automobile( Vehicle ):
pass
class Bus( Vehicle ):
num_passengers = models.IntegerField( )
가장 위에 운송수단 전체를 포함하는 필드들을 가진 Vehicle 클래스 (앞으로 mother class의 의미로 '엄마'라고 부르겠다) 가 선언됐고 이 것을 상속받는 세 개의 모델이 Car, Automobile, Bus (child class의 의미로 '새끼'라고 부르겠다) 이다.
Vehicle은 이름, 제조일을 가진 모델이고, Car는 필드가 타입 하나 뿐이지만 Vehicle한테 물려받은게 있으니까 이름, 제조일, 타입 이렇게 세 개의 필드를 가지고 있을 것이다. 그렇다면 Automobile은? Vehicle의 필드만 고대로 갖고 있겠지. 그렇다면 나는 네 개의 모델 클래스가 있으니까 네 개의 DB 테이블이 생성되는 것일까. 그렇지 않다
메타 옵션에서 abstract = True 를 설정하면 엄마 모델은 실제로 또는 물리적으로 존재하지 않는 가상의 클래스가 된다. 그리고 새끼 모델들은 엄마의 필드와 속성, 함수들을 다 물려받아 실체가 있는 DB 테이블이 된다. 그러니까 나는 세 개의 DB 테이블을 갖게 되는 것이다. 이를 확인하고 싶다면 makemigrations를 통해 생성된 migrations 폴더 안의 파이썬 코드를 열어보면 migrate를 하기 전에 확인할 수 있을 것이다. 물논 직접 migration을 해보면 당연히 확인할 수 있겠지만.
abstract base를 사용한다는 것은 새끼 모델들이 엄마 없이 각각 독립적인 DB 테이블로서 존재하며, 새끼와 엄마의 상속관계는 실제로 없는 것이다. 공통된 필드가 많이 있는 모델 클래스들이 있을 때 코드를 효율적으로 사용하기에 편리한 기능이라고 생각한다.
2. Multi-table inheritance
위 경우와는 다르게 실제로 상속이 일어나는 타입이다? 코드를 보자.
class Vehicle( models.Model ):
name = models.CharField( max_length = 100 )
birthday = models.DateField( )
created = models.DateTimeField( auto_now_add = True )
class Car( Vehicle ):
type = models.CharField( max_length = 100 )
class Automobile( Vehicle ):
pass
class Bus( Vehicle ):
num_passengers = models.IntegerField( )
똑같은 예제 코드를 썼다, 메타 클래스에 abstract = True 가 사라진 것 빼고. 파이썬 클래스 상속하듯이 그냥 쓰면 되는 것이다. 하지만 이로인해 생성될 DB 구조는 많이 달라진다.
이 경우 나는 네 개의 모델 클래스를 통해 정말로, 정직하게 네 개의 DB 테이블을 갖게 된다. 그렇다면 새끼는 엄마를 상속받으니까... 엄마 테이블은 새끼들의 내용을 다 가지고 있는 포괄적인 목록이 되겠네? 그렇다.
좀 더 자세하게 이야기해보자. 이것은 django shell script를 통해 이해하는 것이 가장 효과적이라고 생각한다.
>>> v = Vehicle( )
>>> v.name = 'Accent' # Vehicle의 필드이므로 아무 문제 없다
>>> v.num_passenger = 50 # 당연히 에러
>>> v.save( ) # 실제로는 나머지 필드인 birthday도 채우고 저장해야 할 것이다 (왜?)
>>> b = Bus( )
>>> b.num_passenger = 50 # OK
>>> b.name = 'School Bus' # 엄마의 필드까지 모두 가지고 있기 때문에 OK
>>> b.save( )
자 여기까지 하고 실제 DB 테이블에 어떻게 저장돼있는지 보자. 우선 bus 테이블은 당연히 한 줄이 있을 것이다. 그리고 예상 가능하게도 vehicle 테이블에는 두 줄이 있다. 당연하게도 vehicle 테이블에는 우리가 잘 선언한 필드 name, birthday, created timestamp를 갖고 있을 것이다. 그리고 bus 테이블에서는 역시 선언한 필드 num_passengers와 연결된 vehicle 녀석의 pk가 저장돼있음을 알 수 있다. 그럼 나는 이 연결된 것을 어떻게 찾아갈 수 있을까.
>>> v = Vehicle.objects.get( name = 'School Bus' )
>>> v.bus # v는 Bus 클래스를 갖고 있기 때문에 연결된다
>>> v.bus.num_passenger # 50임을 확인할 수 있다
>>> v.car # 에러가 날 것이다는 것은 모두가 알 것이고, Car.DoesNotExist 에러가 난다
소문자로 연결할 수 있는데, 여기서 문제는 v가 Bus 클래스를 갖고 있다는 사실을 이미 안 상태에서 내가 직접 v.bus라고 타이핑을 통해 연결한다는 것이다. 가상으로 Vehicle 테이블에 정말 많은 줄을 저장해놨다고 생각해보자. 내가 원하는 가장 이상적인 시나리오는 일일이 누가 어느 클래스인지 모르는 상태에서도 Vehicle 클래스에서 연결돼있는 subclass로 바로 연결되는 것이다. 그렇지 못한다면 나는 거꾸로 새끼 모델에서부터 접근해야하는데 그것은 자연스럽지 못한 순서이다. 엄마가 모든 것을 다 갖고 있는데 왜 있을지 없을지도 모르는 새끼에게 먼저 가야 하는 것인가.
이 문제의 해결은 django 에서 바로 지원되지는 않는 기능이지만 외부 모듈을 통해 해결할 수 있다. django-model-utils의 InheritanceManager 를 이용하면 다음과 같은 코드로 내가 바라는 바를 해결해 준다.
from model_utils.managers import InheritanceManager
class Vehicle( models.Model ):
name = models.CharField( max_length = 100 )
birthday = models.DateField( )
created = models.DateTimeField( auto_now_add = True )
objects = InheritanceManager( ) # objects 매니저를 새걸로 바꿔준다
class Car( Vehicle ):
type = models.CharField( max_length = 100 )
class Automobile( Vehicle ):
pass
class Bus( Vehicle ):
num_passengers = models.IntegerField( )
>>> Vehicle.objects.select_subclasses( ) # 내가 원했던 결과를 확인할 수 있다.
>>> Vehicle.objects.get_subclass( name = 'Accent' ) # 하나만 갖고올 때는 다른 명령어.
실제로 DB의 구성은 굉장히 중요하기 때문에 개발을 시작하기 전에 가장 많은 시간과 공을 들이는 문제이다. 상속관계를 잘 이용하면 또는 알고만 있더라고 DB를 구성하는 데 많은 도움이 될 수 있을 거라고 생각한다.
3. Proxy models
proxy의 사전적 의미는 '대리인'이다. Proxy model은 이미 존재하는 모델을 그대로 받아들이면서 새로운 기능을 추가할 수 있다. proxy model의 대표적인 사용 예제는 User model에 나만의 method를 추가하는 것이다. 나는 django가 기본적으로 제공하는 기능을 최대한 손 대지 않고 사용하고 싶기 때문에, 사용자 모델에 추가적인 필드를 추가하거나 하고 싶을 때는 built-in User model과 연결해서 새로운 model을 다음과 같이 만들어서 사용하곤 한다.
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneFIeld(User)
# ... my custom fields ...
age = models.IntegerField()
def my_custom_method(self):
# ... do something ...
# usage
user = User.objects.get(pk=1)
user_age = user.userprofile.age
user_method = user.userprofile.my_custom_method()
usage에서 볼 수 있듯이, 1-1으로 relation이 잡혀있는 UserProfile 모델에 접근해서 그 곳에 정의된 field와 method를 사용할 수 있다. 만약 내가 추가적인 필드는 필요하지 않고 custom method만 필요하다면, 이 1-1 모델은 꽤나 잉여스러운 모델이 될 것이다. 이 경우 proxy model을 이용하면 built-in User model을 그대로 가져갈 수 있다. 다음 예제를 보자.
from django.contrib.auth.models import User
class UserMethod(User):
class Meta:
proxy = True
def my_custom_method(self):
# ... do something ...
built-in User model을 상속하는 UserMethod model을 만들었고, 거기에 아무런 필드를 추가하지 않고 proxy = True라는 Meta 옵션만 주었다. 이제 이 UserMethod model은 마치 대리인처럼 기존 User model의 내용을 모두 가지고 있는 instance를 뱉어낸다. 그리고 이 대리인은 proxy model에서 정의한 custom method를 사용할 수 있다.
proxyuser = UserMethod.objects.get(pk=1)
print proxyuser.username
# built-in User model의 필드 username에 접근할 수 있다.
method_result = proxyuser.my_custom_method() # custom method 실행
그 외의 여러 기능들은 django 공식 문서를 통해서 알아볼 수 있다.
참고자료
django documentation을 찬찬히 읽어보면 도움이 된다
https://docs.djangoproject.com/en/1.9/topics/db/models/#model-inheritance
django-model-utils는 언급한 InheritanceManager 외에 꽤 많은 기능이 있으니 알아둘 만 하다
https://django-model-utils.readthedocs.org/en/latest/index.html
새끼에서 엄마로 연결하기
https://stackoverflow.com/questions/4064808/django-model-inheritance-create-sub-instance-of-existing-instance-downcast/4065189#4065189