관련지식
nosql, scheme, nedb, mongodb

요즘 계속 만들고 있는게 있어서 블로그에 신경을 못쓰고 있었네요. 요즘 NeDB를 매우 많이 쓰고 있어서 RDB와는 다른 NoSQL의 설계 특징을 이야기해보려고 합니다.

웹페이지를 만들면 게시판이 빠지지 않고, 게시판에는 대부분 조회수와 좋아요 기능이 있을것 입니다. ‘조회수’와 ‘좋아요’ 기능이 모든 게시판마다 동일한 기능을 가질 필요는 없겠지만 ‘좋아요’ 는 일반적으로 동일게시물에 대해 한 사용자가 한번만 할수있도록 만들것입니다. ‘조회수’도 한번만 카운트 되도록 만들거나 또는 중복조회 상관없이 계속 카운트를 할것입니다.
오라클과 같은 RDB였다면 별도의 테이블에 게시물ID, 사용자ID를 추가하여 JOIN 함으로써 조회/좋아요의 유무를 판단하겠지만 NoSQL에서는 어떻게 할까요? MongoDB의 경우는 JOIN이 가능하긴 하지만 절대 정답이 아닙니다. 아래 예제에선 NeDB를 기준으로 하지만 워낙 직관적인 소스라 MongoDB를 사용하는 분도 보는데 문제 없으실겁니다.

좋아요 스키마 설계

좋아요가 있는 게시물이 아래와 같은 스키마를 가진다고 가정합니다.

  1. {
  2. _id : '029a83bae8re9',
  3. title : '제목입니다.',
  4. body : '본문입니다.',
  5. likeCount : 3
  6. }

위 스키마를 본다면 현재 몇명이 좋아요 했는지는 알수 있습니다. 하지만 누가 좋아요를 했는지는 알수 없기 때문에 좋아요 했던 사람이 더더 좋아요를 할수 있겠죠? 따라서 스키마에는 좋아요를 한 사용자를 포함해야 합니다. 아래 스키마는 ‘좋아요’를 한 사용자가 user1, user2, user4 이라는것을 나타냅니다.

  1. {
  2. _id : '029a83bae8re9',
  3. title : '제목입니다.',
  4. body : '본문입니다.',
  5. likeCount : 3,
  6. like_users : {
  7. user1 : true,
  8. user2 : true,
  9. user3 : false,
  10. user4 : true
  11. }
  12. }

주의해야 할것은 like_users가 배열이 아니라는 것입니다. like_users를 배열로 구성할 경우 본인의 아이디가 있는지 loop를 돌면서 체크해야 하기 때문에 속도가 느려집니다. true 대신 좋아요를 했던 시간을 저장할수도 있지만 크게 중요하진 않습니다.

이제 게시물을 조회할때 like_users에 본인 아이디로 true값이 있으면 ‘좋아요’를 한 상태인것을 표시할수 있습니다.

자, 이제부터 시작입니다. 그럼 만약 user5가 좋아요를 누른다면 likeCount의 값을 4로 바꾸고 like_users에 값이 추가되어야 합니다. RDB에 익숙한 분은 먼저 SELECT를 한번 해서 값을 가져오고 +1된 값과 user5로 UPDATE 하는것으로 생각하실수 있습니다. 하지만 NoSQL에선 그냥 UPDATE 하면 됩니다.

  1. let tmp = `like_users.${user}`;
  2. let obj1 = {
  3. _id : doc._id //게시물 아이디
  4. };
  5. let obj2 = {};
  6. obj1[tmp] = {$ne : true}; //like_users.user5 가 true가 아닐 경우
  7. obj2[tmp] = true;
  8. db.update(obj1, {
  9. $inc: { likeCount: 1} ,
  10. $set: obj2
  11. }, {}, function (err, numReplaced) { // Callback is optional
  12. console.log(err, numReplaced);
  13. });

실제로 검색 조건이 되는 객체는 아래와 같은 값이 될것입니다.

  1. {
  2. _id : '029a83bae8re9',
  3. like_users : {
  4. user5 : { $ne : true } //좋아요를 상태가 아닐경우
  5. }
  6. }

이미 좋아요가 된 상태라면 검색조건에서 매칭이 안되기 때문에 업데이트를 하지 않습니다. 업데이트를 해야할 경우엔 likeCount 기존값이 무엇이든 상관없이 1 증가하면서 { user5 : true } 를 세팅하게 됩니다. SELECT-UPDATE의 과정없이 단 한번의 실행으로 모두 처리할수가 있습니다. MongoDB도 $inc, $set과 같은 기능을 제공하므로 동일한 구현이 가능합니다.

방문자 카운트 설계

처음엔 조회수를 언급했지만 좋아요와 거의 동일한 구조이므로, 조금 다른 방문자 카운트를 설계해보겠습니다. 물론 중복 카운트는 허용하지 않죠. 매일 매일의 방문자 카운트를 위한 스키마는 아래와 같습니다.

  1. {
  2. _id:"rm2ODlGRBNOSqnWu",
  3. date:"20200317",
  4. count:2,
  5. view_user:{
  6. user1:true,
  7. user2:true
  8. }
  9. }

새로운 사용자가 방문했을때 count 값을 증가시키고 view_user 에 추가하는 것은 좋아요와 동일합니다. 그런데 날짜가 바뀌고 처음 방문자가 생길때 해당 날짜로 된 데이터가 있을까요?
좋아요는 게시물 데이터가 존재하는 상태에서 발생하지만, 방문자수는 그렇지 않습니다. 첫방문시엔 해당날짜로 만들어진 데이터가 없죠. 따라서 날짜가 바뀌면 자동으로 기본데이터를 insert를 하는 스케쥴러를 만들거나, 방문자수를 증가시키기 전에 무조건 insert를 하는 로직을 생각할수도 있습니다. date를 인덱스키로 지정하고 중복 insert할때 발생하는 오류는 무시하면 되니까요. 하지만 그럴 필요는 없습니다.

  1. let tmp = `view_user.${user}`;
  2. let obj1 = {
  3. date : '20200317',
  4. };
  5. obj1[tmp] = {$ne : true};
  6. let obj2 = {};
  7. obj2[tmp] = true;
  8. db.update(obj1, {
  9. $inc : { count : 1 },
  10. $set : obj2
  11. }, {
  12. upsert: true
  13. }, function(err, replaced) {
  14. console.log(err, replaced);
  15. });

설명이 필요없을 정도로 거의 모든 로직이 좋아요와 동일하죠. 딱 한가지 다른점은 upsert 입니다. RDB에선 SELECT 후 기존 데이터가 없으면 INSERT, 있으면 UPDATE를 하는 형태로 만들어야 하지만 NoSQL에선 upsert 하나로 해결할수 있습니다. 첫 방문자의 경우 INSERT가 되고 그 다음 방문자부터는 UPDATE를 하게 됩니다. upsert 기능은 보통 별도 함수나 옵션으로 제공되므로 사용하시는 NoSQL의 문서를 찾아보시면 되겠습니다.