3주차 목요일, 14일차 Today I Learned
장고(Django) 프레임웍을 사용한 API 서버 만들기 (4)
: 장고 REST 프레임웍 - 사용자 (Users)와 인증 (Authentication), POSTMAN
✏️ 학습 내용
1. User 추가하기
owner 필드를 추가한다. owner 필드는 auth.User 모델의 PK를 참조하고 있으며, null 값을 가지도록 지정되어 있다. 그리고 User 객체가 삭제될 때는 연결된 Question 객체도 함께 삭제된다.
ForeignKey는 다른 모델을 참조하기 위한 필드를 의미한다. 모델간의 관계가 어떻게 형성되어 있는지를 파악할 수 있는데, Question 모델은 auth.User 모델을 FK로 가지고 있다. 각각 모델에서 객체를 생성하고, QuerySet을 가져오기 위해서는 auth.User_객체.Question_set.all()을 입력하면 된다.
# polls/models.py
class Question(models.Model):
question_text = models.CharField(max_length=200, verbose_name='질문')
pub_date = models.DateTimeField(auto_now_add=True, verbose_name='생성일')
owner = models.ForeignKey('auth.User', related_name='questions', on_delete=models.CASCADE, null=True)
@admin.display(boolean=True, description='최근생성(하루기준)')
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
def __str__(self):
return f'제목: {self.question_text}, 날짜: {self.pub_date}'
...
# Django Shell
>>> from django.contrib.auth.models import User
>>> User
<class 'django.contrib.auth.models.User'>
>>> User._meta.get_fields()
>>> User.objects.all()
<QuerySet [<User: admin>]>
>>> from polls.models import *
>>> user = User.objects.first()
>>> user.questions.all()
>>> print(user.questions.all().query)
SELECT "polls_question"."id", "polls_question"."question_text", "polls_question"."pub_date", "polls_question"."owner_id" FROM "polls_question" WHERE "polls_question"."owner_id" = 1
2. User 관리하기
User 모델에 대해 시리얼라이즈를 만들고, 뷰를 구현하겠다. User 모델 인스턴스를 시리얼라이즈 하기 위해 UserSerializer 클래스를 생성했다. User 모델의 PK 값으로 연결된 모든 Question 모델의 오브젝트들을 가져와 questions에 할당했다.
# polls_api/serializers.py
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'questions']
# polls_api/views.py
from django.contrib.auth.models import User
from polls_api.serializers import UserSerializer
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
# polls_api/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('question/', QuestionList.as_view(), name='question-list'),
path('question/<int:pk>/', QuestionDetail.as_view()),
path('users/', UserList.as_view(),name='user-list'),
path('users/<int:pk>/', UserDetail.as_view()),
]
3. Form을 이용하여 User 생성하기
User를 생성하는 첫 번째 방법 : 장고를 이용
회원가입 기능을 구현하기 위해 SignupView와 signup.html 템플릿 코드를 작성했다. UserCreationFrom은 '유저이름'과 '패스워드', '패스워드 확인' 필드를 제공하며 회원가입 폼을 자동으로 생성한다. 회원가입이 성공적으로 완료된 이후에는 `user-list` 뷰로 리디렉션되어, 새로 생성된 유저가 리스트에 포함되어 있는지 확인할 수 있다. {{ form.as_p }}은 UserCreationForm을 렌더링한 결과를 출력한다.
# polls/views.py
from django.views import generic
from django.urls import reverse_lazy
from django.contrib.auth.forms import UserCreationForm
class SignupView(generic.CreateView):
form_class = UserCreationForm
success_url = reverse_lazy('user-list')
template_name = 'registration/signup.html'
# polls/templates/registration/signup.html
<h2>회원가입</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">가입하기</button>
</form>
# polls/urls.py
from django.urls import path
from . import views
from .views import *
app_name = 'polls'
urlpatterns = [
path('',views.index, name='index'),
path('<int:question_id>/', views.detail, name='detail'),
path('<int:question_id>/vote/', views.vote, name='vote'),
path('<int:question_id>/result/', views.result, name='result'),
path('signup/', SignupView.as_view(), )
]
# Django Shell
>>> from django.urls import reverse_lazy
>>> reverse_lazy('user-list')
'/rest/users/'
4. Serializer 이용하여 user 생성하기
User를 생성하는 첫 번째 방법 : 장고 REST를 이용
별도로 구현해주어야 하는 코드가 있기 때문에 UserSerializer를 사용하지 않고 따로 RegisterSerializer를 만들었다.
validate() 메서드는 패스워드의 유효성을 검증한다. 서버에서 응답으로 전송하는 JSON 데이터에 password, password2 필드의 값은 포함되지 않는다. attrs는 딕셔너리로 password와 password2라는 키를 사용하여 입력받은 패스워드와 패스워드 확인용 필드의 값을 가져온다. 그리고 두 개의 값이 일치하지 않으면 validationError가 발생한다.
create() 메서드는 유효성 검사를 마친 데이터를 기반으로 User 객체를 생성하고 저장한다. set_password() 메서드를 사용해서 User 모델 인스턴스의 패스워드를 설정한다.
# polls_api/serializers.py
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
password2 = serializers.CharField(write_only=True, required=True)
def validate(self, attrs):
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({"password": "두 패스워드가 일치하지 않습니다."})
return attrs
def create(self, validated_data):
user = User.objects.create(username=validated_data['username'])
user.set_password(validated_data['password'])
user.save()
return user
class Meta:
model = User
fields = ['username', 'password','password2']
# polls_api/views.py
from polls_api.serializers import RegisterSerializer
class RegisterUser(generics.CreateAPIView):
serializer_class = RegisterSerializer
# polls_api/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('question/', QuestionList.as_view(), name='question-list'),
path('question/<int:pk>/', QuestionDetail.as_view()),
path('users/', UserList.as_view(),name='user-list'),
path('users/<int:pk>/', UserDetail.as_view()),
path('register/', RegisterUser.as_view()),
]
5. User 권한 관리
QuestionList와 QuestionDetail 뷰의 코드에서 User 권한과 관련된 내용들을 추가했다. 오직 로그인을 한 상태에서만 질문의 목록을 보거나 새로운 질문을 생성할 수 있고, 오직 질문을 작성한 사람만이 해당 질문의 상세 정보를 조회하거나 수정 및 삭제를 수행할 수 있는 권한을 가질 수 있다. 새로운 질문이 생성될 때 owner 필드에 현재 요청을 보낸 사용자들이 자동으로 할당된다.
# polls_api/urls.py
from django.urls import path,include
from .views import *
urlpatterns = [
path('question/', QuestionList.as_view(), name='question-list'),
path('question/<int:pk>/', QuestionDetail.as_view()),
path('users/', UserList.as_view(),name='user-list'),
path('users/<int:pk>/', UserDetail.as_view()),
path('register/', RegisterUser.as_view()),
path('api-auth/', include('rest_framework.urls'))
]
# mysite/settings.py
from django.urls import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('question-list')
LOGOUT_REDIRECT_URL = reverse_lazy('question-list')
polls_api/serializers.py
class QuestionSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Question
fields = ['id', 'question_text', 'pub_date', 'owner']
# polls_api/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
# polls_api/views.py
from rest_framework import generics,permissions
from .permissions import IsOwnerOrReadOnly
class QuestionList(generics.ListCreateAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
6. Perform create()
QuestionList 뷰 안에서 작성된 perform_create()에 의해서 자동으로 로그인된 owner가 기록되었다. QuestionList 뷰는 ListCreateAPIView를 상속 받았는데, 그 안에는 post() 메서드가 정의되어 있다. 이 post() 메서드 안에는 또 다른 곳에서 상속 받은 create()가 있고, 그 안에 또 perform_create()가 있다. 상속을 받은 것인데, QuestionList 뷰의 perform_create()는 오버라이딩 된 것이다.
상속 구조 : QuestionList < generics.ListCreateAPIView < mixins.CreateModelMixin
QuestionList 안에 정의된 메소드들 :
1) def get(self, request, *args, **kwargs) - 출처 : generics.ListCreateAPIView
2) def create(self, request, *args, **kwargs) - 출처 : mixins.CreateModelMixin
3) def perform_create(self, serializer) : mixins.CreateModelMixin 내용이 지워지고, QuestionList에서 동작
# Django Shell
>>> from polls_api.serializers import QuestionSerializer
>>> question_serializer = QuestionSerializer(data={"question_text": "some text","owner" :"someone"})
>>> question_serializer.is_valid()
True
>>> question_serializer.validated_data
OrderedDict([('question_text', 'some text')])
>>> question = question_serializer.save(id=10000)
>>> question.id
10000
>>> question.question_text
'some text'
7. POSTMAN
POSTMAN은 RESTful API 테스트를 위한 플랫폼으로, 다양한 HTTP 요청을 보내고 응답 결과를 쉽게 확인할 수 있도록 도와준다. 또한, API 요청과 응답 결과를 저장하고 공유할 수 있는 기능도 제공한다.
Postman 공식 홈페이지 https://www.postman.com/ 에서 다운로드 가능하다.
맥의 경우, 화면 좌측 상단 애플 로고 > 이 Mac에 관하여 (About This Mac)에서 칩의 유형을 확인한 후에 설치해야 한다.
POSTMAN에 새로운 Workspace를 생성하여 진행하면 된다. (새로운 리퀘스트 보낼 수 있는 창 생성)
PUT으로 바꾸고, url을 입력한다. 그리고 Headers에서 KEY에 `Content-Type`, Value에 `application/json`을 입력한다. Body에는 row을 선택하여 `{'question_text':'POSTMAN에서 보는 제목'}`을 입력한 후에 [Send] 버튼을 누르면 아래에 내용이 뜬다. 로그아웃 상태에서는 할 수 없다는 것을 알 수 있다.
로그인 된 상태에서 확인하기 위해서는 Headers에 Key를 `Cookie`, Value에는 브라우저 개발자 도구의 Application에 표시된 Storage > Cookies에서 sessionid에서 복사한 코드를 입력한 후에 [Send] 버튼을 눌러 가능하다. csrftoken도 마찬가지로 Key에 `X-CSRFToken`을 입력하여 추가하고, Key에 `Cookie`, Value에 `csrftoken=value값`을 추가로 입력해준다.
GET으로 url을 넣고 [Send] 하면 잘 작동되는 것을 알 수 있다.
💡 배운 점
Django REST Framework에서 User를 추가하고, 관리하고, 생성(Form 이용, Serializer 이용)하고, 권환 관리하는 방법에 대해 배웠다.
그리고 RESTful API 테스트를 할 수 있는 POSTMAN에 대해서도 배웠다.
📝 남아있는 의문과 개선점
배운 내용 실습해보기
Postman 직접 사용해보기
☁️ 소감
user에 관한 내용을 배웠다. 확실히 화면을 보면서 내용을 들으니 조금 더 이해하기는 쉬웠다. 내일까지 수업을 듣고 한 번 전체적으로 다시 실습해보는 시간을 가질 예정이다.