게시판 Restful API

hs·2021년 11월 3일
2

Aimmo 개발 과제로 게시판 Restful API를 작성해 보았다.

필수 조건은 아래와 같다.

  • 게시물 카테고리 및 필터 적용
  • 게시글 검색 기능 추가
  • 댓글 및 대댓글 기능 추가
  • 게시물 조회수 구현
  • Unit Test
  • RestFul API
  • mongodb

팀원

NameGitBlog
김정수https://github.com/holliblelinghttps://velog.io/@hollibleling
윤현묵https://github.com/fall031-mukhttps://velog.io/@fall031
최현수https://github.com/filolahttps://velog.io/@chs_0303

개발기간

21.11.1 ~ 21.11.3

기술스택

  • python, django
  • mongoDB, djongo
  • AWS EC2
  • Slack, Git

ENDPOINT

MethodendpointRequest HeaderRequest BodyRemark
POST/user/signupname, nickname, email, password회원가입
POST/user/loginemail, password로그인
POST/post/listAuthorizationtitle, body, category게시물 작성
GET/post/detail/<int:post_id>Authorization, cookie게시물 조회 및 댓글 조회
DELETE/post/detail/<int:post_id>Authorization게시물 삭제
PUT/post/detail/<int:post_id>Authorizationtitle, body, category게시물 수정
GET/post/list?page=게시물 목록 조회
GET/post/list?category=게시물 카테고리 필터
POST/post/<int:post_id>/commentscontent, parent_comment_id댓글/대댓글 작성
GET/post/<int:post_id>/comments?limit=&offset=&comment_id=대댓글 조회
PATCH/post/<int:post_id>/commentscomment_id댓글/대댓글 수정
DELETE/post/<int:post_id>/comments?comment_id=댓글/대댓글 삭제

개발

초기 셋팅 및 기본 게시판 CRUD구현은 기존에 pre-onboarding 지원과제로 제출했던 git을 클론해와서 진행하였다.

필수 요청 사항 중, 기술적으로 3가지로 나눠 각자 하나씩 맡아 진행하였다.
그 중 내가 맡은 부분은 게시물 카테고리 적용게시물 조회수 구현 이였다.

mongodb

django와 mongodb를 연결하기 위해 djongo 모듈을 사용하여 진행하였다.
djongo는 django의 ORM을 지원해주는 모델이다.
연결을 위해 아래의 강의를 보며 학습했다.

Using Django with MongoDB | MongoDB + Django CRUD API

또한 mongodb를 좀 더 쉽게 사용하기 위해 GUI환경인 MongoDB Compass를 사용하였다.

MongoDB Compass

🏷 게시물 카테고리

카테고리는 RDBMS로 보았을 때 게시물과 1:M 관계로 모델링을 추가하고 카테고리가 출력 되어져야 하는 부분에 각각 데이터를 받아와주었다.

# models.py
class Post(TimeStamp):
   ...
   category = models.ForeignKey("Category", on_delete=models.CASCADE)
   ...
   
 class Category(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        db_table="categories"

또한 카테고리를 통한 필터를 가능하게 만들어 필터에 대한 정보가 주어졌을 때 그에 맞는 카테고리를 가지고 있는 게시글들만 출력하게 구현하였다.

# view.py
class ListView(View):
    def get(self, request):
	if category:
    	    category_id = Category.objects.get(name=category).id
            posts = Post.objects.filter(category_id=category_id).order_by('-id')
        else:
            posts = Post.objects.all().order_by('-id')

또한 전체 게시글을 출력할 때 pagination을 통해 출력되는 게시글의 개수를 정해줬었는데 이 또한 동일하게 구현하였다.

# view.py
class ListView(View):
    def get(self, request):
        try:
            page       = request.GET.get("page")
            page       = int(page or 1)
            page_size  = 10
            limit      = page_size * page 
            offset     = limit - page_size
        ...
        result = [{
                "count" : len(posts)-(offset+i),
                "title" : post.title,
                "hit" : post.hit,
                "body" : post.body,
                "nickname" : post.user.nickname
            } for i,post in enumerate(posts [offset:limit])]

데이터를 가져올 때 게시글 앞에 번호를 매겨주고 싶었는데 데이터를 hard delete로 지울 경우 id값이 밀리는 현상이 생겨 고민을 하다가 전체 게시글의 수에서 offset과 반복되는 인덱스만큼의 값을 빼줘 중간에 데이터가 비어있다해도 번호가 이어지게 만들었다.

🏷 게시물 조회수 구현

기존에 구현한 조회수 같은 경우 get으로 게시글이 불러오질 때마다 hit을 1씩 추가시켜 저장을 하여 구현했었다.

post.hit += 1
post.save()

하지만 위와 같이 구현할 경우 한 사용자가 중복으로 조회수를 올릴 수 있는 문제 점이 존재하였다.
이에 관하여 찾아보니 3가지 방법이 있었다.

  1. session
  2. ip
  3. cookie

세 가지 방식 중 가장 많이 사용되는 방식이 cookie 를 통하여 조회수를 관리하는 방법이였다. 기존 방식에는 cookie 만료 시간을 주어 지정한 시간이 지나면 자동으로 쿠키가 삭제되는 방식이였다. 하지만 이번 과제에는 "중복된 user가 조회수를 올릴 수 없게 구현" 이라 서술되어 있어 만료시간을 주지 않았다.

class PostView(View):
    @transaction.atomic
    @login_decorator
    def get(self, request,post_id):
        try:
            post = Post.objects.get(id=post_id)
            
            ...
            
            response = JsonResponse({"Result" : result, "Comment" : Result_comment}, status=200)
            
            if request.COOKIES.get('hit'):
                cookies = request.COOKIES.get('hit')
                cookies_list = cookies.split('|')
                if str(post.id) not in cookies_list:
                    
                    post.hit += 1
                    post.save()

                    result["hit"] = post.hit
                    response = JsonResponse({"Result" : result, "Comment" : Result_comment}, status=200)
                    
                    response.set_cookie('hit', cookies+f'|{post.id}', expires=None)
            else:
                post.hit += 1
                post.save()
  
                result["hit"] = post.hit
                response = JsonResponse({"Result" : result, "Comment" : Result_comment}, status=200)
            
                response.set_cookie('hit', post.id, expires=None)
            return response

        except Post.DoesNotExist:
            return JsonResponse({"message" : "POSTS_NOT_FOUND"}, status=404)
  1. 첫번째 조건문을 통해 cookie에 hit이 존재하는지를 확인한다. 만약 없을 경우 바로 조회수를 '1' 추가 해주고 return 한다.
  2. 존재할 경우 쿠키를 '|'를 기준으로 나누어준다.(cookie hit=1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1)
  3. 게시물의 id가 cookie_list에 존재하지 않을 경우 '1' 추가해준다.

🏷 아쉬운 점

  1. IP를 통해 접속기록을 저장하여 하는 방식도 구현해보고 싶었는데 어렵고 이를 습득하여 적용하기엔 시간이 부족하였다.
  2. mongodb 즉, NoSQL을 사용하는데 이를 재대로 활용하기에는 시간이 많이 모자랐다. 하지만 기존에 사용하던 RDBMS만 사용해서 구현을 하다 NoSQL에 관해서 보았는데 굉장히 흥미로웠다. 어떤 점에서 보면 좀 더 가용성이 좋아보이기도 하고 재밌어보였다.
  3. 게시물 상세 get 함수 상단에 "@transaction.atomic"이 있는데 이는 쿠키에 저장하기 위해 트랜젝션을 써주었다. 하지만 지금 생각해보니 굳이 함수 전체를 원자성으로 묶을 필요가 있었을까 싶다. 쿠키에 저장하는 부분만 with을 통해 원자성을 부여했다면 더 좋은 코드가 되었을 것 같다.
profile
무엇이든 끝까지 보람차게

0개의 댓글