미준수 시 재작성 요청 )@extend_schema 데코레이터를 활용하여 해당 API 에 대한 git config --local commit.template ./.github/commit_template.txtoz_externship_be/
├── .github/ # 깃허브 설정 파일 ( 커밋템플릿, 이슈템플릿, pr 템플릿, CI / CD 등 )
│ ├── COMMIT_TEMPLATE/ # 하위에 커밋 템플릿을 정의
│ ├── ISSUE_TEMPLATE/ # 하위에 이슈템플릿을 정의
│ └── workflows/ # 하위에 CI / CD 스크립트를 정의
│ ├── checks.yml/ # develop 또는 main 브랜치에 Push 또는 PR Merge 시 데이터 베이스 연결 확인, 코드 포매팅 체크, 테스트 통과 여부를 검사하는 스크립트
│ ├── dev_deploy.yml/ # develop 브랜치에 Push시 개발 서버에 배포 자동화를 구현한 스크립트
│ └── prod_deploy.yml/ # develop 브랜치에 Push시 개발 서버에 배포 자동화를 구현한 스크립트
├── config/
│ ├── __init__.py
│ ├── settings/
│ │ ├── base.py # 프로젝트 전역 공통 설정 파일
│ │ ├── dev.py # 개발 서버 프로젝트 전역 설정 파일
│ │ ├── local.py # 로컬 환경 프로젝트 전역 설정 파일
│ │ └── prod.py # 프로덕션 환경 프로젝트 전역 설정 파일
│ ├── asgi.py
│ ├── urls.py
│ └── wsgi.py
├── apps/ # 앱 디렉토리 (앱별로 디렉토리를 나눔)
│ ├── core/ # 공통 앱 (공통으로 사용되는 utils, base 모델, commands 정의)
│ │ ├── commands/ # 장고 커맨드 등록 폴더
│ │ ├── utils/ # 프로젝트 전역에서 공통으로 사용되는 유틸 함수를 정의하는 폴더
│ │ ├── tests/ # core 내에 정의된 util 메서드 혹은 클래스에 대한 테스트들을 구현하는 폴더
│ │ └── models.py # 모든 앱에서 공통으로 사용되는 base 모델 정의 (ex. TimeStampModel)
│ ├── app_name1/
│ │ ├── migrations/ # 마이그레이션 파일
│ │ ├── services/ # 앱에서 사용되는 서비스 로직을 구현하는 폴더 / 서비스 로직
│ │ ├── tests/ # 앱에서 사용되는 테스트들을 구현하는 폴더
│ │ ├── models/ # 앱에서 사용되는 모델들을 정의하는 폴더
│ │ ├── urls/ # 앱 전용 URL 라우팅을 정의하는 폴더
│ │ ├── serializers/ # 시리얼 라이저 모음 폴더
│ │ ├── views/ # CBV, FBV 를 구현하는 폴더 / HTTP 호출 관련
│ │ └── apps.py # 앱 설정
│ ├── app_name2/ # 다른 앱
│ │ ├── migrations/ # 마이그레이션 파일
│ │ ├── services/ # 앱에서 사용되는 서비스 로직을 구현하는 폴더 / 서비스 로직
│ │ ├── tests/ # 앱에서 사용되는 테스트들을 구현하는 폴더
│ │ ├── models/ # 앱에서 사용되는 모델들을 정의하는 폴더
│ │ ├── urls/ # 앱 전용 URL 라우팅을 정의하는 폴더
│ │ ├── serializers/ # 시리얼 라이저 모음 폴더
│ │ ├── views/ # CBV, FBV 를 구현하는 폴더
│ │ └── apps.py # 앱 설정
│ └── ...
├── envs/ # 환경변수 파일들
│ ├── .local.env # 로컬 환경에서 서버 구동 및 테스트 시 필요한 환경변수
│ ├── .dev.env # 개발 서버 환경에서 서버 구동 및 테스트 시 필요한 환경변수
│ └── .prod.env # 배포 환경에서 서버 구동 및 테스트 시 필요한 환경변수
├── resources/ # 초기 설정 파일 및 스크립트, nginx, docker, kubernetes 의 yaml 파일
│ ├── nginx/
│ │ ├── Dockerfile # nginx 이미지 빌드 도커 파일
│ │ └── nginx.local.conf # 로컬 환경에서 테스트 용 nginx 설정 파일
│ │ └── nginx.dev.conf # 개발 서버 환경에서 테스트 용 nginx 설정 파일
│ │ └── nginx.prod.conf # 프로덕션 서버 환경에서 테스트 용 nginx 설정 파일
│ └── scripts/ # 필요한 shell scripts를 모아두는 디렉터리 (test, formatter, create_dummy 등)
│ ├── code_formatting.sh # black, isort 코드 포매팅 실행 스크립트
│ └── test.sh # mypy 타입 검사 수행 및 전체 테스트코드 실행 시 사용되는 스크립트
├── manage.py # Django 실행 파일
├── poetry.lock # poetry 의존성 패키지 설치 정보
├── pyproject.toml # poetry 의존성 패키지 목록 및 설정
├── dockerfile # 도커 이미지 빌드 파일
├── docker-compose.local.yml # 로컬 환경 테스트 용 도커 컨테이너 정의 파일
└── README.md # 프로젝트 소개서
현재 세팅된 프로젝트에서는 black, isort, mypy를 사용하여 코드 포매팅과 타입 어노테이션 준수를 확인
mypy는 정적 타입 검사기
Python 코드에서 타입 힌트를 이용해 오류를 사전에 잡는 도구
사용하는 이유
def add(x: int, y: int) -> int:
return x + y
add("1", "2") # mypy는 여기서 오류 발생 (str 대신 int가 필요)
import 정렬 도구
Python 코드에서 import 구문을 자동으로 정렬해주는 도구
사용하는 이유
- 1. 자동 정렬 전
import sys
import os
from django.conf import settings
import datetime
- 2. 자동 정렬 후
import datetime
import os
import sys
from django.conf import settings
자동 코드 포매터
Python 코드를 PEP8 스타일 가이드에 맞게 자동으로 정리해주는 도구
사용하는 이유
- 1. 정리 전
def hello(name): print("Hello, " + name)
- 2. 정리 후(black 적용)
def hello(name):
print("Hello, " + name)
프로젝트 루트디렉터리/resources/scripts 디렉터리에 위치한 code_formatting.sh 파일을 실행하여 코드 포맷을 일관되게 유지하고, Django 내부에 포함된 TestClient를 활용하여 테스트코드를 작성from rest_framework.test import APITestCase
from django.urls import reverse
from rest_framework import status
from django.contrib.auth import get_user_model
User = get_user_model()
class UserSignupTest(APITestCase):
def setUp(self):
# ex: path('signup/', SignupView.as_view(), name='user-signup')
self.signup_url = reverse('user-signup')
self.valid_payload = {
"username": "testuser",
"email": "test@example.com",
"password": "securepassword123"
}`
def test_signup_success(self):
response = self.client.post(self.signup_url, data=self.valid_payload)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(User.objects.filter(username="testuser").exists())
self.assertEqual(response.data["username"], "testuser")
self.assertNotIn("password", response.data) # 보안상 비밀번호는 응답에 없어야 함
def test_signup_missing_fields(self):
response = self.client.post(self.signup_url, data={})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("username", response.data)
self.assertIn("email", response.data)
self.assertIn("password", response.data)
def test_signup_duplicate_username(self):
# 이미 같은 username의 사용자 생성
User.objects.create_user(username="testuser", email="test1@example.com", password="password123")
response = self.client.post(self.signup_url, data=self.valid_payload)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("username", response.data)
def test_signup_weak_password(self):
weak_payload = self.valid_payload.copy()
weak_payload["password"] = "123"
response = self.client.post(self.signup_url, data=weak_payload)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("password", response.data)
drf-spectacular 를 사용djangorestframework 의 APIView 클래스의 extend_schema 데코레이터를 사용하여 스키마를 구성하고 @extend_schema(
tags=["User"],
summary="회원 정보 업데이트 API",
description="""
오즈 코딩 스쿨 이용자들 중 로그인 한 회원이 자기 자신의 정보를 수정할 때 사용하는 API 입니다.
요청 본문으로 업데이트할 필드를 선택적으로 포함하여 요청을 보내면, 서버에서 해당 유저의 유저정보 필드들 중 요청 본문에 포함된 필드를 업데이트 합니다.
""",
request=UserUpdateRequestSerializer,
responses=UserUpdateResponseSerializer
)
class UserUpdateAPIView(APIView):
def patch(self, request: Request) -> Response:
serailizer = UserUpdateRequestSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
instance = serailizer.save()
return Response(data=UserUpdateResponseSerializer(instance).data, status=status.HTTP_200_OK)
오즈 코딩 스쿨 이용자들 중 로그인 한 회원이 자기 자신의 정보를 수정할 때 사용하는 API
요청 본문으로 업데이트할 필드를 선택적으로 포함하여 요청을 보내면,
서버에서 해당 유저의 유저정보 필드들 중 요청 본문에 포함된 필드를 업데이트 함
/admin/users | 2. /admin/exams | 3. /admin/qna-questions백엔드 서버가 어떤 기능을 제공하는지 URL, Method, Request/Response 등을 표준 형식으로 정의한 문서
서버와 클라이언트(프론트/앱/협업 개발자)들이 약속하는 규칙서
| 기능 | URL | Method | Request | Response |
|---|---|---|---|---|
| 로그인 | /api/users/login/ | POST | {id, pw} | {access, refresh} |
# OpenAPI 3.0 YAML
paths:
/users/login:
post:
summary: User Login
requestBody:
content:
application/json:
schema:
type: object
properties:
username:
type: string
password:
type: string
responses:
'200':
description: Login success
POST /api/users/register/
Request:
{
"username": "kihoon",
"password": "1234",
"email": "test@example.com"
}
Response:
201 Created
{
"id": 1,
"username": "kihoon",
"email": "test@example.com"
}
View, Model, Serializer 등으로부터 분리하여, oz_externship/
├── apps/
│ └── users/
│ ├── models.py
│ ├── services/
│ │ └── auth_service.py
│ ├── serializers.py
│ ├── urls.py
│ └── views.py
# models.py
- 1. 데이터베이스 테이블과 매핑되는 구조 정의 / DB 저장,조회 기능 담당
from django.contrib.auth.models import AbstractBaseUser
from django.db import models
class User(AbstractBaseUser):
email = models.EmailField(unique=True, max_length=50)
name = models.CharField(max_length=20)
nickname = models.CharField(max_length=20, unique=True)
phone = models.CharField(max_length=13)
birthday = models.DatetimeField()
created_at = models.DatetimeField(auto_now_add=True)
updated_at = models.DatetimeField(auto_now=True)
USERNAME_FIELD = "email
class Meta:
db_table = "users"
# services/auth_service.py
- 1. 핵심 비즈니스 로직 담당/트랜젝션 처리/복잡한 흐름 제어/여러 모델 간 연동 등
from django.contrib.auth import authenticate
from rest_framework_simplwjwt.tokens import RefreshToken
class AuthService:
@staticmethod
def jwt_login(email: str, password: str) -> dict:
user = authenticate(email=email, password=password)
if user is None:
raise ValueError("이메일 또는 비밀번호가 올바르지 않습니다.")
refresh_token = RefreshToken.for_user(user)
return {
"access_token": str(refresh_token.access_token),
"refresh_token": str(refresh_token),
"user": user
}
# serializers.py
- 1. 요청 데이터의 유효성 검사
- 2. 모델 인스턴스를 JSON 등으로 직렬화하여 응답 데이터 생성
from rest_framework import serializers
from apps.users.models import User
class UserLoginRequestSerializer(serailizers.Serializer)
email = serializers.EmailField(max_length=50)
password = serializers.CharField()
class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("id", "nickname", "email")
read_only_fields = fields
class UserLoginResponseSerializer(serializers.Serializer):
access_token = serializers.CharField()
refresh_token = serializers.CharField()
user = UserInfoSerializer()
# views.py
- 1. 요청을 받고, 서비스 호출 및 응답을 반환
- 2. 비즈니스 로직은 최소화
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from apps.users.services.auth_service import AuthService
from apps.users.serializers import UserLoginRequestSerializer, UserLoginResponseSerializer
class UserLoginAPIView(APIView):
def post(self, request):
serailizer = UserLoginRequestSerializer(request.data)
serailizer.is_valid(raise_exception=True)
# AuthService를 호출하여 jwt_login 메서드를 통해 비즈니스 로직 구현
try:
response_data = AuthService.jwt_login(serailizer.validated_data)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
rsp_serilizer = UserLoginResponseSerializer(response_data)
return Response({"data": rsp_serilizer.data}, status=status.HTTP_200_OK)
@staticmethod 또는 @classmethod를 활용해 독립적으로 호출 가능하게 구성transaction.atomic으로 데이터 정합성을 관리 가능class Product(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
image = models.ImageField(upload_to='products')
price = models.IntegerField()
stock = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class ProductSerializer(serializers.ModelSerializer):
image = serializers.ImageField(write_only=True)
image_url = serializers.CharField(read_only=True, source='image.url')
class Meta:
model = Product
exclude = ('created_at', 'updated_at')
extra_kwargs = {
'stock': {'write_only': True},
'description': {'write_only': True},
}
ProductSerializer 는 Product 등록, 리스트 조회 시 사용할 시리얼라이저class ProductDetailSerializer(serializers.ModelSerializer):
image = serializers.ImageField(write_only=True)
image_url = serializers.CharField(read_only=True, source='image.url')
class Meta:
model = Product
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
ProductDetailSerializer는 Product 단일 상세 조회, 정보 수정 시 사용할 시리얼라이저read_only_fields, extra_kwargs, validators 등을 통해 유효성 검증을 추가 가능