[데이터 엔지니어링 데브코스 2기] TIL-4주차-파트05 장고를 활용한 API서버 만들기(Users, Authentication)

이재호·2023년 11월 2일
0

1. User

장고에서는 특정 모델(테이블)에 대한 유저를 정의할 수가 있습니다.
코드와 함께 예시를 살펴 보면 다음과 같습니다.

  1. polls/models.py 파일을 다음과 같이 수정합니다
from django.db import models

from django.utils import timezone
import datetime
from django.contrib import admin

class Question(models.Model):
    # verbose_name 옵션에 필드의 칼럼명 입력
    question_text = models.CharField(max_length=200, verbose_name="질문") # 텍스트를 저장하는 필드
    pub_date = models.DateTimeField(auto_now_add=True, verbose_name="생성일") # 날짜를 저장하는 필드
    # 해당 질문의 owner를 정의.
    # owner가 삭제되면 Qeustion은 모두 delete(CASCADE)되며,
    # 필드가 비어 있어도(null) 괜찮다는 의미.
    # 유저의 Question을 불러올 때, realated_name인 'questions'로 가져옴.
    owner = models.ForeignKey('auth.User', related_name='questions', on_delete=models.CASCADE, null=True)

    # @admin.display 어노테이션으로 함수의 칼럼명 입력 
    @admin.display(boolean=True, description='최근생성(하루기준)')
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

    def __str__(self):
        return self.question_text    

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE) # 외래키 Question을 CASCADE로 참조한다.
    choice_text = models.CharField(max_length=200) # 텍스트를 저장하는 필드
    votes = models.IntegerField(default=0) # 숫자를 저장하는 필드
    def __str__(self):
        return self.choice_text
  1. 터미널에서 python manage.py makemigrations 명령어를 입력 후, python manage.py migrations 명령어를 입력합니다.

  2. Django Shell에서 여러 값들을 확인해 봅니다.

# 장고에서 제공하는 User 모델을 임포트합니다.
>>> from django.contrib.auth.models import User
>>> User
<class 'django.contrib.auth.models.User'>
# User 모델의 정보를 확인해 봅니다.
# id, password,last_login 등의 필드를 가지고 있습니다.
>>> User._meta.get_fields()
(<ManyToOneRel: admin.logentry>, <django.db.models.fields.AutoField: id>, <django.db.models.fields.CharField: password>, <django.db.models.fields.DateTimeField: last_login>, <django.db.models.fields.BooleanField: is_superuser>, <django.db.models.fields.CharField: username>, <django.db.models.fields.CharField: first_name>, <django.db.models.fields.CharField: last_name>, <django.db.models.fields.EmailField: email>, <django.db.models.fields.BooleanField: is_staff>, <django.db.models.fields.BooleanField: is_active>, <django.db.models.fields.DateTimeField: date_joined>, <django.db.models.fields.related.ManyToManyField: groups>, <django.db.models.fields.related.ManyToManyField: user_permissions>)
# 현재 저장된 유저 목록을 확인합니다.
>>> User.objects.all()
<QuerySet [<User: admin>, <User: admin2>]>


# 이제 저희가 정의한 모델과 함께 비교해 봅니다.
>>> from polls.models import *
>>> user = User.objects.first()
>>> user.questions.all()
<QuerySet []>
>>> print(user.questions.all())
<QuerySet []>
# 해당 쿼리문이 어떻게 되어있는지 확인해 봅니다.
# WHERE 절에서 owner_id가 1인 것으로 묶고 있는 것을 확인할 수가 있습니다.
>>> 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
# 해당 유저의 id가 1인 것을 확인할 수가 있습니다.
>>> user.id
1
>>> question = Question.objects.first()
# 해당 질문의 선택지 객체 목록을 확인합니다.
>>> question.choice_set.all()
<QuerySet [<Choice: 바다>, <Choice: 강>]>
# 해당 쿼리문을 확인해 봅니다.
# WHERE 절에서 choice의 question의 id가 1인 것들을 갖고 오는 것을 확인할 수 있습니다.
>>> print(question.choice_set.all().query)
SELECT "polls_choice"."id", "polls_choice"."question_id", "polls_choice"."choice_text", "polls_choice"."votes" FROM "polls_choice" 
WHERE "polls_choice"."question_id" = 1

위에서 보이는 것처럼, Question에 대해서 쿼리를 할 때는 owner_id를 기준으로 쿼리를 하고, Choice에 대해서 쿼리를 할 때는 question__id를 기준으로 쿼리를 하고 있다는 것을 알 수가 있습니다.

이유는 Question과 Choice 모델이 각각 User와 Question를 ForeignKey로 참조하고 있기 때문입니다.
즉,
{ 한 명의 User : 하나 이상의 Question }
{ 하나의 Question : 하나 이상의Choice } 로 볼 수 있습니다.




2. User 관리

User에 대한 Serializer와 View를 정의해 봅니다.
절차는 다음과 같습니다.

  1. polls_api/serializers.py 파일에 UserSerializer를 추가해 줍니다.
# Django의 REST 프레임워크에서 시리얼라이저 임포트.
from rest_framework import serializers
from polls.models import Question
# User 모델 임포트
from django.contrib.auth.models import User

# ModelSerializer를 상속받는 경우, 필드 및 메서드가 자동으로 지정됨.
class QuestionSerializer(serializers.ModelSerializer):
    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date']

# 추가된 UserSerializer
class UserSerializer(serializers.ModelSerializer):
    # User 모델의 Primary Key 값으로 연결된 모든 Question 모델의 오브젝트들을 가져옴
    questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'questions']
  1. polls_api/views.py 파일에 User에 대한 View를 추가해 줍니다.
from polls.models import Question
from polls_api.serializers import QuestionSerializer, UserSerializer
from rest_framework import generics
from django.contrib.auth.models import User

# generics 클래스의 ListCreateAPIView 상속
# get, post 기능이 구현되어 있음
class QuestionList(generics.ListCreateAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer

# generics 클래스의 RetrieveUpdateDestroyAPIView 상속
# get, put, delete 기능이 구현되어 있음.
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer

# User 목록 조회 뷰
class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

# 특정 User 상세 페이지 조회 뷰
class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
  1. 마지막으로 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(), name='question_detail'),
    # 추가된 경로들
    path('users/', UserList.as_view()),
    path('users/<int:pk>/', UserDetail.as_view()),   
]
  1. http://127.0.0.1:8000/rest/users/ URL에서 페이지 화면을 확인해 봅니다.

    유저들에 대한 리스트가 잘 나오는 것을 확인할 수가 있습니다.



3. Form을 사용하여 User 만들기

장고에서 제공하는 Form 기능을 이용하여 User를 만들 수 있습니다. 장고에서 제공하는 기능은 polls/ 에 구현하였습니다.
절차는 다음과 같습니다.

  1. 우선 polls_api/urls.py 파일에서 'user-list' name을 추가해 줍니다. 이는 뒤에서 나올 reverse_lazy가 해당 name으로 url을 만들기 위함입니다.
from django.urls import path
from .views import *

urlpatterns = [
    path('question/', QuestionList.as_view(), name='question-list'),
    path('question/<int:pk>/', QuestionDetail.as_view(), name='question_detail'),
    path('users/', UserList.as_view(), name='user-list'),
    path('question/<int:pk>/', QuestionDetail.as_view, name='question_detail'),
]
  1. polls/views.py 파일에 사용자 입력을 위한 View를 만들어 줍니다.
from django.http import HttpResponse, HttpResponseRedirect
from .models import *
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django.db.models import F
# generic 뷰 모델 임포트
from django.views import generic
# url을 생성하는 revese_lazy 메서드 임포트
from django.urls import reverse_lazy
# 유저생성폼 임포트
from django.contrib.auth.forms import UserCreationForm

def index(request):
	latest_question_list = Question.objects.order_by('-pub_date')[:5]
	context = {'questions' : latest_question_list}
	return render(request, 'polls/index.html', context)

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {'question': question, 'error_message': f"선택이 없습니다. id={request.POST['choice']}"})
    else:
        # F : DB에서 읽는다는 의미.
        selected_choice.votes = F('votes') + 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:result', args=(question.id,)))
    
def result(request, question_id):
    question = get_object_or_404(Question, id=question_id)
    return render(request, 'polls/result.html', {'question' : question})

# 회원가입 뷰 클래스 정의
class SignUpView(generic.CreateView):
    form_class = UserCreationForm
    # 회원 가입 성공 시, success_url로 이동(리디렉션)함.
    success_url = reverse_lazy('user-list')
    # 템플릿을 정의함
    template_name = 'registration/signup.html'
  1. polls/template/registration/signup.html 파일을 생성 후, 해당 템플릿 파일을 다음과 같이 작성합니다.
<h2>회원가입<h2>
<form method="post">
    {% csrf_token %}
    <!-- views.py의 form_class를 그대로 받아옴. -->
    {{ form.as_p }}
    <button type="submit">가입하기</submit>
</form>
  1. http://127.0.0.1:8000/polls/signup/ 에서 페이지를 확인해 봅니다.
    User의 회원가입 페이지가 나온 것을 확인할 수가 있습니다. 해당 페이지에서 username과 password를 입력하여 가입을 진행할 수 있습니다. 다만 장고의 CreateView의 경우, 간단한 패스워드를 입력할 때 재입력을 요청하고 있는 것을 확인할 수 있습니다.



4. Serializer를 사용하여 User 만들기

장고 rest_framework에서 제공하는 Serializer 기능을 이용해서도 User를 만들 수 있습니다. django rest framwork에서 제공하는 기능은 polls/ 에 구현하였습니다. 절차는 다음과 같습니다.

  1. polls_api/serializers.py 파일에 사용자 등록 시리얼라이저를 정의합니다.
# Django의 REST 프레임워크에서 시리얼라이저 임포트.
from rest_framework import serializers
from polls.models import Question
# User 모델 임포트
from django.contrib.auth.models import User
# 간단한 패스워드 가입에 대한 거부 처리를 위한 임포트
from django.contrib.auth.password_validation import validate_password

# ModelSerializer를 상속받는 경우, 필드 및 메서드가 자동으로 지정됨.
class QuestionSerializer(serializers.ModelSerializer):
    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date']

class UserSerializer(serializers.ModelSerializer):
    # User 모델의 Primary Key 값으로 연결된 모든 Question 모델의 오브젝트들을 가져옴
    questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'questions']

# 사용자 등록 시리얼라이저 정의
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

    # User에는 password2가 정의되어 있지 않기 때문에 error가 발생.
    # 따라서 직접 create를 정의해야 함.
    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']
  1. polls_api/views.py 파일에 사용자 등록 뷰를 정의합니다.
from polls.models import Question
from polls_api.serializers import *
from rest_framework import generics
from django.contrib.auth.models import User

# generics 클래스의 ListCreateAPIView 상속
# get, post 기능이 구현되어 있음
class QuestionList(generics.ListCreateAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer

# generics 클래스의 RetrieveUpdateDestroyAPIView 상속
# get, put, delete 기능이 구현되어 있음.
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer

class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

# 회원 등록 뷰 클래스 정의
class RegisterUser(generics.CreateAPIView):
    serializer_class = RegisterSerializer
  1. 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(), name='question_detail'),
    path('users/', UserList.as_view(), name='user-list'),
    path('users/<int:pk>/', UserDetail.as_view()),
    path('register/', RegisterUser.as_view()),
]
  1. http://127.0.0.1:8000/rest/register/ 에서 결과 화면을 확인합니다.

    username과 password를 입력하여 post할 수 있는 페이지를 확인할 수가 있습니다. serializers.py에서 validate_password 라이브러리 등을 활용하여서, 패스워드가 일치하지 않거나 패스워드가 너무 단순하거나 등의 처리를 할 수 있습니다. 또한 create() 메서드를 정의하였기에 유저 등록까지 가능한 것을 볼 수 있습니다.



5. User 권한 관리

다음과 같은 기능들에 대해서 구현을 해 봅니다.

  • question 목록을 조회하는 페이지에서 login과 logout이 표시되도록 하는 기능.
    => polls_api/urls.py 파일에 'api-auth' 경로를 추가해 줘야 합니다. 그리고 settings.py 파일에 로그인/로그아웃에 대한 리디렉션 URL을 정의해 줍니다.
  • question 목록을 조회하는 페이지에서 해당 유저가 owner가 되어 qeustion을 등록하는 기능. (다만, login된 상태이어야 함.)
    => polls_api/serializers.py 파일에서 질문 시리얼라이저에 owner 필드를 추가해 주며, 읽기 전용으로 지정합니다. 그러고나서 polls_api/views.py 파일의 QuestionList에 질문 생성 메서드 perform_create()를 정의합니다. 또한 permission_classes를 지정해 줘야 합니다. 이후 QuestionDeatail에도 permission_classes를 지정합니다.
  • 특정 question 페이지에서 질문 내용을 수정하는 기능. (다만, 해당 question을 등록한 user만이 해당 기능을 수행할 수 있어야 함.)
    => permissions.py 파일을 생성해서 클래스를 정의하여 해당 유저가 owner가 맞는지 비교하는 기능을 정의합니다. 해당 클래스를 QuestionDetail의 permission_classes에 추가해 줍니다.

위 기능들을 모두 코드로 구현하면 다음과 같습니다.

  1. polls_api/urls.py 파일에 'api-auth' 경로를 추가합니다.
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(), name='question_detail'),
    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')),
]
  1. mysite/settings.py 파일에 로그인/로그아웃에 대한 리디렉션 URL을 추가해 줍니다.
...
from django.urls import reverse_lazy

LOGIN_REDIRECT_URL = reverse_lazy('question-list')
LOGOUT_REDIRECT_URL = reverse_lazy('question-list')
...
  1. polls_api/serializers.py 파일에 위 기능들에 대한 코드를 구현해 줍니다.
# Django의 REST 프레임워크에서 시리얼라이저 임포트.
from rest_framework import serializers
from polls.models import Question
# User 모델 임포트
from django.contrib.auth.models import User
# 간단한 패스워드 가입에 대한 처리를 위해 임포트
from django.contrib.auth.password_validation import validate_password

# ModelSerializer를 상속받는 경우, 필드 및 메서드가 자동으로 지정됨.
class QuestionSerializer(serializers.ModelSerializer):
    # 사용자의 이름으로 읽어서 해당 owner를 read only로 갖고 옴.
    owner = serializers.ReadOnlyField(source='owner.username')
    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date', 'owner']

class UserSerializer(serializers.ModelSerializer):
    # User 모델의 Primary Key 값으로 연결된 모든 Question 모델의 오브젝트들을 가져옴
    questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'questions']

# 사용자등록 시리얼라이저 정의
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

    # User에는 password2가 정의되어 있지 않기 때문에 직접 create를 정의해야 함.
    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']
  1. polls_api/permissions.py 파일을 생성 후, 다음 코드를 작성합니다.
# 특정 질문에 대한 수정 권한을 관리하기 위한 파일.

from rest_framework import permissions

# owner이거나 owner가 아니라면 읽기용으로만 접근 가능.
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
  1. polls_api/views.py 파일에 위 기능들에 대한 코드를 구현해 줍니다.
from polls.models import Question
from polls_api.serializers import *
# 로그아웃된 상태에서 post를 하지 못하도록 막기 위해 permissions 임포트
from rest_framework import generics, permissions
from django.contrib.auth.models import User
from .permissions import IsOwnerOrReadOnly

# generics 클래스의 ListCreateAPIView 상속
# get, post 기능이 구현되어 있음
class QuestionList(generics.ListCreateAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer
    # 로그인된 상태에서만 질문 등록이 가능하게 만듦.
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    # QuestionList 뷰에서 create할 때, owner는 현재 로그인된 user로
    # 지정되어서 create 됨.
    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

# generics 클래스의 RetrieveUpdateDestroyAPIView 상속
# get, put, delete 기능이 구현되어 있음.
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer
    # 로그인된 상태에서만 질문 수정이 가능하게 만듦.
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

# 회원 등록 뷰 클래스 정의
class RegisterUser(generics.CreateAPIView):
    serializer_class = RegisterSerializer



6. perform_create()

perform_create는 여러 상속을 거쳐 overriding된 메서드입니다. 호출 순서: post() -> crete() -> perform_create(). 따라서, 위의 코드에서는, owner가 해당 유저가 되도록 perform_create 메서드를 overring한 것입니다.

def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

그리고 serializer.save()를 호출할 때, owner를 지정한 것처럼 read only와 같은 필드를 별도로 지정할 수도 있습니다.

profile
천천히, 그리고 꾸준히.

0개의 댓글