2025/11/19 Django - 12

김기훈·2025년 11월 19일

TIL

목록 보기
60/191

오늘 학습 내용 ✅

12. TDD와 REST API 만들기

⭐️ TDD

TDD의 장점

  • 높은 코드 품질: 테스트를 먼저 작성하므로, 코드가 예상대로 동작하는지 명확하게 확인 가능
  • 버그 감소: 코드 작성 전에 테스트를 고려하므로, 잠재적인 버그를 미리 방지
  • 리팩토링의 용이성: 테스트 코드가 리팩토링 후에도 기능이 잘 동작하는지 보장
  • 명확한 요구사항: 테스트 케이스를 작성하면서 요구사항을 명확히 이해하고 정의 가능

TDD의 단점

  • 시간 소모: 초기 테스트 작성 및 반복적인 테스트 실행으로 인해 개발 시간이 증가
  • 복잡성: 복잡한 로직의 경우, 테스트 케이스 작성이 어려움
  • 과도한 테스트: 모든 코드를 테스트하려고 하면, 비효율적인 테스트 코드가 늘어남

Django Test Code

  • Django는 자체적으로 강력한 테스트 프레임워크를 제공하며, unittest를 기반으로 동작
    • Django의 테스트는 애플리케이션의 다양한 부분을 자동화된 테스트로 확인할 수 있게 해줌

기본 테스트 코드 구조

  • Django에서 테스트는 각 앱의 tests.py 파일에 작성됨
    • 각 테스트는 TestCase 또는 APITestCase를 상속받아 작성
from django.test import TestCase
from .models import MyModel

class MyModelTest(TestCase):
    def test_model_creation(self):
        my_model = MyModel.objects.create(name="Test")
        self.assertEqual(my_model.name, "Test")

Django에서의 TDD 실천

    1. 테스트 작성 (Red): 실패하는 테스트를 먼저 작성
def test_example(self):
    response = self.client.get('/my-url/')
    self.assertEqual(response.status_code, 200)
    1. 코드 작성 (Green): 테스트가 통과할 수 있도록 최소한의 코드를 작성
from django.http import HttpResponse

def my_view(request):
    return HttpResponse("Hello, World!")
    1. 리팩토링 (Refactor): 테스트가 통과한 후, 코드를 리팩토링하여 개선합니다.

주요 테스트 기법

모델 테스트

  • 모델의 메서드, 속성, 데이터베이스 동작을 테스트
class MyModelTest(TestCase):
    def test_str_method(self):
        my_model = MyModel(name="Test")
        self.assertEqual(str(my_model), "Test")

뷰 테스트

  • 뷰의 HTTP 응답, 템플릿 렌더링 등을 테스트
from django.urls import reverse

class MyViewTest(TestCase):
    def test_view_url_exists(self):
        response = self.client.get(reverse('my_view'))
        self.assertEqual(response.status_code, 200)

View Test 실습 준비

  • 바로 test.py에 작성해도 괜찮지만 관리하기 어려움( 분리하는게 좋음 )
    • 리팩토링 하고나면 원래 tests.py 에 있던 코드들이 __init__.py로 이동
    • 이름을 바꾼후 __init__.py를 다시 만들거나 잘라내기 해서 코드를 옮긴다.
  • from .model_test import *
    • model_test에 있는 항목들을 임포트해와서 사용 가능

View Test 실습 시작

  • 기능
    • blog_list -> get으로 요청시 Blog_list에 1개의 context만 들어있음
    • Blog_create -> post 요청시에 새로운 Blog가 생성 됨
    • LoginRequired -> 확인
  • 3가지 방법
    • published_at가 null인것
    • published_at가 future인것
    • published_at가 현재보다 작은것

  • test_blog_list

오류발생

  • self.assertEqual(response.context.get('blog_list'), blog_list.count())

    • response.context.get('blog_list') → QuerySet (리스트 형태)
    • blog_list.count() → 정수값(1)
  • 해결

    • 리스트로 변경
      • self.assertEqual( list(response.context.get('blog_list')), list(blog_list) )
    • 둘다 정수값으로 변경
      • self.assertEqual( response.context.get('blog_list').count(), blog_list.count() )
  • AssertionError: 3 != 2


폼 테스트

  • 폼의 유효성 검증을 테스트
from .forms import MyForm

class MyFormTest(TestCase):
    def test_valid_form(self):
        form = MyForm(data={'name': 'Test'})
        self.assertTrue(form.is_valid())

    def test_invalid_form(self):
        form = MyForm(data={'name': ''})
        self.assertFalse(form.is_valid())

시그널 테스트

  • 시그널이 제대로 작동하는지 테스트
from django.db.models.signals import post_save
from .models import MyModel

class MySignalTest(TestCase):
    def test_post_save_signal(self):
        with self.assertLogs('myapp', level='INFO') as cm:
            MyModel.objects.create(name="Test")
        self.assertIn('Signal triggered', cm.output[0])

Django에서 테스트 실행

  • python manage.py test
    • tests.py에 정의된 모든 테스트를 실행하고, 결과를 출력
  • python manage.py test <app name>
    • 특정 앱의 tests.py에 정의된 모든 테스트를 실행하고, 결과를 출력

실습🔥

  • poetry init / poetry add django / django-admin startapp config .
    • python manage.py startapp blog / settings.py의 INSTALLED_APPS등록

  • is_active 추가 및 content 의 TextField가 TimeField여서 오류 였음

  • 의도적으로 틀린값을 주었을 때 어디서 오류가 나고 고쳐야하는지 나옴

  • setUp

    • 테스트 코드가 돌기전에 먼저 돔
  • 함수 이름 test_ 필수


REST API

REST

  • 상태를 표현하는 전송

API

  • 네이버 로그인할때 -> profile/me -> 응다을 받아옴

REST API

SPA

  • React / Vue / Angular
  • GraphQL 공부 추천

REST API 작성

  • APPEND_SLASH

  • admin페이지 이용 - python manage.py createsuperuser


⭐️ DRF

  • Django REST Framework 공식 문서
  • Django REST Framework
    • Django에서 웹 API를 쉽게 구축할 수 있도록 도와주는 강력한 라이브러리
      • 직렬화(Serialization), 뷰(View), 권한 부여(Authorization) 등의 기능을 제공
      • RESTful API 개발을 단순화함

Serializer

  • Django 모델 인스턴스를 JSON과 같은 데이터 포맷으로 변환
  • JSON 데이터를 모델 인스턴스로 변환하는 데 사용 / 즉, 데이터의 직렬화와 역직렬화를 담당
    • fields에 적은 항목들만 리턴해줌

사용법

  from rest_framework import serializers
  from .models import MyModel

  class MyModelSerializer(serializers.ModelSerializer):
      class Meta:
          model = MyModel
          fields = '__all__'
  • ModelSerializer: Django 모델을 기반으로 하는 직렬화를 자동으로 처리
  • Serializer: 더 복잡한 로직이 필요할 때 사용되며, 필드와 그 처리 로직을 커스터마이징 가능

주요 메서드

ModelSerializer 주요 메서드

  • to_representation(instance): 모델 인스턴스를 JSON 데이터로 변환
  • create(validated_data): 유효한 데이터를 사용하여 모델 인스턴스를 생성
  • update(instance, validated_data): 기존 모델 인스턴스를 업데이트

serializer 주요 메서드

  • validate(self, attrs): 요청으로부터 입력받은 값들에 대해서 검증을 수행하는 메서드

View (Generic Views, APIView)

APIView

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

class MyAPIView(APIView):
    def get(self, request):
        data = {"message": "Hello, World!"}
        return Response(data, status=status.HTTP_200_OK)

    def post(self, request):
        data = request.data
        return Response(data, status=status.HTTP_201_CREATED)
  • Django의 기본 View를 확장하여 RESTful API 엔드포인트를 생성할 수 있도록 해줌
  • HTTP 메서드(GET, POST, PUT, DELETE 등)를 오버라이드하여 사용할 수 있음

Generic Views

  • CRUD 작업을 간단히 처리할 수 있도록 미리 정의된 클래스
    • Generic Views를 사용하면 반복적인 코드 작성을 줄이기 가능
from rest_framework import generics
from .models import MyModel
from .serializers import MyModelSerializer

class MyModelListCreateView(generics.ListCreateAPIView):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
  • ListCreateAPIView: 목록 조회 및 새 객체 생성을 처리
  • RetrieveUpdateDestroyAPIView: 객체의 조회, 수정, 삭제를 처리

ViewSets

  • 관련된 여러 뷰 로직을 하나의 클래스에 결합한 것
  • ModelViewSet, ReadOnlyModelViewSet과 같은 기본 ViewSet을 사용하면,
    • 더욱 간결하게 API를 정의 가능
from rest_framework import viewsets
from .models import MyModel
from .serializers import MyModelSerializer

class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer

DRF의 주요 개념들

1. Router

  • URL과 ViewSet을 자동으로 연결해주는 기능
    • 이를 통해 개발자는 URLConf를 일일이 작성할 필요 없이, ViewSet만으로 URL 매핑이 가능
from rest_framework.routers import DefaultRouter
from .views import MyModelViewSet

router = DefaultRouter()
router.register(r'mymodels', MyModelViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

2. Permissions

  • API에 대한 접근 권한을 제어하는 기능
    • 기본적으로 DRF는 다양한 권한 클래스를 제공하며, 이를 통해 API 접근을 제한 가능
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView

class MyAPIView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        return Response({"message": "Authenticated!"})
  • IsAuthenticated: 인증된 사용자만 접근 가능
  • IsAdminUser: 관리자만 접근 가능
  • AllowAny: 누구나 접근 가능

3. Authentication

  • DRF는 다양한 인증 방식을 지원
  • 기본적인 Session 인증, Token 인증 외에도 OAuth, JWT 등의 다양한 인증 방식을 사용 가능
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView

class MyAPIView(APIView):
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def get(self, request):
        return Response({"message": "Token authenticated!"})

4. Throttling

  • API 요청을 제한하는 기능
  • 특정 시간 내에 요청할 수 있는 횟수를 제한하여 API 남용을 방지 가능
from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView

class MyAPIView(APIView):
    throttle_classes = [UserRateThrottle]

    def get(self, request):
        return Response({"message": "Throttled request!"})

RF 실습

  • poetry add djangorestframework
    • INSTALLED_APPS 에 'rest_framework', 등록
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}  

  • router.register(r'users', api_views.UserViewSet, basename='user')
    • path('', include(router.urls)) 이걸로 /api/ 는 고정
    • r'users' = include에서 users로 시작하라는 것과 같은 의미
      • 이걸로 /api/users/

  • /blog / GET -> List | POST -> Create
  • /blog/1 GET -> Detail / PUT, PATCH -> update / DELETE -> delete

⭐️ ViewSet

  • Django REST Framework 공식 문서 - ViewSets | Django REST Framework 공식 문서 - Routers
  • ViewSet은 Django REST Framework에서 제공하는 강력한 기능
    • 여러 CRUD(Create, Retrieve, Update, Delete) 작업을 단일 클래스에서 처리할 수 있게 해줌
  • ViewSet은 기본적으로 Django의 뷰와 비슷함
    • 다양한 동작(GET, POST, PUT, DELETE 등)을 하나의 클래스에서 정의 가능
    • 그리하여, 코드의 중복을 줄이고 간결하게 관리 가능

1. ViewSet의 종류

  • ViewSet: 모든 CRUD 작업을 직접 정의할 수 있는 기본 ViewSet.
  • ModelViewSet: ViewSet을 상속받아 모델의 CRUD 작업을 자동으로 처리
  • ReadOnlyModelViewSet: 읽기 전용 작업(리스트, 상세 조회)만 처리

2. ViewSet 사용법

ModelViewSet

  • ModelViewSet은 가장 많이 사용되는 ViewSet, 모델의 모든 CRUD 작업을 처리
    • querysetserializer_class만 정의하면, 나머지 작업은 자동으로 처리됨
from rest_framework import viewsets
from .models import MyModel
from .serializers import MyModelSerializer

class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer

ReadOnlyModelViewSet

  • ReadOnlyModelViewSet은 읽기 전용 API를 제공하며, listretrieve 작업만 지원
from rest_framework import viewsets
from .models import MyModel
from .serializers import MyModelSerializer

class MyModelReadOnlyViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer

Custom ViewSet

  • 기본 제공되지 않는 동작이나 커스텀 동작을 추가하고 싶은 경우
    • ViewSet을 직접 상속받아 필요한 메서드를 정의 가능
from rest_framework import viewsets
from rest_framework.response import Response

class CustomViewSet(viewsets.ViewSet):
    def list(self, request):
        data = {"message": "This is a list view"}
        return Response(data)

    def retrieve(self, request, pk=None):
        data = {"message": f"This is the detail view of {pk}"}
        return Response(data)

3. Router와 ViewSet 연결

Router

  • Router는 URL 라우팅을 자동으로 처리해주는 Django REST Framework의 기능
    • ViewSet과 함께 사용하면, URL 패턴을 자동으로 생성할 수 있어 URL 설정을 간편하게 가능

SimpleRouter

  • SimpleRouter는 가장 기본적인 Router로, ViewSet과 연결하여 기본적인 CRUD URL을 자동으로 생성
python코드 복사
from rest_framework.routers import SimpleRouter
from .views import MyModelViewSet

router = SimpleRouter()
router.register(r'mymodels', MyModelViewSet)

urlpatterns = router.urls
  • 다음과 같은 URL 패턴이 자동으로 생성
    • GET /mymodels/list
    • GET /mymodels/{pk}/retrieve
    • POST /mymodels/create
    • PUT /mymodels/{pk}/update
    • DELETE /mymodels/{pk}/destroy

DefaultRouter

  • DefaultRouterSimpleRouter의 기능에 더해, 기본 API 루트 엔드포인트를 추가로 생성 해줌
    • 아래의 경우
      • / 에 API 루트 엔드포인트가 추가되어 사용자가 어떤 엔드포인트를 사용할 수 있는지 쉽게 파악가능
rom rest_framework.routers import DefaultRouter
from .views import MyModelViewSet

router = DefaultRouter()
router.register(r'mymodels', MyModelViewSet)

urlpatterns = router.urls

Router와 기존 URL 패턴 통합

  • Router를 사용하면서도, 기존의 URL 패턴과 함께 사용 가능
    • 아래의 코드처럼 진행되면 ViewSet으로 관리되는 URL과 개별 뷰 함수가 처리하는 URL을 모두 관리 가능
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import MyModelViewSet, custom_view

router = DefaultRouter()
router.register(r'mymodels', MyModelViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('custom/', custom_view),

BlogViewSet🔥

  • django-extionsion | ipython 설치후 shell_plus 사용 가능
    • INSTALLED_APPS에 django-extionsion 등록 필요

새롭게 알게된 내용 ✅

  • git에서 왜 새로운 브랜치를 만들 때, git pull을 해야하는가
    • 새 브랜치를 만들 때는 항상 최신 main을 기반으로 만들어야 하기 때문
  • GraphQL 공부해보기 추천

어려운 내용(추가 학습 필요) ✅

  • 쿼리셋에 대해서 깊이 이해해보기
  • 연관 객체 다루기(ORM)
    • Lazy Loading -> 시너지
    • 외래키
  • ERD를 좀더 완전하게 이해해보기
  • 모델테스트
    • 이해가 좀더 많이 필요할 듯

오늘 발생한 문제(발생 했다면) ✅

  • view test 오류 하나 스스로 처리해보기
    • 개선이 안되는 중
  • ViewSet 좀더 자세하게 해보기

라이브세션

  • 레코드를 조회한다 = 테이블안의 특정조건을 충족하는 데이터를 가져온다.
  • CREATE = 데이터 생성
  • GET = 데이터를 가져올 때 / 조회할 때 = 어떤 데이터를 얻어온다/가져온다
    • 조건을 만족하지 않는 데이터를 가져오려하면 오류 발생(사용시 주의 )
    • ShortURL.objects.get(id=1) = id가 1인 데이터를 가져옴
  • PRINT_SQL=1 python manage.py shell
    • 환경변수 PRINT_SQL 이 설정되어 있을 때,
    • Django 가 실행하는 SQL 쿼리를 콘솔에 출력하도록 로그 설정을 켜는 코드
    1. Django의 QuerySet은 기본적으로 지연 로딩(Lazy loading) 방식으로 동작한다.
    • 지연 로딩이란?
      • 데이터베이스 조회를 최대한 미루는 것
    • 지연 로딩은 왜 필요?
      • 불필요한 데이터 조회를 최대한 막기 위해서(= 최대한 효율적으로 동작)
    1. QuerySet은 언제 실제로 데이터베이스에 조회를 하는가?
    • 데이터를 실제로 사용해야 되는 시점에 평가(evaluate)
    • 데이터가 실제로 사용되는 시점: 반복문(for), list(), dict(), ..
    1. 쿼리가 발생하긴 하지만, 완벽한 평가가 이루어지지 않는 경우
    • print() -> LIMIT 21-> 출력을 위해 최대 21의 데이터만 조회 get(), first(), Last()
    • -> LIMIT 1-> 1개만 가져오기 때문에, 전체 쿼리셋은 평가하지 않음
profile
안녕하세요.

0개의 댓글