SW개발/Django

[Django]GenericForeignKey, ContentType로 여러 모델과의 관계 맺기

RDB를 사용하여 모델을 만들다 보면 한 모델이 여러 모델과의 관계를 맺어야 하는 순간이 생기기 마련입니다. 일반적으로 생각한다면 ForeignKey를 이용하여 모델링을 하는 방법이 떠오를 것입니다.

 

위의 설명으로는 부족하니 하나의 모델을 통해 예시를 들어 설명해보겠습니다.

class Post(models.Model):
    title = models.CharField(max_length=100)
    # 여러 필드들 

class Comment(models.Model):
    content = models.CharField(max_length=100)
    # 여러 필드들
    
class Recommend(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    comment = models.ForeignKey(Comment, on_delete=models.CASCADE)

하나의 포스트와 댓글이 있고, 각각 추천수가 존재할 수 있다고 가정해보겠습니다.

이 때, ForeignKey를 활용하여 컬럼을 생성하여 처리할 수 있는데 이 경우에는 참조할 모델의 수만큼 컬럼이 생성된다는 단점이 존재합니다. (지금은 2개에 불과하지만 더 많아질 가능성이 존재함)

 

Django에는 이를 해결하기 위한 GenericForeignKey라는 것이 존재합니다.

 

GenericForeignKey, ContentType 사용하기

자세한 설명은 장고 공식 Document를 참조하면 될 것 같습니다.

https://docs.djangoproject.com/en/3.2/ref/contrib/contenttypes/#id1

 

The contenttypes framework | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

해당 포스팅에서는 직접 코드를 통해 사용하는 방법을 다뤄보겠습니다.

 

settings.py 설정

INSTALLED_APPS = [
    'django.contrib.contenttypes',
     # ...
]

GenericForeignKey를 사용하기 위해서는 django의 contenttypes가 필요합니다. 장고에 기본적으로 내장 되어있기 때문에 settings.py에 추가해줌으로써 간단하게 사용할 수 있습니다.

 

ContentType 란?

ContentType 모델은 기본적으로 장고에 내장되어 있는 모델입니다. 첫 마이그레이션시 django_content_type 라는 테이블 명으로 생성됩니다.

CREATE TABLE `django_content_type` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `app_label` varchar(100) NOT NULL,
  `model` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `django_content_type_app_label_model_76bd3d3b_uniq` (`app_label`,`model`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

 

id, app_label, model 이라는 필드가 내장되어 있습니다. 따라서 ContentTypeFK를 맺게되면 특정한 모델과 범용적으로 관계를 맺을 수 있다는 것을 의미합니다.

 

모델 변경

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation

class Post(models.Model):
    title = models.CharField(max_length=100)
    recommend = GenericRelation('Recommend', related_query_name='post')
    # 여러 필드들 

class Comment(models.Model):
    content = models.CharField(max_length=100)
    recommend = GenericRelation('Recommend', related_query_name='comment')
    # 여러 필드들
    
class Recommend(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete.CASCADE)
    object_id = models.PositiveIntegerField()
    content_objct = GenericForeignKey('content_type', 'object_id')
    # 여러 필드들

앞선 모델을 GenericForeignKey ContentType 모델을 활용해 재정의 했습니다.

  • content_type
    content_type 필드는 ContentType 모델과 FK로 연결되는 필드입니다.
  • object_id
    관련된 모델의 PK를 저장할 수 있는 필드입니다. (Post, Comment의 PK를 의미)
  • content_object
    GenericForeignKey로 content_type, object_id 필드를 전달합니다. 이 필드는 db에는 반영되지 않습니다.

 

모델 사용

recommend = Recommend.objects.filter(post__title='leffe tistory')

Post 모델에 related_query_name을 정의해주었으므로 Recommend 모델에서 post라는 이름으로 접근이 가능합니다.

(Comment 모델일 경우에는 comment__content와 같은 방법으로 접근할 수 있습니다.)

 

남용은 금물

지금까지 보았던 것처럼 GenericForeignKey, ContentType을 활용하면 ForeignKey 필드를 여러개 생성하지 않고도 손쉽게 다양한 관계를 맺을 수 있게됩니다. 당장의 필드 갯수를 줄여주기에 좋아보일 수 있습니다.

 

하지만, 외부 키 제약조건 없이 사용하는 NoSQL DB와 거의 동일한 형태를 띄기 때문에 사용에 주의를 기울여야 합니다. 이 필드를 남용하게 된다면 다음과 같은 문제가 발생할 가능성이 있습니다.

  • 모델 간의 인덱싱이 존재하지 않으면 쿼리 속도에 손해를 가져옵니다.
  • 다른 테이블에 존재하지 않는 레코드를 참조할 수 있는 데이터 충돌의 위험성이 존재합니다.

따라서, 되도록이면 이용을 피하고 꼭 필요한 부분에만 적용시키는 것이 좋습니다.

이 관계를 사용하는 것 대신에 모델 디자인을 새롭게 하거나, PostgreSQL의 필드를 통해서 해결할 수 있는지 먼저 확인해보아야 합니다.

불가피하게 이용해야만 하는 경우라면 서드 파티 앱을 통해서 데이터를 깔끔하게 유지하는 방법도 좋을 것 입니다.

 

Django 에서 범용 관계(Generic Relation)을 맺는 방법을 알아보았습니다. 범용이라는 의미 만큼 쉽게 관계를 맺을 수 있지만 이에 따라오는 문제들을 한번쯤은 생각해보고 사용하는 것이 좋을 것 같습니다.

 

728x90