Hot Item

[Hot Item] #6. 회원가입 / 로그인 / 로그아웃 API 구현 및 Swagger UI 테스트

devjingood 2026. 3. 25. 11:03

이 글에서 다루는 내용

  • config/urls.py에 Swagger UI 및 accounts URL 연결하기
  • SignupSerializer, LogoutSerializer 작성하기
  • SignupView, LogoutView 작성하기
  • accounts/urls.py에서 로그인/로그아웃/회원가입 라우팅하기
  • token_blacklist 설정 및 Swagger UI에서 전체 흐름 테스트하기

1. config/urls.py — Swagger UI와 accounts 연결

from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    path('api/accounts/', include('accounts.urls')),
]

URL 역할

api/schema/ OpenAPI 스키마를 JSON으로 반환 (Swagger UI의 원본 데이터)
api/docs/ Swagger UI 페이지 (브라우저에서 직접 API 테스트 가능)
api/accounts/ accounts 앱의 URL을 하위 경로로 포함

SpectacularSwaggerView는 url_name='schema'로 위에서 정의한 api/schema/를 참조합니다. api/schema/가 반환하는 JSON을 읽어서 Swagger UI를 렌더링하는 구조입니다.


2. token_blacklist 앱 등록 및 migrate

LogoutView에서 사용할 token.blacklist()는 SimpleJWT의 token_blacklist 앱이 활성화되어 있어야 동작합니다. settings.py의 INSTALLED_APPS에 추가합니다.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework',
    'rest_framework_simplejwt.token_blacklist',  # 추가
    'drf_spectacular',

    'accounts',
]

등록 후 migrate를 실행해 블랙리스트 관련 테이블을 생성합니다.

python manage.py migrate
Operations to perform:
  Apply all migrations: ..., token_blacklist, ...
Running migrations:
  Applying token_blacklist.0001_initial... OK
  ...

💡 token_blacklist가 하는 일
로그아웃된 Refresh Token을 DB에 기록해둡니다. 이후 해당 토큰으로 재발급을 시도하면 블랙리스트를 확인하고 거부합니다. 이 앱 없이 token.blacklist()를 호출하면 AttributeError가 발생합니다.


3. accounts/serializers.py

from rest_framework import serializers
from django.contrib.auth import get_user_model

User = get_user_model()

class SignupSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ('username', 'password', 'phone_number')

    def create(self, validated_data):
        user = User.objects.create_user(
            username=validated_data['username'],
            password=validated_data['password'],
            phone_number=validated_data['phone_number']
        )
        return user


class LogoutSerializer(serializers.Serializer):
    refresh = serializers.CharField(help_text="발급 받은 Refresh Token을 입력하세요.")

SignupSerializer

ModelSerializer를 상속해 User 모델 기반으로 만들었습니다. password 필드에 write_only=True를 지정한 것이 중요합니다. 이 옵션이 없으면 회원가입 응답 JSON에 비밀번호가 평문으로 포함됩니다.

create 메서드를 직접 오버라이드한 이유는 create_user()를 사용해야 비밀번호가 해시 처리되기 때문입니다. User.objects.create()를 쓰면 비밀번호가 평문으로 저장됩니다.

LogoutSerializer

ModelSerializer가 아닌 일반 Serializer를 상속합니다. DB 모델과 무관하게 요청 바디에서 refresh 토큰 문자열만 받으면 되기 때문입니다. help_text는 Swagger UI에서 해당 필드 옆에 설명 문구로 표시됩니다.


4. accounts/views.py

from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth import get_user_model
from .serializers import *

User = get_user_model()


class SignupView(generics.CreateAPIView):
    queryset = User.objects.all()
    serializer_class = SignupSerializer
    permission_classes = [AllowAny]


class LogoutView(generics.GenericAPIView):
    permission_classes = [IsAuthenticated]
    serializer_class = LogoutSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        try:
            refresh_token = serializer.validated_data["refresh"]
            token = RefreshToken(refresh_token)
            token.blacklist()
            return Response(
                {"message": "성공적으로 로그아웃 되었습니다."},
                status=status.HTTP_205_RESET_CONTENT
            )
        except Exception as e:
            return Response(
                {"error": "유효하지 않거나 이미 만료된 토큰입니다."},
                status=status.HTTP_400_BAD_REQUEST
            )

SignupView

generics.CreateAPIView를 상속하면 POST 요청을 받아 시리얼라이저로 유효성 검사 후 저장하는 로직이 자동으로 구현됩니다. permission_classes = [AllowAny]로 비로그인 사용자도 접근할 수 있게 합니다.

LogoutView

permission_classes = [IsAuthenticated]로 로그인된 사용자만 접근 가능합니다. 요청 바디에서 Refresh Token을 받아 token.blacklist()로 블랙리스트에 등록합니다. 이미 만료됐거나 유효하지 않은 토큰이 들어오면 Exception으로 잡아 400을 반환합니다.


5. accounts/urls.py

from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import *

urlpatterns = [
    path('signup/', SignupView.as_view(), name="signup"),
    path('login/', TokenObtainPairView.as_view(), name="login"),
    path('refresh/', TokenRefreshView.as_view(), name="token_refresh"),
    path('logout/', LogoutView.as_view(), name="logout"),
]

로그인과 토큰 갱신은 직접 View를 만들지 않고 SimpleJWT가 제공하는 TokenObtainPairView와 TokenRefreshView를 그대로 사용합니다. 이 두 View는 이미 Swagger UI에도 자동으로 문서화됩니다.

최종적으로 구성된 accounts API 엔드포인트는 다음과 같습니다.

메서드 URL 설명 인증 필요

POST api/accounts/signup/ 회원가입
POST api/accounts/login/ 로그인 (Access + Refresh Token 발급)
POST api/accounts/refresh/ Access Token 재발급
POST api/accounts/logout/ 로그아웃 (Refresh Token 블랙리스트 등록)

6. Swagger UI에서 테스트하기

서버를 실행하고 http://localhost:8001/api/docs/에 접속합니다.

python manage.py runserver 0.0.0.0:8000

회원가입 테스트

POST /api/accounts/signup/을 열고 username, password, phone_number를 입력해 Execute합니다. 201 Created가 반환되면 성공입니다.

로그인 테스트

POST /api/accounts/login/에서 방금 만든 계정으로 로그인합니다. 성공하면 응답 바디에 access와 refresh 토큰이 반환됩니다.

{
  "access": "eyJhbGciOiJIUzI1NiIsInR...",
  "refresh": "eyJhbGciOiJIUzI1NiIsInR..."
}

인증이 필요한 API 테스트

Swagger UI 우측 상단의 Authorize 버튼을 클릭하고, Bearer <access_token> 형식으로 입력합니다. 이후 IsAuthenticated가 걸린 API를 호출할 때 헤더에 자동으로 포함됩니다.

로그아웃 테스트

POST /api/accounts/logout/에 로그인 시 받은 refresh 토큰을 입력하고 Execute합니다. 205 Reset Content와 함께 로그아웃 메시지가 반환됩니다. 이후 같은 refresh 토큰으로 /refresh/를 호출하면 400이 반환되어 블랙리스트가 정상 동작하는 것을 확인할 수 있습니다.


7. 정리

오늘 한 작업을 요약하면:

  • config/urls.py에 Swagger UI 경로와 accounts 앱 URL을 연결했습니다.
  • token_blacklist 앱을 INSTALLED_APPS에 등록하고 migrate로 블랙리스트 테이블을 생성했습니다.
  • SignupSerializer에서 write_only=True로 비밀번호 노출을 막고, create_user()로 비밀번호를 해시 처리했습니다.
  • LogoutView에서 Refresh Token을 블랙리스트에 등록해 재사용을 차단했습니다.
  • SimpleJWT의 TokenObtainPairView, TokenRefreshView를 직접 URL에 연결해 로그인/갱신 기능을 별도 구현 없이 사용했습니다.
  • Swagger UI에서 회원가입 → 로그인 → 로그아웃 전체 흐름을 테스트했습니다.

다음 포스트부터는 이 프로젝트의 핵심인 Redis Sorted Set을 이용한 대기열 시스템 구현을 시작합니다. 🚀