회원가입

[리팩토링] 8. (2) 메인페이지 좋아요, 댓글 수 Board View 코드 리팩토링

Beany 2024-05-11

AS-IS Code

def get_board_set_from_board_group(request, board_group_id):
    ...완료!!!


def home(request):
    ...이번장!!!

def board(request, board_url):
    ...

def post_detail(request, board_url, pk):
    ...

def reply_write(request, board_url, pk):
    ...

def rereply_write(request, board_url, pk):
    ...

def reply_delete(request, board_url, pk):
    ...

def rereply_delete(request, board_url, pk):
    ...

def like(request, board_url, pk):
    ...

get_board_set_from_board_group 리팩토링 정보 보기

Board 앱의 View 함수들을 살펴보니 총 9개의 코드가 존재합니다.
하나하나씩 불필요한 코드를 제거하거나 리팩토링해 보겠습니다

 


 

지금은 매번 이 페이지를 조회할 때마다 아래와 같은 코드를 이용해서 좋아요와 댓글 수를 가져오고 있습니다.

def home(request):
    ...
    liked_ordered_post_qs = get_active_posts().select_related(
        'board',
        'author',
    ).annotate(
        reply_count=Count('replys', distinct=True) + Count('rereply', distinct=True),
        like_count=Count('likes', distinct=True),
    ).order_by(
        '-like_count',
        '-reply_count',
        '-id',
    )[:6]

물론 이런 방법으로 데이터를 가져오는 것도 있습니다. 하지만 댓글의 숫자나 좋아요의 숫자가 많아지면 성능이 나빠지는 경향이 있습니다.

지금은 게시글의 좋아요 수와 댓글 수를 각각의 테이블에서 특정 게시글 ID로 조회한 후, 그만큼의 개수를 가져오고 있습니다.

아래 쿼리와 같이 SQL 이 조회가 됩니다.

SELECT "board_post"."id",
       ...
       (COUNT(DISTINCT "board_reply"."id") + COUNT(DISTINCT "board_rereply"."id")) AS "reply_count",
       COUNT(DISTINCT "board_like"."id") AS "like_count",
  FROM "board_post"
  LEFT OUTER JOIN "board_reply"
    ON ("board_post"."id" = "board_reply"."post_id")
  LEFT OUTER JOIN "board_rereply"
    ON ("board_post"."id" = "board_rereply"."post_id")
  LEFT OUTER JOIN "board_like"
    ON ("board_post"."id" = "board_like"."post_id")
 INNER JOIN "board_board"
    ON ("board_post"."board_id" = "board_board"."id")
 INNER JOIN "accounts_user"
    ON ("board_post"."author_id" = "accounts_user"."id")
 WHERE "board_post"."is_active"
 GROUP BY "board_post"."id",
       ...
 ORDER BY "like_count" DESC, "reply_count" DESC, "board_post"."id" DESC
 LIMIT 6

 

다른 방법이 존재합니다.

방식은 이렇습니다.

  1. Post 테이블에 추가적인 컬럼 like_count, reply_count, rereply_count를 만듭니다.
  2. 사용자가 좋아요를 누르거나 좋아요를 취소하는 행동을 하면, Like 테이블에서 Post 기준으로 있는 데이터 개수만큼 조회한 후, like_count를 최신화합니다. (reply_count, rereply_count도 마찬가지입니다.)
  3. 조회할 때, Count SQL 문을 사용하지 않고, Post에 최신화된 like_count, reply_count, rereply_count 컬럼으로 데이터를 보여줍니다.

로직을 조금 복잡하게 할 것인가, 아니면 성능을 더 좋게 할 것인가?

Trade-off로 따지면, 이번 케이스는 후자가 더 좋다고 판단됩니다.

 

이제 코드를 작성해 봅시다.

 

1. 코드 작성


각각의 코드는 아래와 같습니다. 

리펙토링은 나중에 신경 쓰고, 이번에 하려는 작업만 신경 씁시다.

(리펙토링은 다른 게시글에...)

@login_required(login_url='/')
def reply_write(request, board_url, pk):
    if request.method == 'POST':
        post = get_object_or_404(Post, board__url=board_url, pk=pk)
        if request.POST.get('reply_body'):
            Reply.objects.create(post=post, author=request.user, body=request.POST.get('reply_body'))

    return HttpResponseRedirect(reverse('board:post', args=[board_url, pk]))


# 답글 작성
@login_required(login_url='/')
def rereply_write(request, board_url, pk):
    reply = get_object_or_404(Reply, id=pk)
    if request.method == 'POST' and request.POST.get('rereply'):
        rereply = Rereply()
        rereply.reply = reply
        rereply.author = request.user
        rereply.body = request.POST.get('rereply')
        rereply.save()
    return HttpResponseRedirect(reverse('board:post', args=[board_url, reply.post.id]))


# 댓글 삭제
@login_required(login_url='/')
def reply_delete(request, board_url, pk):
    reply = get_object_or_404(Reply, id=pk)
    post_id = reply.post.id
    if reply.author == request.user or request.user.is_superuser:
        reply.delete()
    return HttpResponseRedirect(reverse('board:post', args=[board_url, post_id]))


# 답글 삭제
@login_required(login_url='/')
def rereply_delete(request, board_url, pk):
    rereply = get_object_or_404(Rereply, id=pk)
    post_id = rereply.post.id
    if rereply.author == request.user or request.user.is_superuser:
        rereply.delete()
    return HttpResponseRedirect(reverse('board:post', args=[board_url, post_id]))


# 좋아요 추가 삭제
@login_required(login_url='/')
def like(request, board_url, pk):
    post = get_object_or_404(Post, id=pk)
    qs = Like.objects.filter(author=request.user, post=post)
    if qs.exists():
        qs.delete()
    else:
        Like.objects.create(author=request.user, post=post)
    return HttpResponseRedirect(reverse('board:post', args=[board_url, pk]))

 

먼저 해야하는 작업은 테이블 컬럼 추가합니다.

class Post(TimeStampedModel):
    title = models.CharField(max_length=150)
    body = RichTextUploadingField()
    def_tag = models.CharField(max_length=150, null=True, blank=True)
    post_img = models.ImageField(upload_to='post_img/', null=True, blank=True)
    board = models.ForeignKey(Board, on_delete=models.CASCADE)
    author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='posts', on_delete=models.CASCADE)
    tag_set = models.ManyToManyField('Tag', blank=True)
    like_count = models.BigIntegerField(default=0, db_index=True)  # 추가
    reply_count = models.BigIntegerField(default=0, db_index=True)  # 추가
    rereply_count = models.BigIntegerField(default=0, db_index=True)  # 추가
    is_active = models.BooleanField(default=True)

 

Service Layer 에 좋아요, 댓글, 대댓글 개수를 업데이트하는 함수를 만듭니다.

def update_post_reply_count(post_id: int) -> None:
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        return
    post.reply_count = Reply.objects.filter(post_id=post_id).count()
    post.save(update_fields=('reply_count',))


def update_post_rereply_count(post_id: int) -> None:
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        return
    post.rereply_count = Rereply.objects.filter(post_id=post_id).count()
    post.save(update_fields=('rereply_count',))


def update_post_like_count(post_id: int) -> None:
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        return
    post.like_count = Like.objects.filter(post_id=post_id).count()
    post.save(update_fields=('like_count',))

 

새로 만든 함수로 View 코드를 수정합니다.

@login_required(login_url='/')
def reply_write(request, board_url, pk):
    if request.method == 'POST':
        post = get_object_or_404(Post, board__url=board_url, pk=pk)
        if request.POST.get('reply_body'):
            Reply.objects.create(post=post, author=request.user, body=request.POST.get('reply_body'))
            update_post_reply_count(pk)  # 여기

    return HttpResponseRedirect(reverse('board:post', args=[board_url, pk]))


# 답글 작성
@login_required(login_url='/')
def rereply_write(request, board_url, pk):
    reply = get_object_or_404(Reply, id=pk)
    if request.method == 'POST' and request.POST.get('rereply'):
        rereply = Rereply()
        rereply.reply = reply
        rereply.author = request.user
        rereply.body = request.POST.get('rereply')
        rereply.save()
        update_post_rereply_count(pk)  # 여기
    return HttpResponseRedirect(reverse('board:post', args=[board_url, reply.post.id]))


# 댓글 삭제
@login_required(login_url='/')
def reply_delete(request, board_url, pk):
    reply = get_object_or_404(Reply, id=pk)
    post_id = reply.post.id
    if reply.author == request.user or request.user.is_superuser:
        reply.delete()
        update_post_reply_count(post_id)  # 여기
    return HttpResponseRedirect(reverse('board:post', args=[board_url, post_id]))


# 답글 삭제
@login_required(login_url='/')
def rereply_delete(request, board_url, pk):
    rereply = get_object_or_404(Rereply, id=pk)
    post_id = rereply.post.id
    if rereply.author == request.user or request.user.is_superuser:
        rereply.delete()
        update_post_rereply_count(post_id)  # 여기
    return HttpResponseRedirect(reverse('board:post', args=[board_url, post_id]))


# 좋아요 추가 삭제
@login_required(login_url='/')
def like(request, board_url, pk):
    post = get_object_or_404(Post, id=pk)
    qs = Like.objects.filter(author=request.user, post=post)
    if qs.exists():
        qs.delete()
    else:
        Like.objects.create(author=request.user, post=post)
    update_post_like_count(pk)  # 여기
    return HttpResponseRedirect(reverse('board:post', args=[board_url, pk]))

 

 

이제 업데이트하는 로직을 추가했으니, 좋아요와 댓글 수를 조회하는 코드를 업데이트합니다.

def home(request):
    ...
    liked_ordered_post_qs = get_active_posts().select_related(
        'board',
        'author',
    ).order_by(
        '-like_count',
        '-reply_count',
        '-id',
    ).only(  # only 이용!
        'id',
        'board__url',
        'author__nickname',
        'title',
        'body',
        'created_at',
        'like_count',
        'reply_count',
        'rereply_count',
    )[:6]
    ...
    liked_ordered_posts=[
        HomePost(
            id=liked_ordered_post.id,
            board_url=liked_ordered_post.board.url,
            title=liked_ordered_post.title,
            body=liked_ordered_post.body,
            like_count=liked_ordered_post.like_count,
            reply_count=liked_ordered_post.reply_count + liked_ordered_post.rereply_count,  # 여기
            author_nickname=liked_ordered_post.author.nickname,
            created_at=liked_ordered_post.created_at.strftime('%Y-%m-%d'),
        )
        for liked_ordered_post in liked_ordered_post_qs
    ],
    ...

 

이제 쿼리를 보면 너무 깔끔합니다.

 

이렇게 한 Controller 안에서 처리하는 방법 말고, Event 방식으로도 업데이트가 가능합니다.
이벤트는 너무 오버스펙이라고 판단하여 사용자가 함수를 호출할 때만 작업하도록 했습니다.

 

끝~!

1 0
블로그 일기
제 블로그의 고도화 과정을 설명합니다. 이는 코드 리팩토링과 추가된 기능들에 대해 기록하기 위한 게시판입니다. 어떤 기능이 추가되었는지, 무엇이 개선되었는지 등 고도화되는 과정을 자세히 다룰 예정입니다.
Yesterday: 456
Today: 51