이 API의 Model은
abstracts.py 파일에서 확인할 수 있는데,
빨간색 박스의 Model의 코드를 보면
left값과 right값이 없는 것을 보면 MTPP가 아니라
인접 리스트 모델을 사용하고 있음을 확인할 수 있다.
따라서 IT업계에서는
사실상 MTPP는 사장된 솔루션이라 보는게 맞지 않나 싶다.
왜냐하면 MTPP 보다는 인접 리스트 모델의 쪽이 좀 더 이해하기 쉬우며
이는 가독성이 더 좋다는 의미이기 때문이다.
물론 두 개를 구현해보고 성능 테스트를 한 후에
결론을 내리는 것이 좋겠지만,
이 글의 성격은 성능 보다는 구현에 포커스를 맞추고 있기 때문에
성능 이슈를 논외로 하고 인접 리스트 모델을 사용하도록 하겠다.
Django에서 인접 리스트 모델(Adjacency List Model)로 댓글 기능 구현
그럼 이제 직접 Django 에서 인접 리스트 모델(Adjacency List Model)로
중첩된 댓글 기능을 구현해보자.
이전 글에서 설명했듯이
인접 리스트 모델은 상위 parent 노드를 가르키는 번호가 들어가게 된다.
따라서 추가되는 것은 Django의 기존 Comment 관련 Model에
자기 자신(Model)을 Parent로 가질 수 있도록 설정해주면 된다.
class Comment(models.Model):
c_name = models.CharField('Commented name',max_length = 10, help_text = 'You can enter up to 10 characters.')
c_date = models.DateField('Commented date',default = datetime.date.today)
c_description = models.CharField('Text',max_length = 100, help_text = 'Enter your comment. n\You can enter up to 100 characters.')
post = models.ForeignKey('post', on_delete = models.SET_NULL, null = True)
#parent filed of Adjacency List Model
parent = models.ForeignKey('self', on_delete = models.CASCADE, null = True)
위의 사진과 코드는 Comment 기능을 구현하기 위한
Model인데, 빨간색 박스가 인접 리스트 모델로 구현하기 위해 추가 한 코드이다.
parent 번호를 저장할 필드를 추가하고,
이를 참조할 수 있게 ForeignKey로 만들어 준다.
그 다음은 당연히 Model 구조가 바뀌었기 때문에
마이그레이션을 다시 해야 한다.
그 후에 Admin App을 들어가보면
위와 같이 수정 할 수 있는 Parent라는 새로운 필드가 생겼고
참고 가능한(추가 했던 댓글)들이 풀 다운에 모두 표시되게 된다.
확인이 끝났다면 View를 추가 해보자.
다만, 위의 방법은 서버 단에서 가장 적게 수정하는 방법이며,
클라이언트 단에서의 구현이 조금 복잡할 수 있다.
왜냐하면 우리가 원하는 것은 중첩된 표현을 해야하기 때문이며,
단순히 parent를 추가했다고 해서
그것 만으로 클라이언트 단에서 표현하기에는 부족하기 때문이다.
따라서 이외에도 몇 가지 솔루션을 제시해 볼 수 있는데
다른 솔루션에 대해서는 아래와 같다.
① DB단에서 구현
DB단에서 구현도 가능한데
위에서 언급한 바와 같이 parent라는 필드를 만드는 것으로는
중첩된 댓글 기능을 구현할 수는 없다.
서버 단과 클라이언트 단은 별개이기 때문이기도 하며
서버단 에서 로직 상으로 문제가 있던 없건
값만 준다면 클라이언트단과는 딱히 상관 없기 때문이다.
왜냐하면 일반적으로 생각하는 중첩된 댓글들은
댓글에 댓글이 그리고 그 댓글에 댓글이 달릴 수록
화면 상으로 오른쪽으로 이동하기 때문이다.
이를 표현하기 위해서는 Level이나 Depth를 포함해
경우에 따라는 더 많은 필드를 필요로 하는데
DB단(서버 단)에서 새로운 컬럼들을 더 추가하고,
이를 SQL로 받아오는 형식으로도 구현이 가능하다.
실제로 구글링을 해보면 이런 식의 구현도 꽤 나 찾아볼 수 있다.
다만 이 경우 DB에서 많은 처리가 이루어지기 때문에
DB에 부담이 많이 갈 수 있으며,
SQL로 구현해야하기 때문에 구현에 어려움이 있을 수도 있다.
하지만, 일반적으로 한 게시글에 다는 댓글은 그리 많지는 않기 때문에
운용하는데에는 큰 문제는 없어 보인다.
물론 이는 위에서도 언급한 것과 같이
실제 과부하 테스트를 해볼 필요가 있다.
② 중첩된 댓글의 새로운 모델을 활용한 구현
처음 제시한 솔루션은 이전에 설명했듯이
재귀로 불러오기 때문에
기본적으로 성능에 대한 의심이 있을 수 있으며,
인접 리스트 모델에 대한 이해가 필요하기 때문에
다소 가독성이 떨어질 수 있다.
기존 Comment라는 모델에
댓글이 달릴 경우 생성되는 모델을 새로 추가하는 방법이다.
이쪽이 다소 이해가 편할 수 있다.
다만, 계속해서 참고해나가는 것이 아니고,
새로운 모델을 계속해서 생성하기 때문에
첫 번째 방법과 유사하게 성능 문제가 있을 수 있 수 있다.
③ 인접 리스트 모델의 오픈 소스를 활용한 구현
잘 구현된 인접 리스트 자료구조 오픈 소스를 활용하는 방법이다.
Python이 훌륭한 점은 오픈 소스를 활용하기 매우 편하다는 것인데
이를 활용하는 방법이다.
시스템이 커지면 커질 수록, 엔터프라이즈 급의 시스템에서
활용하기 가장 좋은 솔루션이라고 생각 한다.
왜냐하면, 가장 퍼포먼스가 뛰어날 것으로 생각되기 때문이다.
(물론 성능 테스트를 해봐야 확실하겠지만)
후에 시간이 남는다면,
바꿔보고 싶은 솔루션이다.
④ 중첩된 댓글의 새로운 View을 활용한 구현
Model이 아닌 View를 추가해서 구현할 수도 있다.
내가 소개 하고자하는 솔루션은 새로운 View를 추가하고,
클라이언트 단의 box를 활용한 level과 depth를 통해 화면에
중첩된 댓글을 구현하는 방법이다.
def reply_comment_create(request, comment_id):
comment = get_object_or_404(Comment, id = comment_id)
post = comment.post
form = CommentCreateForm(request.POST)
blog_admin_flag = admin_permissions_confirm(request)
#if 'POST' request
if request.method == 'POST':
form = CommentCreateForm(request.POST)
if form.is_valid():
in_create_description = form.cleaned_data['create_description']
create_reply = Comment(
post = post,
parent = comment,
c_name = request.user.username,
c_date = datetime.date.today(),
c_description = in_create_description,
)
create_reply.save()
# redirect to 'post-detail' that 'blog/blogs/post_id'
return HttpResponseRedirect(reverse('post-detail', args=[post.id]))
#if 'GET' request
else:
form = CommentCreateForm()
context = {
'form': form,
'post': post,
'comment': comment,
'blog_admin_flag': blog_admin_flag,
}
return render(request, 'dryblog/post_form.html', context)
여기서 유심히 살펴봐야할 부분은
빨간색 박스 부분이다.
해당 View는 중첩된 댓글을 달 경우 참고하게 되는 View인데,
기존에 있던 Comment 모델의 parent 필드에
부모 노드가 되는 comment 객체를 넣어주는 것을 확인할 수 있다.
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
</head>
{% extends "base_generic.html" %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title">Title: {{ post.p_title }}</h1>
<div class="content">
<p><strong>Posted Name:</strong> <a href="{% url 'user_detail' post.p_name %}">{{ post.p_name }}</a></p> <!-- author detail link not yet defined -->
<p><strong>Posted Date:</strong> {{ post.p_date }}</p>
<p><strong>Description:</strong> {{ post.p_description }}</p>
</div>
<!-- <p><strong>Genre:</strong> {% for genre in book.genre.all %} {{ genre }}{% if not forloop.last %}, {% endif %}{% endfor %}</p> -->
<!-- <div style="margin-left:20px;margin-top:20px"> -->
<a href="{% url 'comment_create' post.id %}">Create</a>
<hr>
<h4 class="title is-5">Comments</h4>
{% for comment in comment_list %}
<hr>
<!-- <p class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'm' %}text-danger{% else %}text-warning{% endif %}">{{ copy.get_status_display }}</p>
{% if copy.status != 'a' %}
<p><strong>Due to be returned:</strong> {{copy.due_back}}</p>
{% endif %} -->
<div class="box">
<p><strong>Commented Name:</strong> {{comment.c_name}}
<a href="{% url 'comment_update' post.id comment.id %}">Update</a>
<a href="{% url 'comment_delete' post.id comment.id %}">Delete</a>
</p>
<p><strong>Commented Date:</strong> {{comment.c_date}}</p>
<p><strong>Description:</strong> {{comment.c_description}}</p>
<a href="{% url 'reply_comment_create' comment.id %}">Reply</a>
{% with reply_comment_list=comment.comment_set.all %}
{% include 'dryblog/include/reply_comment_detail.html' %}
{% endwith %}
<!-- <p class="text-muted"><strong>Id:</strong> {{copy.id}}</p> -->
</div>
{% endfor %}
</div>
</section>
{% endblock %}
클라이언트 단에서의 로직은 빨간색 박스를 나타내며
각 댓글의 level 또는 depth는 section 태그와 container, box 태그를 통해 구현 한다.
이를 통해 DB단에서 level이나 depth 컬럼을 추가하지 않고도
중첩된 댓글의 구현이 가능하다.
여기서 유심히 살펴봐야하는 곳은
With 태그를 활용해서 계속해서
dryblog/include/reply_comment_detail.html 안의 코드들을 불러오는 부분인데
dryblog/include/reply_comment_detail.html의 코드를 살펴보면 아래와 같다.
{% for reply_comment in reply_comment_list %}
<div class="box">
<p><strong>Commented Name:</strong> {{reply_comment.c_name}}</p>
<p><strong>Commented Date:</strong> {{reply_comment.c_date}}</p>
<p><strong>Description:</strong> {{reply_comment.c_description}}</p>
<a href="{% url 'reply_comment_create' reply_comment.id %}">Reply</a>
{% with reply_comment_list=reply_comment.comment_set.all %}
{% include 'dryblog/include/reply_comment_detail.html' %}
{% endwith %}
</div>
{% endfor %}
이 부분에서 빨간색 박스 부분이 중요한데
계속해서 reply_comment.id를 참고해 링크를 생성하게 되는데
처음 생성된 comment 객체가 아닌 reply 링크를 통해 생성된
comment 객체를 참고하게 된다.
여기가 재귀를 이용하는 부분으로 가장 중요한 부분으로
여기에 기존 comment 객체의 id를 넣게된다면
클라이언트 단에서 3단 이상으로는 표현되지 않기 때문에 주의해야 한다.
마지막으로 맨 상단에 위와 같은 시트 스타일을 추가할 필요가 있다는 점을 잊지 말자.
정상적으로 구현이되었다면 위와 같이
중첩된 댓글이 브라우저 화면에 표시되는 것을 확인할 수 있다.
규모에 따른 솔루션에 대해
실제 많은 솔루션들을 보고 개발도 해본 결과
패턴을 크게 두 가지로 나눌 수 있다는 결론을 내렸다.
여기서 규모란,
해당 서비스를 이용하는 사용자의 규모를 말한다.
① 이용자가 적은 시스템
이용자가 적은 시스템에서는
어떤 솔루션도 나쁘지 않다고 생각 한다.
물론 시스템의 퍼포먼스는 당연히
댓글의 댓글 정도만 허용하는 것이 좋지만,
중첩된 댓글 기능은
사용자를 좀 더 편안하게 해줄 수 있기 때문에
중첩된 댓글 기능을 솔루션으로 활용하는 것이 좋을 수도 있다.
따라서 중첩된 댓글도, 댓글의 댓글 정도만 허용하는 것도
모두 훌륭한 솔루션이라 생각 한다.
대표적으로 블로그의 댓글 기능을 예로 들 수 있다.
② 이용자가 많은 시스템
이용자가 많은 시스템에서는
일부분 타협할 수 밖에 없는데,
대부분의 큰 규모의 시스템에서는
중첩된 댓글을 허용하지 않는다.
따라서 이용자가 많은 시스템을 개발한다면,
댓글의 댓글 까지만 허용하는 것이 좋을 수 있다.
다만, 이럴 경우 '대상'에 대한 혼란을 야기할 수 있기 때문에
이를 보완할 수 있는 추가적인 솔루션이 필요할 것이다.
대표적으로 네이버 뉴스 댓글과 같은 시스템을 예로 들 수 있다.
결론
DB에서 계층적 데이터를 구현하기 위해
어떤 솔루션이 가장 좋을까에 대한 이야기의 결론으로서
여전히 인접 리스트 모델(Adjacency List Model)을 사용하는 것이
적절하다는 결론을 조심스럽게 내려볼 수 있을 것이다.
하지만, 애초에 댓글이라는 것은
한 게시물에 그렇게 많이 달리지 않기 때문에
인접 리스트 모델로도 충분히 솔루션으로서 사용할 수 있다고 생각한다.
물론 위에서 언급했다시피
최고의 솔루션은 이에 맞는 새로운 자료 구조를 만들어
제시하는 것이 최고의 솔루션이며,
잘 구현되어 있는 인접 리스트 모델 등과 같은
오픈 소스를 사용하는 것이 차선의 솔루션 일 것이다.
다만 여기서 다른 개발자들이 이해를 쉽게 하기 위해
내가 제시한 방법을 통해 좀 더 가독성을 높일 것인지,
아니면 가독성을 좀 버리고,
오픈 소스를 사용할 것인지에 대한 것은
이를 도입하려하는 엔지니어의 몫일 것이다.