Django의 Restframework로 학식봇 API서버를 만들었었는데,
코드를 작성한지 상당히 오래 지나서 다시 한번 기억을 되살릴겸 작성한다.
보통 장고로 작업할때는 Dependency 문제를 해결하기 위해 가상환경으로 진입하여 작업한다.
이 포스팅에서는 가상환경을 위해 pipenv를 사용한다. ( 설치 방법은 http://hides.kr/913 참고 )
다음으로 django와 djangorestframework를 설치한다.
pipenv shell
pipenv install django
pipenv install djangorestframework
다음으로 프로젝트와 앱을 생성해준다.
우리는 메뉴에 관한 정보와 익명게시판을 위한 정보가 필요하기 때문에 menu, board두개의 앱을 생성할 것이다.
django-admin startproject project_cnuapi
django-admin startapp menu
django-admin startapp board
이제 settings.py를 열고 아래의 라인을 추가하여 우리가 생성한 앱과 rest framework를 추가해준다.
INSTALLED_APPS = [
'rest_framework',
'menu',
'board',
]
데이터베이스는 MySQL을 사용할 것이다.
http://hides.kr/689 를 참고하여 설정해준다.
추가적으로 한국시간을 사용하기 위해 TIME_ZONE을 바꿔준다.
TIME_ZONE = 'Asia/Seoul'
이쯤에서 migrate한다.
python3 manage.py migrate
먼저 간단한 메뉴부터 작성한다.
장고의 모델부터 만들어줘야 한다.
menu/models.py를 연다.
from django.db import models
class Menu(models.Model):
place = models.CharField(max_length=20, primary_key=True)
mon = models.CharField(max_length=3000)
tue = models.CharField(max_length=3000)
wed = models.CharField(max_length=3000)
thu = models.CharField(max_length=3000)
fri = models.CharField(max_length=3000)
sat = models.CharField(max_length=3000)
sun = models.CharField(max_length=3000)
place에 Primary key를 줬다.
이제 시리얼라이저를 작성해줘야 한다.
정보를 직렬화해주는 파일인데 쉽게 생각해서 유저에게 어떠한 정보를 보여줄 지 정한다고 생각하면 된다.
menu/serializers.py 파일을 생성한다.
from rest_framework import serializers
from . import models
class MenuSerializer(serializers.ModelSerializer):
class Meta:
model = models.Menu
fields = '__all__'
Rest framework의 serializers를 사용하기 위해 임포트 해줬고 우리가 작성한 모델을 이용하기 위해 이또한 임포트해줬다.
그리고 메뉴에 관한 시리얼라이즈를 생성해준다.
사용할 모델은 이전에 작성한 Menu이며 fields를 통해 특정 컬럼들을 출력해줄 수 있는데 우리는 모든 컬럼을
출력해줄 것이므로 __all__ 로 써줬다.
이제 menu/views.py를 열고 아래의 라인을 추가한다.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from . import models, serializers
class Menu(APIView):
def get(self, request, format=None):
all_menu = models.Menu.objects.all()
serializer = serializers.MenuSerializer(all_menu, many=True)
return Response(status=status.HTTP_200_OK, data=serializer)
1 : 장고의 기본적인 뷰가 아닌 rest framework의 view를 추가
2 : Response값을 뱉어주기 위해 추가
3 : 상태 코드를 쉽게 주기 위해 추가
4 : 정의한 models와 serializers를 사용하기 위해 추가
임포트한 내용은 위와 같이 설명할 수 있다.
APIView를 상속받아서 사용하면 get/put/delete/post 등의 메소드를 Override하여 사용할 수 있다.
즉 이미 지정된 메소드를 우리가 수정할 수 있다는 말이다.
따라서 get으로 요청이 들어온다면 모든 메뉴를 불러오고 시리얼라이즈에 연결해준 다음 Response로 뿌려준다.
(참고로 format=None으로 주면 json타입으로 주는것을 의미/many=True는 여러개를 리턴할 때 사용)
마지막으로 라우팅만 해주면 된다.
아까 프로젝트의 이름을 project_cnuapi로 지정했었다.
project_cnuapi/urls.py를 아래와 같이 수정한다.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('menu/', include('menu.urls'))
]
사용자가 menu로 들어오면 menu앱의 urls로 연결시키는 라인이다.
(참고로 장고 2.0부터는 path를 사용하여 라우팅해야 한다)
다음으로 menu/urls.py 파일을 생성하고 아래와 같이 적어준다.
from django.conf.urls import url
from django.urls import path
from . import views
urlpatterns = [
path('', views.Menu.as_view(), name='menu')
]
프로젝트에서 이미 라우팅을 /menu로 받아오므로 ''로 지정해주면 /menu로 들어왔을 때의 작업을 정의해줄 수 있다.
참고로 우리는 view를 함수가 아닌 Class형태로 작성했기 때문에 .as_view()를 통해 변환시켜줘야 한다.
모두 저장하고 브라우저로 /menu를 들어가보면 아래와 같이 정상적으로 출력된다.
이제 익명 게시판을 만들어본다.
board/models.py에 아래의 내용을 추가한다.
from django.db import models
class Article(models.Model):
article_no = models.AutoField(primary_key=True)
content = models.CharField(max_length=500, null=False)
password = models.CharField(max_length=20, null=False, default='1234')
date = models.DateTimeField(auto_now_add=True)
class Comment(models.Model):
article_no = models.ForeignKey('Article', on_delete=models.CASCADE)
content = models.CharField(max_length=50, null=False, default='')
password = models.CharField(max_length=20, null=False, default='1234')
date = models.DateTimeField(auto_now_add=True)
글이 있다면 당연히 댓글도 있기 때문에 댓글도 정의해줬다.
Comment의 article_no를 ForeignKey로 주고 Article을 바라보게 해줬다.
물론, 글이 삭제되면 댓글도 삭제되야 하므로 on_delete=models.CASCADE 속성도 줬다.
이제 board/serializers.py를 생성하고 아래의 내용을 넣는다.
from rest_framework import serializers
from . import models
class ArticleSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = models.Article
fields = '__all__'
class CommentSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = models.Comment
fields = '__all__'
password는 일반 사용자가 확인할 수 없도록 만들어야 하기 때문에 write_only 속성을 True로 줬다.
위처럼 지정해주면 fields를 __all__로 줘도 API를 확인했을 때 password 컬럼이 출력되지 않는다.
이제 board/views.py를 연다.
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from . import models, serializers
class Article(APIView):
def get(self, request, format=None):
all_article = models.Article.objects.all()
serializer = serializers.ArticleSerializer(all_article, many=True)
return Response(status=status.HTTP_200_OK, data=serializer.data)
class Comment(APIView):
def get(self, request, format=None):
all_comment = models.Comment.objects.all()
serializer = serializers.CommentSerializer(all_comment, many=True)
return Response(status=status.HTTP_200_OK, data=serializer.data)
마찬가지로 Article과 Comment에 관련된 내용을 넣었다.
마지막으로 연결해주기 위해 프로젝트/urls.py에 아래의 내용을 추가
path('board/', include('board.urls')),
그리고 board/urls.py를 생성하고 아래의 내용을 넣어서 라우팅을 해준다.
from django.conf.urls import url
from django.urls import path
from . import views
urlpatterns = [
path('article/', views.Article.as_view(), name='article'),
path('comment/', views.Comment.as_view(), name='comment')
]
이제 /board/article/ 로 접속하면,
위처럼 성공적으로 내용이 출력된다.
이제 여기서 단순 조회가 아닌 삽입, 삭제, 수정 기능을 넣어줘야 한다.
아까 APIView를 사용하면 get/post/put/delete를 오버라이드 할 수 있다고 했다.
views.py 파일을 수정하여 get과 마찬가지로 관련 내용을 넣어주면 된다.
class Article(APIView):
def get(self, request, format=None):
all_article = models.Article.objects.all()
serializer = serializers.ArticleSerializer(all_article, many=True)
return Response(status=status.HTTP_200_OK, data=serializer.data)
def post(self, request, format=None):
serializer = serializers.ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(status=status.HTTP_201_CREATED, data=serializer.data)
else:
return Response(status=status.HTTP_400_BAD_REQUEST, data=serializer.errors)
아까는 단순 조회하는 GET메소드에 대해서만 정의해줬지만 이제 값을 삽입하는 POST메소드에서도 정의를 해줬다.
먼저 serializers를 이용하여 사용자에게 입력받은 값을 넣어주고, (request.data를 통해 HTTP Body값을 받을 수 있다)
해당 값이 유효한지 is_valid()를 통해 검사해준다.
유효한 형태의 값이라면 .save()를 통해 저장한 후 201을 리턴해준다.
주의할 점은 모든 작업의 끝부분에 Response를 리턴해주지 않으면 오류가 발생한다.
class Comment(APIView):
def get(self, request, format=None):
all_comment = models.Comment.objects.all()
serializer = serializers.CommentSerializer(all_comment, many=True)
return Response(status=status.HTTP_200_OK, data=serializer.data)
def post(self, request, format=None):
serializer = serializers.CommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(status=status.HTTP_201_CREATED, data=serializer.data)
else:
return Response(status=status.HTTP_400_BAD_REQUEST, data=serializer.errors)
코멘트도 같다. 조회는 모든 코멘트를 불러온 후 리턴해주고
값의 삽입도 Article과 마찬가지로 작업해준다.
또한 여기서 한가지 더 작업을 해줘야한다.
모든 게시글에는 댓글을 작성할 수 있기 때문에 댓글의 내용과 댓글의 갯수도 같이 표현해줘야 한다.
하지만 이 작업을 현재의 Article에 작성하면 단순히 제목을 보여주는 형태의 게시판에서는
불필요한 내용까지(댓글, 댓글 개수) 불러오기 때문에 리소스를 낭비하게 된다.
따라서 ArticleDetail이라는 새로운 뷰를 만들기로 한다.
먼저 board/urls.py에 아래의 내용을 추가한다.
from django.conf.urls import url
from django.urls import path
from . import views
urlpatterns = [
path('article/', views.Article.as_view(), name='article'),
path('comment/', views.Comment.as_view(), name='comment'),
path('article/<int:article_no>', views.ArticleDetail.as_view(), name='article_detail'),
]
위에서 장고 2.0부터는 path를 이용한다고 말했다.
path를 이용하면 url을 이용한 라우팅보다 정규식 작업등을 상당히 편리하게 해줄 수 있다.
위처럼 article/<int:article_no> 라고 적어준다면, 정수 타입의 article_no라는 변수 값을 받는다는 것이다.
따라서 views.py에서 메소드를 오버라이드할 때
def get(self, request, article_no, fotmat=None) 형태로 article_no라는 변수를 이용할 수 있다.
다음으로 models.py를 조금 더 수정해줘야 한다.
class Comment(models.Model):
article_no = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')
content = models.CharField(max_length=50, null=False, default='')
password = models.CharField(max_length=20, null=False, default='1234')
date = models.DateTimeField(auto_now_add=True)
크게 변한점은 없지만 article_no의 related_name에 comments라는 값을 줬다.
ariticle_no은 현재 Article의 값을 참조하고 있는 외래키이다.
따라서 Article에서 Comment의 값을 불러올 때 related_name으로 준 comments를 통해 관련 값들을 쉽게 불러올 수 있다.
이제 serializers.py를 연다.
class CommentDetailSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comment
fields = (
'content',
'date'
)
먼저 디테일한 게시글의 정보를 표현해주는 시리얼라이저를 작성하기전에 해당 시리얼라이저에서
참조할 코멘트 관련 시리얼라이저를 하나 더 추가한다.
이유는 이전 코멘트 시리얼라이즈의 필드를 보면 fields = '__all__' 형태로 되어있다.
우리가 게시글을 통해 확인하고 싶은 코멘트 모델의 컬럼은 content와 date 단 두개인데
위에서 모든 컬럼을 출력하도록 설정해놨기 때문이다.
class ArticleDetailSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True)
comments = CommentDetailSerializer(many=True, read_only=True)
commentCount = serializers.IntegerField(source='comments.count', read_only=True)
class Meta:
model = models.Article
fields = (
'article_no',
'content',
'password',
'date',
'comments',
'commentCount'
)
다음으로 위처럼 ArticleDetailSerializer를 작성한다.
아까 related_name을 comments로 줬기 때문에 변수 이름을 해당 값으로 지정해주고
방금 위에서 만든 CommentDetailSerializer를 통해 값을 가져온다.
또한 게시글을 위한 시리얼라이저에서 코멘트에 관한 정보를 건들면 안되기 때문에 read_only=True 속성을 줬다.
댓글의 수를 표현해주기 위해 commentCount라는 값을 하나 더 만들어줬는데, source를 이름.count로 주면 해당 값의 갯수를 가져올 수 있다.
위처럼 선언해주고 fields를 통해 표현해주고 싶은 모든 값들을 넣는다.
마지막으로 이 모든 값들을 표현해줄 뷰를 작성해야 한다.
views.py 파일에 아래의 내용을 추가해준다.
class ArticleDetail(APIView):
def get(self, request, article_no, format=None):
try:
article = models.Article.objects.get(article_no=article_no)
serializer = serializers.ArticleDetailSerializer(article)
return Response(status=status.HTTP_200_OK, data=serializer.data)
except models.Article.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
def put(self, request, article_no, format=None):
try:
article = models.Article.objects.get(article_no=article_no)
if article.password != request.data['password']:
return Response(status=status.HTTP_304_NOT_MODIFIED)
else:
serializer = serializers.ArticleSerializer(article, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(status=status.HTTP_201_CREATED, data=serializer.data)
else:
return Response(status=status.HTTP_304_NOT_MODIFIED, data=serializer.errors)
except models.Article.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
except:
return Response(status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, article_no, format=None):
try:
article = models.Article.objects.get(article_no=article_no)
if article.password != request.data['password']:
return Response(status=status.HTTP_304_NOT_MODIFIED)
else:
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except models.Article.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
except KeyError:
return Response(status=status.HTTP_400_BAD_REQUEST)
별거 없다. 이전에 설명했던 것과 루틴은 동일하고 수정, 삭제에 관한 코드만 여기에 정의해줬다.
여기서 하나 말하고 싶은 점은 이전에는 DELETE 메소드도 POST나 PUT처럼 Body값이 있는 줄 알았다.
그런데 제대로 확인해보니 바디값이 없었다.
신기한건 바디값으로 주고 그 값에서 추출해서 작업을 해도 동작은 된다는 점이다.
이 부분에 관해 주위 사람들에게 물어봤는데 실 서비스에서는 삭제하는 기능을 만들지 않는다고 한다.
이유인즉슨 굳이 데이터를 지울 필요가 없으며 데이터들은 약관에 명시된 기간동안 보존하고
그 이후에 일괄적으로 삭제한다고 한다. 또한 보안적인 요소에서도 좋지 않다.
그렇기 때문에 회사에서는 실제로 해당 데이터베이스 유저에 Delete 권한 조차도 주지 않는다고 한다.
튼, 모두 저장하고 테스트를 하기 위해 1번 게시글에 2개의 코멘트를 추가한 후 /board/article/1 로 들어가보면,
위처럼 정상적으로 표현된다.