왜 쓰는가?

 

인스타그램과 같은 피드기능을 만드려한다. 하지만 피드에는 다양한 타입을 지원해야한다.

여기서 말하는 다양한 타입이란 인스타그램의 일반적인 게시물(사진, 글)뿐만 아니라 게임, 장소 등등을 포함한다.

어떻게하면 효율적으로 피드 기능을 만들 수 있을까?

 

 

각자 만들기

 

게시물(Post), 게임(Game), 장소(Location) 이렇게 3개의 타입이 Feed에 있어야 하며, 늘 최신순으로 display되어야 한다는 요구사항이 들어 왔다고 가정하자.

이때 가장 처음 생각할 수 있는 방법은 DB(Django Model)에서 Post, Game, Location을 각각 만드는 방법이다.

가장 쉽게 생각할 수 있지만 이 방법을 따르면 우선 최소 3번의 쿼리를 해야하며, Union 또는 코드 상에서 합쳐줘야 하는 번거로움이 있다.

또한 Feed는 마우스 스크롤을 내리면 새로운 Feed를 계속 불러와야하니 개발을 하기도 상당히 번거롭다.

해서 내가 추천하는 방법은 Django Proxy를 이용하는 방법이다. 물론 장,단점이 있지만 메인 기능이 Feed라면 생각 해볼만한 옵션이다.

 

 

Django Proxy를 활용한 Feed 다형성 구현

 

Java JPA ORM에서는 이를 Single Table Strategy라 부른다. 특징으로는 하나의 테이블(Feed)에 필드를 몰아서 생성한다.

DB 정규화의 문제, 특히 null 값 허용을 많이 해야하는 단점이 있지만, 공통 부분 재사용, 다형성 등 장점 또한 많다. 코드를 보며 자세히 살펴보자.

 

 

사용 예제 코드

Feed는 Game, Post의 부모 클래스이며, 실제 DB로 사용되는 모델이다.

Game과 Post는 코드상의 가상의 클래스이며, 실제 DB Table은 가지지 않는다.

Feed의 필드 및 메소드를 간략히 소개하겠다.

필드

  • dtype : 해당 Feed의 타입이 무엇인지(Post, Game, Team) 중 하나
  • title, body : Post 전용 필드
  • game_name, game_location : Game 전용 필드
  • joined_users : Game, Team 공통 필드이며 참가자를 의미한다.
  • like_count : Post, Game, Team의 공통 필드, 해당 피드의 좋아요 개수를 의미한다.

메소드

  • get_queryset : Post, Game Manager에 정의된 TYPE을 참조해 TYPE의 클래스만 필터링해 가져온다.
  • create : Post, Game Manager에 정의된 TYPE을 참조해 해당 클래스 생성

나머지 자세한 사용법은 TEST 코드를 참조하자.

from typing import Optional

from django.contrib.auth.models import User
from django.db import models
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _


class FeedType(models.TextChoices):
    POST = 'post', _('포스트')
    GAME = 'game', _('게임')
    TEAM = 'team', _('팀')


class Feed(models.Model):
    class FeedManager(models.Manager):
        def get_queryset(self):
            queryset: QuerySet = super().get_queryset()
            dtype: Optional[str] = getattr(self, 'TYPE', None)
            if dtype is not None:
                queryset = queryset.filter(dtype=dtype)
            return queryset

        def create(self, **kwargs):
            return super().create(**kwargs, dtype=self.TYPE)

    title = models.CharField(max_length=128)
    body = models.TextField()
    dtype = models.CharField(
        max_length=10,
        choices=FeedType.choices,
        default=FeedType.POST,
    )
    like_count = models.PositiveIntegerField(default=0)
    joined_users = models.ManyToManyField(User, blank=True)
    game_name = models.CharField(max_length=128)
    game_location = models.CharField(max_length=128)
    room_name = models.CharField(max_length=128)

    objects = FeedManager()

    def add(self, user: User):
        self.joined_users.add(user)

    def like(self):
        self.like_count += 1
        self.save()


class Post(Feed):
    class PostManager(Feed.FeedManager):
        TYPE = FeedType.POST

    objects = PostManager()

    class Meta:
        proxy = True

    def __str__(self):
        return f'{self.title}'


class Game(Feed):
    class GameManager(Feed.FeedManager):
        TYPE = FeedType.GAME

    objects = GameManager()

    class Meta:
        proxy = True

    def __str__(self):
        return f'{self.game_name}'

 

테스트 코드
from django.contrib.auth.models import User
from django.test import TestCase

from core.models import Game, Feed, Post


class GameTest(TestCase):
    def test_model(self):
        user = User.objects.create(username="a")
        user.save()
        game: Game = Game.objects.create(game_name="한게임", game_location="서울")
        self.assertEqual(game.game_name, "한게임")
        self.assertEqual(game.dtype, "game")
        self.assertEqual(game.dtype.label, "게임")

        post: Post = Post.objects.create(title="title", body="body")
        self.assertEqual(post.title, "title")
        self.assertEqual(post.body, "body")
        self.assertEqual(Feed.objects.count(), 2)
        self.assertEqual(Game.objects.count(), 1)
        self.assertEqual(Post.objects.count(), 1)

        # feed(game) add test
        game.add(user)
        self.assertEqual(game.joined_users.count(), 1)

        # feed(game) like test
        game.like()
        game.like()
        self.assertEqual(game.like_count, 2)

 

소스코드 : https://github.com/seunwoolee/Django-Proxy-Model

+ Recent posts