본문으로 건너뛰기
김신건의 로그

[Django] ContentTypes, Generic Relations

· 수정 · 📖 약 1분 · 435자/단어 #django #contenttypes #generic-foreign-key #polymorphic
django contenttypes, GenericForeignKey, polymorphic django, django comment polymorphic

정의

django.contrib.contenttypes모든 모델을 동적으로 참조하는 시스템. 댓글이 Post, Photo, Video 등 여러 모델을 가리킬 수 있게 함. GenericForeignKey + ContentType을 활용.

활성화

기본 INSTALLED_APPS에 포함됨:

INSTALLED_APPS = [
    "django.contrib.contenttypes",
    ...
]

각 모델마다 ContentType row 자동 생성 (app_label + model).

ContentType 활용

from django.contrib.contenttypes.models import ContentType

ct = ContentType.objects.get_for_model(Post)
ct.app_label    # "blog"
ct.model        # "post"
ct.model_class()    # Post 클래스

GenericForeignKey

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

class Comment(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey("content_type", "object_id")

    body = models.TextField()
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
post = Post.objects.first()
Comment.objects.create(content_object=post, body="좋아요!", author=user)

photo = Photo.objects.first()
Comment.objects.create(content_object=photo, body="멋져요!", author=user)

# 조회
for comment in Comment.objects.all():
    obj = comment.content_object    # Post 또는 Photo
    print(type(obj), obj)

GenericRelation (역방향)

from django.contrib.contenttypes.fields import GenericRelation

class Post(models.Model):
    title = models.CharField(max_length=200)
    comments = GenericRelation(Comment)

class Photo(models.Model):
    url = models.URLField()
    comments = GenericRelation(Comment)

post.comments.all()    # Post의 모든 댓글
photo.comments.create(body="...", author=user)

GenericRelation은 모델에 정의된 reverse accessor.

N+1 회피

# Bad: N+1
for c in Comment.objects.all():
    print(c.content_object.title)    # 매번 SQL

# prefetch_related로 자동 처리
for c in Comment.objects.prefetch_related("content_object"):
    print(c.content_object)

또는 prefetch_related_objects:

from django.db.models import Prefetch

comments = Comment.objects.all()
prefetch_related_objects(comments, "content_object")

ContentTypes로 권한 확인

# Django auth는 contenttypes 기반
user.has_perm("blog.add_post")

내부적으로 ContentType.objects.get(app_label="blog", model="post") 후 permission row.

자주 보는 패턴

댓글 / 좋아요 / 북마크

class Bookmark(models.Model):
    user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey()

    class Meta:
        unique_together = ("user", "content_type", "object_id")

user.bookmark_set.all()
Bookmark.objects.create(user=user, content_object=post)

여러 모델 (Post, Photo, Video) 모두 북마크 가능.

Activity feed

class Activity(models.Model):
    actor = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    verb = models.CharField(max_length=50)    # "created", "liked", "commented"

    target_ct = models.ForeignKey(ContentType, null=True, on_delete=models.CASCADE, related_name="+")
    target_id = models.PositiveIntegerField(null=True)
    target = GenericForeignKey("target_ct", "target_id")

    action_object_ct = models.ForeignKey(ContentType, null=True, on_delete=models.CASCADE, related_name="+")
    action_object_id = models.PositiveIntegerField(null=True)
    action_object = GenericForeignKey("action_object_ct", "action_object_id")

    timestamp = models.DateTimeField(auto_now_add=True)

# Alice가 Post에 댓글 추가
Activity.objects.create(
    actor=alice,
    verb="commented_on",
    target=post,
    action_object=comment,
)

django-activity-stream 라이브러리가 이 패턴.

Polymorphic 모델 (django-polymorphic 대안)

class Item(models.Model):
    name = models.CharField(max_length=200)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey()    # 실제 객체 (Book, Movie, Game 등)

함정

1. cascade 없음

post.delete()
# post를 참조하는 Comment는 orphan으로 남음!

GenericForeignKey는 FK 제약 없음. 부모 삭제 시 자식 안 지워짐.

해결:

  • 부모에 GenericRelation + cascade 안 됨 (Django 제약)
  • signal로 수동 처리
  • django-polymorphic 같은 라이브러리
from django.db.models.signals import pre_delete

@receiver(pre_delete, sender=Post)
def delete_comments(sender, instance, **kwargs):
    ct = ContentType.objects.get_for_model(instance)
    Comment.objects.filter(content_type=ct, object_id=instance.id).delete()

2. 검색/필터 어려움

Comment.objects.filter(content_object__title__icontains="django")    # 안 됨

특정 타입만 필터:

post_ct = ContentType.objects.get_for_model(Post)
Comment.objects.filter(content_type=post_ct, object_id__in=...)

복잡한 쿼리는 일반 FK가 더 편함.

3. content_object__title 같은 lookup X

Comment.objects.filter(content_object__author=user)    # 안 됨

처음부터 일반 FK + STI/MTI 검토.

4. 인덱스

class Comment(models.Model):
    content_type = ...
    object_id = ...

    class Meta:
        indexes = [
            models.Index(fields=["content_type", "object_id"]),
        ]

(content_type, object_id) 복합 인덱스로 lookup 빠르게.

5. JSON 직렬화

serializer.data    # GenericForeignKey 자동 직렬화 X

DRF는 수동 처리 필요.

class CommentSerializer(serializers.ModelSerializer):
    content_object = SerializerMethodField()

    def get_content_object(self, obj):
        if isinstance(obj.content_object, Post):
            return PostSerializer(obj.content_object).data
        if isinstance(obj.content_object, Photo):
            return PhotoSerializer(obj.content_object).data
        return None

ContentType vs Polymorphic

패턴장단점
GenericForeignKeyDjango 빌트인, 유연. 쿼리 약함, 무결성 약함
django-polymorphic풍부한 다형성 (서브클래스 자동 매핑)
Single Table Inheritance한 테이블 + type 컬럼 (Rails STI 패턴)
Multi-Table InheritanceDjango 기본 (Book(Item))

대부분 일반 FK + 추상 베이스 또는 MTI가 더 명확. ContentTypes는 정말 다형성 필수일 때만.

자주 보는 라이브러리

  • django-polymorphic: 다형성 모델
  • django-taggit: 태그 시스템 (내부적으로 GenericFK)
  • django-activity-stream: 활동 피드
  • django-comments: 댓글 (deprecated, django-fluent-comments)

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기