ks.dgoon.lee log

dboard - comment 관련 코드 재작성


2024/01/19 00:00:44 #django #dev #comment #django-wiki #bulletin board #database #dboard #javascript #jQuery

sort_order 필드를 추가하고 나서, 글 하나에 댓글을 1000개정도 마구 붙인 다음 화면을 띄워 보았다.

쿼리 개수 자체는 그리 많지 않으나 (16개), 만들어진 html 을 그냥 저장했더니  3.5MB...

댓글이 많은 글이라면 인기있는 글일텐데(게다가 마지막 댓글을 받은 글이 위로 올라가는게 기본 동작이다), 여러사람이 동시에 열면 DB가 힘들기 이전에 트래픽이 미칠 수 있다. 일단 댓글 1000개가 있는 글을 볼 때 말이 되는 동작을 하는게 목표. 현재는 django 가 서버사이드에서 html 을 몽땅 그려 내려주고 있다. 만약 내가 새 댓글을 달았다? 그러면 1001개 댓글을 다시 다 그려서 내려보낸다 😱😱😱

  1. 페이징을 해서 10개나 25개, 100개씩만 보여준다? -> 고려해볼 수 있음
  2. 서버에서 fragment view 를 잘 설계해서 빡세게 캐싱하고, 부분부분 업데이트한다? -> 말은 되는데... 예전에 RoR 로 삽질할때 해봤던 적이 있는데 괴로웠다.
  3. 브라우저가 API 로 데이터 가져간 다음에 알아서 화면에 그린다? -> 이게 가장 깔끔하고 효율적이지 않을까?

정도 생각을 해봤다. 조금 더 고민해보면 1번 댓글 페이징은 하면 도움이 될텐데, 이걸 2번(fragment view를 만들어 내려줄거냐) 방식으로 만들거냐 3번(api 를 만들고 클라가 알아서 그릴거냐)으로 만들거냐 하는 구현 방식의 선택이 된다. 풀어야 하는 문제로 바꿔서 생각을 해 보면,

  1. 서버에서 브라우저로 보내는 데이터가 정보량 대비 너무 크다.
  2. 사용자 액션이 있을 때마다 전체 화면을 다시 내려보내는건 너무 비효율적이다. 1번과 더불어 트래픽지옥이 됨.

fragment view 를 만드는걸로 전체 업데이트를 줄여 어느정도 효율화(2번)를 시킬 수 있겠지만, 결국 모든 뷰를 렌더링하고 나서 내려보내기 때문에 한 화면의 전체 용량(댓글 1000개 글을 열었더니 3.5MB 였다는...)이 줄어들진 않는다. 물론 중복이 많으니 압축효율은 좋겠지만... 여기서 중복이란 화면을 구성하기 위한 html, css 등인거고 실제 정보량은 그렇게 크진 않다는 것이지. 댓글 그리기 위해 만들어진 html, css 를 눈으로 보고 대충 가늠해보면 전체 데이터의 10% 정도가 진짜 정보일 것 같다. 말인 즉슨, 트래픽 줄이려면 API 로 데이터만 쓱 받아가야 한다는 이야기. 여기까지 생각하고 나니 선택의 여지가 없다.

apis.py 추가

일단 django app 에 apis.py 를 추가했다. 그리고 urls.py 에서 'api/v1/BLAHBLAH' 들을 views 대신 apis 쪽으로 연결. 섞이면 헷갈리니까 apis.py 에 API 구현들을 몰아 넣었다. 사실 글 작성시 백그라운드로 draft 를 저장하거나, 게시판 순서 변경 등을 위해 $.ajax 요청을 받는 엔드포인트들이 있긴 했다. 이참에 걔들도 모두 이름을 api/v1/BLAHBLAH 로 바꿔서 apis.py 로 이동시켰다.

댓글 CRUD 구현

댓글 쓰기, 댓글 목록 가져오기, 댓글 고치기, 댓글 삭제하기 등은 기존 views.py 에 구현되어 있던 코드가 있어서, 그걸 기반으로 적당히 API 를 만들어 넣었다. 만들고 나서 잠깐 테스트를 해 보았는데,

  1. 댓글 1000개 데이터 요청은 response 크기가 대략 290KB. 전체 화면 용량 대비 1/10 이 안된다.
  2. 댓글이 사라진 글 화면의 html 크기는 28KB. 3.5MB 에서 1/100 이하로 줄었다... 저게 전부 댓글 크기였고만.
  3. 그러니까, 28KB 화면을 일단 띄우고 API 로 290KB 데이터 가져와서 화면에 그려주는걸로 바꾸기만 하면, 기존과 똑같은 동작을 해도 트래픽이 1/10 이하(3.5MB -> 0.028 + 0.29 = 0.318MB, 대략 -91%다)로 줄어든다. 👏 안할 이유가 없는데!?

다른 구현 없이 요렇게만 해도 훨 나아질 것 같다. 일단 화면에 그려보자.

댓글 화면에 그리기

django template 으로 화면에 그리던 부분이 이미 있으니, 이걸 가져와서 <template>태그로 감싼 다음 내용을 비우고 껍데기만 남겼다. 그리고 가져온 comment 목록을 이터레이션하면서 template 클론 -> 내용 채우기 -> appendTo(targetElement) 를 쭉 했다. 이미 정렬된 배열이고, depth도 다 알고 있기 때문에 보기엔 트리처럼 보이지만 각 댓글들은 padding-leftdepth 따라 다르게 가지고 있는 1차원 배열이다. 트리 구조로 만들지 않음.

모던 브라우저+인터넷 환경은 정말 빠르다. (서버+DB)는 Azure KoreaCentral 에 있고, 나는 서울 어딘가에 있는데

  • 290KB 데이터 가져오는데 150ms
  • 가져온 다음 화면에 렌더링하는건 시간이 느껴지지 않을 정도

결국 화면이 뜨고 0.1~0.2초후에 댓글 1000개가 쭉 생겨났다. 오... 그럴싸하다.

이 템플릿 클론+내용 채우기 하면서 jQuery 사용법을 좀 익혔다. 나중에 jQuery 도 튜토리얼 한번 보긴 봐야겠다.

새로 댓글 달기

  • 1차 구현: 댓글을 달면 전체 comment tree 를 다시 그린다. 이렇게 하면 처음 만들어 두었던 django template 으로 전체를 그리는 구현 대비 트래픽이 1/10 이하로 줄어든다. comment 만 업데이트 하니까 해당 화면을 띄울 때 필요로 하는 쿼리 16개를 아낄 수 있고. comment 가져오는건 쿼리 하나면 됨. 트래픽도, DB 리소스도 다 아껴짐.
  • 2차 구현: 필요한 부분만 업데이트. 최적화 하자면 많이 할 수 있을 것도 같지만, 일단 두가지만 했다.
    1. depth=0인 댓글을 다는 경우, 새로 생성한 comment 정보를 기존 comment 목록 제일 끝에 붙인다.
    2. depth>0 인 댓글을 다는 경우, 해당 thread (depth=0인 댓글을 루트로 하는 subtree) 만 다시 그린다. 약간 깜빡하는게 있긴 하지만 뭐 참아줄만 함.
    3. 사실 바로 상위 parent 부터 다시 그리는게 더 좋긴 한데, subtree 를 빠르게 재구성하는 정보(descendants)가 thread 기준으로만 있다. post=thread 로 했으면 이것도 안됐을것... 굳이 한 글에 달린 댓글들이지만 depth=0 인 댓글들은 따로 root 로 보고 sort_order 를 계산한 이유가 이것이다.

참고: 여기서 thread 란? depth=0 인 최상위 댓글을 루트로 해서 그 아래 달린 모든 대댓글들의 집합. thread 기준 descendants 를 빨리 찾을 수 있도록 DB 설계를 해 두었다. parent 는 바로 아래위 정보만 가지고 있기 때문에 parent 만 가지고 특정 댓글의 descendants 를 모두 찾으려면 쿼리를 여러번 해야 한다.

depth=0인 최상위 댓글을 다는 경우, 트래픽은 (댓글 컨텐츠 길이에 따라 다르겠지만) 1KB 미만이고 DB 접근도 별로 없다. depth>0 인 댓글을 다는 경우라면, 트래픽이야 여전히 얼마 안되지만 sort_order 업데이트 때문에 DB 에 쿼리도 몇번 있고, 최악의 경우 한 thread 에 있는 다른 모든 댓글이 업데이트된다. 댓글 1000개 가 아니라 대댓글 1000개가 되면 퍼포먼스 문제가 좀 생길 수도 있겠지만... 그래도 write 보다는 read 가 훨씬 더 많이 일어날 것이기 때문에 read 가 빠른 쪽이 나을것 같다. write/read 비중의 가정이 깨지면 그때 또 최적화 하면 되지 뭐.

댓글 업데이트/삭제

이건 comment tree 구조를 바꾸지는 않기 때문에 비교적 깔끔하다. API 가 성공하면, 해당 comment 를 찾아서 내용을 바꾸면 된다. dboard 에서는 댓글을 삭제하더라도 comment tree 구조에는 영향을 주지 않고 *삭제한 댓글입니다* 라고 표시해주기 때문에 이 부분이 쉬워졌다. 화면이 깜빡이지도 않음.

코드 정리

여기까지 해 놓고 보니... 기존 views.py 에 있던 comment 관련 코드는 이제 모두 삭제. 댓글은 API 와 JS(jQuery)로 모두 구현했다.

django template 파일에 JS 코드를 만들어 넣고 있었는데, 꼭 필요한 몇줄 남기고 나머지는 post.js 에 몰아넣은 다음에 static file 로 취급하고, 댓글 로딩이 필요한 데에서만 가져다 쓰게 했다. 그러면 실서비스 환경에서 post.jsCDN 타겠지.

마지막 생각

이정도면... 사용성 문제 아니면 굳이 페이징 안해도 되는거 아니야?? 라는 생각이 든다. 우선순위 뒤로 미뤄두고 다른거 먼저 해야겠다.

기술적으로는, 지금 댓글의 구조와 디자인이 명확하게 구분되지 않은 상태인데 ... 라고 써두고 보니 디자인 바꾸려면 template 이랑 내용 채우는 함수 하나만 손대면 되긴 되네...? 여튼 조금 더 우아하게 정리하고 삽질하면 disqus 같은 서비스처럼 쉽게 가져다 쓸 수 있게 할 수 있을 것 같은데, 이것은 언젠가 먼 미래의 나에게 하고싶으면 해보라고 맡긴다.



댓글 0개