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
다른 방법이 존재합니다.
방식은 이렇습니다.
로직을 조금 복잡하게 할 것인가, 아니면 성능을 더 좋게 할 것인가?
Trade-off로 따지면, 이번 케이스는 후자가 더 좋다고 판단됩니다.
이제 코드를 작성해 봅시다.
각각의 코드는 아래와 같습니다.
리펙토링은 나중에 신경 쓰고, 이번에 하려는 작업만 신경 씁시다.
(리펙토링은 다른 게시글에...)
@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 방식으로도 업데이트가 가능합니다.
이벤트는 너무 오버스펙이라고 판단하여 사용자가 함수를 호출할 때만 작업하도록 했습니다.
끝~!