Skip to content

geniee44/django_rest_framework_16th

ย 
ย 

Repository files navigation

CEOS 16๊ธฐ ๋ฐฑ์—”๋“œ ์Šคํ„ฐ๋”” ๋ชจ๋ธ๋ง ๋ฐ drf ์—ฐ์Šต์„ ์œ„ํ•œ ๋ ˆํฌ

2์ฃผ์ฐจ ๋ฏธ์…˜: DB ๋ชจ๋ธ๋ง ๋ฐ Django ORM

todo mate โœ“

์˜ค๋Š˜ ํ•˜๋ฃจ ํ•ด์•ผํ•  ์ผ๊ณผ ์žˆ์—ˆ๋˜ ์ผ์„ ์‰ฝ๊ณ  ์˜ˆ์˜๊ฒŒ ๊ธฐ๋กํ•  ์ˆ˜ ์žˆ๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜

์ฃผ์š” ๊ธฐ๋Šฅ ์ •๋ฆฌ

  • ๋ชฉํ‘œ ์„ค์ •, ๋ชฉํ‘œ๋ณ„ ์ƒ‰์ƒ ๊ด€๋ฆฌ
  • ๋ชฉํ‘œ ๋‹น ํ• ์ผ ๋ชฉ๋ก ๋ถ„๋ฅ˜
  • ํ• ์ผ ๋ณด๊ด€ํ•จ์œผ๋กœ ์ด๋™
  • ์›ํ•˜๋Š” ์‹œ๊ฐ„์— ํ• ์ผ ์‹œ๊ฐ„ ์•Œ๋ฆผ
  • ์˜ค๋Š˜ ํ•˜๋ฃจ ์ผ๊ธฐ ์ ๊ธฐ
  • ์ผ๊ธฐ์— ๋Œ€ํ‘œ ์ด๋ชจ์ง€ ์„ค์ •
  • ์นœ๊ตฌ ๊ณ„์ • ํŒ”๋กœ์ž‰

DB ์„ค๊ณ„

image

  • todo์˜ color๋‚˜ diary์˜ emoji๋Š” ๊ฐœ๋ฐœํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ํ”„๋ก ํŠธ ์ธก๊ณผ ํ•ฉ์˜ํ•ด์„œ ๊ฒฐ์ •ํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

  • follower/following์˜ ์„ค๊ณ„๊ฐ€ ์ €๋Ÿฐ ์‹์ด ์•„๋‹ ๊ฒƒ ๊ฐ™๋‹ค.

models.py ์ž‘์„ฑ ๋๋‚˜๋ฉด migration!

python manage.py makemigrations
python manage.py migrate

ORM ์ด์šฉํ•ด๋ณด๊ธฐ

python shell ๋“ค์–ด๊ฐ€๊ธฐ

python manage.py shell
  1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ํ•ด๋‹น ๋ชจ๋ธ ๊ฐ์ฒด 3๊ฐœ ๋„ฃ๊ธฐ image
  2. ์‚ฝ์ž…ํ•œ ๊ฐ์ฒด๋“ค์„ ์ฟผ๋ฆฌ์…‹์œผ๋กœ ์กฐํšŒํ•ด๋ณด๊ธฐ (๋‹จ, ๊ฐ์ฒด๋“ค์ด ๊ฐ์ฒด์˜ ํŠน์„ฑ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ตฌ๋ถ„๊ฐ€๋Šฅํ•œ ์ด๋ฆ„์œผ๋กœ ๋ณด์—ฌ์•ผ ํ•จ) image
  3. filter ํ•จ์ˆ˜ ์‚ฌ์šฉํ•ด๋ณด๊ธฐ

image

์—๋Ÿฌ ํ•ด๊ฒฐ

  • ModuleNotFoundError: No module named 'environ'

    $ pip install django-environment
    
  • NameError: name '_mysql' is not defined

    django์—์„œ mysql ๊ฐœ๋ฐœํ•  ๋•Œ ๊ฐ€๋” ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋ผ๊ณ  ํ•œ๋‹ค. mysql์„ reinstallํ•ด๋ณด๊ณ  ์•ˆ๋œ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด pymysql๋กœ ์ถฉ๋Œ ๋ฐ ํ˜ธํ™˜ ๋ฌธ์ œ ์žก๊ธฐ!

    $ pip install pymysql
    

    setting.py ์—ญํ• ์„ ํ•˜๋Š” settings/base.py์— ์•„๋ž˜ ์ฝ”๋“œ ์ถ”๊ฐ€

    import pymysql
    
    pymysql.install_as_MySQLdb()
    
  • RuntimeError: 'cryptography' package is required for sha256_password or caching_sha2_password auth methods

    $ pip install cryptography
    
  • MySQL django.db.utils.OperationalError : (1045, " 'root'@ 'localhost'์‚ฌ์šฉ์ž์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๊ฐ€ ๊ฑฐ๋ถ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค (์•”ํ˜ธ ์‚ฌ์šฉ : YES)")

    settings.py์˜ DATABASE_PASSWORD ์žฌํ™•์ธ

์ƒˆ๋กœ ์•Œ๊ฒŒ๋œ ์ 

  • OneToOneField

    ์ผ๋Œ€์ผ ๊ด€๊ณ„๋กœ unique=True๋ฅผ ์ด์šฉํ•ด์„œ ๋งŒ๋“  ForeignKey์™€ ๋น„์Šทํ•˜์ง€๋งŒ ๋‹จ์ผ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ๋ฆฌํ„ดํ•˜๋Š” ์—ญ์ฐธ์กฐ๋ผ๋Š” ์ ์ด ๋‹ค๋ฅด๋‹ค.

  • DateField์—์„œ default=datetime.date.today()๋ฅผ ์ผ๋”๋‹ˆ ์•„๋ž˜์™€ ๊ฐ™์€ ๊ฒฝ๊ณ ๊ฐ€ ๋‚˜ํƒ€๋‚ฌ๋‹ค.

    It seems you set a fixed date / time / datetime value as default for this field. This may not be what you want. If you want to have the current date as default, use `django.utils.timezone.now`
    

    ๊ทธ๋ž˜์„œ from django.utils import timezone๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ timezone.localtime() ํ˜•์‹์œผ๋กœ ๋ฐ”๊พธ๊ธด ํ–ˆ๋Š”๋ฐ ์™œ ์ด๋ ‡๊ฒŒ ํ•ด์•ผ ํ•˜๋Š”์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ๋‹ค.

๋А๋‚€ ์ 

DB ์„ค๊ณ„๋ฅผ ๋„ˆ๋ฌด ์˜ค๋žœ๋งŒ์— ํ•ด๋ด์„œ ๊ฐ์ด ์ž˜ ์•ˆ์žกํ˜”๋‹ค. create/update ์‹œ๊ฐ„๋„ ํ•„๋“œ๋กœ ์ถ”๊ฐ€ํ–ˆ์–ด์•ผ ํ–ˆ๋Š”๋ฐ ์žŠ์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  following/follower๋ฅผ ๊ตฌ์กฐ ์ƒ ์–ด๋–ป๊ฒŒ ํ‘œํ˜„ํ•ด์•ผ ํ• ์ง€ ๋ชจ๋ฅด๊ฒ ์–ด์„œ ๋‚ด ์ƒ๊ฐ๋Œ€๋กœ ํ•ด๋ดค๋Š”๋ฐ ์•„๋งˆ ํ‹€๋ฆฐ ๊ฒƒ ๊ฐ™๋‹ค. ๐Ÿฅฒ ์ฑ… ์ข€ ์ฝ๊ณ  ๊ณต๋ถ€ํ•ด์•ผ๊ฒ ๋‹ค!


3์ฃผ์ฐจ ๋ฏธ์…˜ : DRF1 - Serializer ๋ฐ API ์„ค๊ณ„

๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์ˆ˜์ •

image

2์ฃผ์ฐจ ๊ณผ์ œ ์ฝ”๋“œ๋ฆฌ๋ทฐ ๋•Œ ๋ง์”€ํ•ด์ฃผ์…จ๋˜ ์ ๋“ค์„ ๋ฐ˜์˜ํ•˜์—ฌ ๊ตฌ์กฐ๋ฅผ ์ˆ˜์ •ํ–ˆ๋‹ค.

class BaseModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True, null=True)
    updated_at = models.DateTimeField(auto_now=True)
    deleted_at = models.DateTimeField(null=True, default=None)
    is_deleted = models.BooleanField(default=False)

    class Meta:
        abstract = True

    def delete(self, using=None, keep_parents=False):
        self.is_deleted = True
        self.deleted_at = timezone.now()
        self.save()

BaseModel Class๋ฅผ ๋งŒ๋“ค์–ด ๋‹ค๋ฅธ ๋ชจ๋ธ์—์„œ๋„ ๋ฐ˜๋ณต์ ์œผ๋กœ ํ•„์š”ํ•œ ๋ณ€์ˆ˜๋“ค์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ๋‹ค.

๋ฐ์ดํ„ฐ ์‚ฝ์ž…

image image

  • ์ถ”๊ฐ€๋œ ๋ฐ์ดํ„ฐ

    • Category: study, play
    • Todo: django study, code review, lets go sinchon
  • mysql๋กœ ํ™•์ธ

    image

๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API

  • URL: api/todo
  • METHOD: GET image

ํŠน์ • ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API

  • URL: api/todo/int:pk
  • METHOD: GET image

์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ createํ•˜๋„๋ก ์š”์ฒญํ•˜๋Š” API

  • URL: api/todo

  • METHOD: POST

  • BODY

    {
      "user": "์œ ์ € ID",
      "category": "์นดํ…Œ๊ณ ๋ฆฌ ID",
      "content": "TODO ๋‚ด์šฉ"
    }

    image

    deadline์„ ์ง€์ •ํ•˜์ง€ ์•Š์•„๋„ ๊ดœ์ฐฎ์ง€๋งŒ models.py์—์„œ field์™€ default์˜ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•˜์—ฌ ์—๋Ÿฌ๊ฐ€ ๋‚˜ ์ด๋ฒˆ์—๋งŒ ์„ค์ •ํ•ด์ฃผ์—ˆ๋‹ค. ์ถ”ํ›„์— ์ˆ˜์ • ์˜ˆ์ •

ํŠน์ • ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œ ๋˜๋Š” ์—…๋ฐ์ดํŠธ ํ•˜๋Š” API

์‚ญ์ œ

  • URL: api/todos/< int:pk >

  • METHOD: DELETE

    image

    ์‚ญ์ œ ๊ฒฐ๊ณผ

    image

์—…๋ฐ์ดํŠธ

  • URL: api/todo/< int:pk >

  • METHOD: POST

  • BODY

    {
      "user": "์œ ์ € ID",
      "category": "์นดํ…Œ๊ณ ๋ฆฌ ID",
      "์ˆ˜์ •์„ ์›ํ•˜๋Š” ํ•„๋“œ"
    }

    image

    user์™€ category๋ฅผ body ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๊ณ  api๋ฅผ ์š”์ฒญํ•˜์˜€๋”๋‹ˆ ํ•„์ˆ˜๊ฐ’์ด๋ผ๊ณ  ์—๋Ÿฌ๊ฐ€ ๋‚ฌ๋‹ค. ์•ˆํ•ด๋„ ์ƒ๊ด€ ์—†๋Š” ๊ฒƒ์œผ๋กœ ์•„๋Š”๋ฐ ํ™•์ธ ํ•„์š”!

    serializer = TodoSerializer(instance=todo, data=data, partial=True)

    serializer์— partial=True์„ ์ถ”๊ฐ€ํ•˜์—ฌ ํ•ด๊ฒฐ

์—๋Ÿฌ ํ•ด๊ฒฐ

  • BaseModel์˜ created_at

    image

    ์ด๋•Œ created_at์— ๊ทธ๋ƒฅ auto_now_add=True๋งŒ ์ง€์ •ํ•ด์ฃผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด default๋ฅผ ์ถ”๊ฐ€ํ•˜๋ผ๋Š” ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜์˜จ๋‹ค.

    image

    ๊ทธ๋ž˜์„œ default๋ฅผ ์ง€์ •ํ•ด์ฃผ๋ฉด ๋‘˜ ์ค‘์— ํ•˜๋‚˜๋งŒ ์“ฐ๋ผ๊ณ  ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ์ถœ๋ ฅ๋˜์–ด null=True์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์šฐ์„  ํ•ด๊ฒฐํ•ด์ฃผ์—ˆ๋‹ค.

  • DELETE DELETE ์š”์ฒญ ์‹œ์— ๋ฐœ์ƒ

    image

    TypeError: __init__() missing 1 required positional argument: 'data'
    

    ์—๋Ÿฌ๊ฐ€ ๋‚˜์ง€๋งŒ DB๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด ์–ด์ฐŒ๋๋“  ์ง€์›Œ์ ธ ์žˆ์—ˆ๋‹ค. ๊ตฌ๊ธ€๋งํ•ด๋ด๋„ ์ž˜ ๋ชจ๋ฅด๊ฒ ์–ด์„œ ๋” ์ฐพ์•„๋ณด๊ณ  ์ˆ˜์ •ํ•ด์•ผ ํ•œ๋‹ค.

    โ†’ JsonResponse๋ฅผ Response๋กœ ์ˆ˜์ •ํ•˜์—ฌ ํ•ด๊ฒฐ!

  • safe

    TypeError: In order to allow non-dict objects to be serialized set the safe parameter to False.
    

    GET ์š”์ฒญ ์‹œ์— ์ž๊พธ ๋ฐœ์ƒํ–ˆ๋˜ ์—๋Ÿฌ์ด๋‹ค. views.py์—์„œ ๊ฐ api์˜ ๋ฆฌํ„ด ๊ฐ’์— safe๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

    return JsonResponse(serializer.data, safe=False)

ํšŒ๊ณ 

๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋œ ๊ธฐ๋ถ„! ๋„ˆ๋ฌด ์žฌ๋ฐŒ์—ˆ๋‹ค ๐Ÿค“ ์ฒ˜์Œ์— urls.py์— ๋‚ด๊ฐ€ ์ง  todo path๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๊ฒƒ์„ ์ƒ์œ„ url conf์—์„œ ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ์ด๋Ÿฐ ๋ฐ”๋ณด ๊ฐ™์€ ์‹ค์ˆ˜๋Š” ๋„๋Œ€์ฒด ์–ธ์ œ ๋๋‚˜๋Š”๊ฑด์ง€..

๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค์–ด์„œ ์ƒ์† ๋ฐ›๋Š” ๊ด€๊ณ„๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ๋‚˜์„œ ์ฝ”๋“œ๋ฅผ ์งœ๋ ค๊ณ  ํ•˜๋‹ˆ๊นŒ serializer์—์„œ๋„ ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์ธ๊ฐ€ ๊ณ ๋ฏผ์ด ์žˆ์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์„ธ์…˜ ๋•Œ ์•Œ๋ ค์ฃผ์‹  SerializerMethodField๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ์ข€ ํ•˜๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ์—๋Ÿฌ๊ฐ€ ์ƒ๊ฒจ์„œ ์šฐ์„  ์ฃผ์„์ฒ˜๋ฆฌ ํ•ด๋†จ๋‹ค.๐Ÿ˜ข

์ด๋ฒˆ ๊ณผ์ œ์—์„œ ๋ชจ๋ฅด๋Š” ๋ถ€๋ถ„๋“ค์„ ๋งŽ์ด ๋ฐœ๊ฒฌํ•ด์„œ ๋‹ต๋‹ตํ•˜๊ธฐ๋„ ํ–ˆ์ง€๋งŒ ๊ณต๋ถ€ํ•  ๊ฒƒ๋“ค์„ ์ฐพ์€ ๊ฒƒ ๊ฐ™์•„ ์ข‹์•˜๋‹ค!


4์ฃผ์ฐจ : DRF2 - API View & Viewset & Filter

์ €๋ฒˆ ์ฃผ์ฐจ์™€ ๋น„๊ตํ–ˆ์„ ๋•Œ ๋‹ฌ๋ผ์ง„ ์ ๋“ค:

  • url ํ˜•ํƒœ: todo/ โ†’ todos/
  • ํŠน์ • ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ๋ฉ”์†Œ๋“œ: PUT โ†’ PATCH
  • BaseModel์—์„œ ์‚ญ์ œ ์—ฌ๋ถ€์™€ ์‹œ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋˜ is_deleted์™€ deleted_at ํ•„๋“œ ์ค‘ is_deleted ์ œ๊ฑฐ
  • migration ํŒŒ์ผ๋“ค git์— ์ถ”๊ฐ€

DRF API View ์˜ CBV ์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜๊ธฐ

๊ธฐ์กด์— FBV(Function-Based View)๋กœ ์ฝ”๋”ฉํ–ˆ๋˜ ๋‚ด์šฉ์„ CBV(Class-Based View)๋กœ ์ˆ˜์ •ํ•˜์˜€๋‹ค.

views.py refactoring ์ „/ํ›„

# FBV
@csrf_exempt
@api_view(['GET', 'POST'])
def todo_list(request):
    if request.method == 'GET':
        todos = Todo.objects.filter(deleted_at=None)
        serializer = TodoSerializer(todos, many=True)
        return JsonResponse(serializer.data, safe=False, status=200)
# CBV
class TodoList(APIView):
    def get(self, request):
        todos = Todo.objects.filter(deleted_at=None)
        serializer = TodoSerializer(todos, many=True)
        return JsonResponse(serializer.data, safe=False, status=200)

urls.py refactoring ์ „/ํ›„

# FBV
urlpatterns = [
    path('todos/', views.todo_list, name="todo_list"),
    path('todos/<int:pk>', views.todo_detail, name="todo_detail"),
]
# CBV
urlpatterns = [
    path('todos/', TodoList.as_view()),
    path('todos/<int:pk>', TodoDetail.as_view()),
]

Viewset์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜๊ธฐ

views.py refactoring ํ›„

class TodoViewSet(viewsets.ModelViewSet):
    serializer_class = TodoSerializer
    queryset = Todo.objects.all()

urls.py refactoring ํ›„ (Router ์‚ฌ์šฉํ•˜์—ฌ url mapping)

router = routers.DefaultRouter()
router.register(r'todos', TodoViewSet)

urlpatterns = router.urls

filter ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

  • ํŠน์ • user filtering
  • content์— ํŠน์ • ๋ฌธ์ž์—ด ํฌํ•จ๋˜๋Š”์ง€ ํŒ๋ณ„ํ•˜์—ฌ filtering
class TodoFilter(FilterSet):
    user = filters.CharFilter(method='user_filter')
    content = filters.CharFilter(field_name='content', lookup_expr='icontains')

    class Meta:
        model = Todo
        fields = ['user', 'content']

    def user_filter(self, queryset, user, value):
        filtered_queryset = queryset.filter(**{
            user: value,
        })
        return filtered_queryset


class TodoViewSet(viewsets.ModelViewSet):
    serializer_class = TodoSerializer
    queryset = Todo.objects.all()

    filter_backends = [DjangoFilterBackend]
    filterset_class = TodoFilter

user filter

url: /api/todos/?user=''

image

content filter

url: /api/todos/?content=''

image

user & content filter

url: /api/todos/?user=''&content=''

image

์—๋Ÿฌ ํ•ด๊ฒฐ

  • Field ์‚ญ์ œ ์—๋Ÿฌ

    image

    is_deleted ํ•„๋“œ๋ฅผ ์‚ญ์ œํ•˜๊ณ  deleted_at์œผ๋กœ๋งŒ ์‚ญ์ œ ์—ฌ๋ถ€์™€ ์‹œ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋„๋ก models.py๋ฅผ ์ˆ˜์ •ํ•˜์˜€๋‹ค. ํŒŒ์ผ ์ˆ˜์ • ํ›„์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ํ–ˆ๋Š”๋ฐ๋„ DB์—๋Š” ๋ฐ˜์˜์ด ๋˜์ง€ ์•Š์•„ ์•„์ง ํ•„๋“œ๊ฐ€ ๋‚จ์•„์žˆ์–ด ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜์˜€๋‹ค. mysql๋กœ ๋“ค์–ด๊ฐ€ ALTER TABLE ํ…Œ์ด๋ธ”๋ช… DROP ์ปฌ๋Ÿผ๋ช…;๋กœ ํ•„๋“œ๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ์‚ญ์ œํ•˜์—ฌ ํ•ด๊ฒฐ

  • Todo TypeError

    image

    ํŠน์ • ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•  ๋•Œ ๋ฐœ์ƒํ–ˆ๋˜ ์—๋Ÿฌ๋กœ get_object_or_404๋ฅผ objects.filter๋กœ ์ˆ˜์ •ํ•˜์—ฌ ํ•ด๊ฒฐ

ํšŒ๊ณ 

๊ณผ์ œํ•˜๋ ค๊ณ  ๋ณด๋‹ˆ๊นŒ ๋ถ„๋ช… ์›”์š”์ผ๊นŒ์ง€๋งŒ ํ•ด๋„ ์žˆ๋˜ migration file๋“ค์ด ๋‹ค ๋‚ ๋ผ๊ฐ€์„œ ๊ฐ„๋‹ด์ด ์„œ๋Š˜ํ–ˆ๋‹ค. git์— migration file๋“ค์„ ๊ตณ์ด ์˜ฌ๋ฆด ํ•„์š”๊ฐ€ ์žˆ๋‚˜..? ์‹ถ์–ด์„œ ์•ˆ์˜ฌ๋ ธ์—ˆ๋Š”๋ฐ ์ด์ œ ๊ผฌ๋ฐ•๊ผฌ๋ฐ• ์˜ฌ๋ ค์•ผ๊ฒ ๋‹ค. ํŒŒ์ผ๋“ค์ด ๋‹ค ๋‚ ๋ผ๊ฐ”์–ด๋„ DB ์—ฐ๊ฒฐ์€ ์ž˜ ๋˜์–ด์žˆ๊ณ  migration ๊ธฐ๋ก๋“ค์„ ๋ณด๋ฉด ์•„์ง ๋‹ค ์žˆ๋Š”๋ฐ ์™œ ๋‚ด ๋กœ์ปฌ์—์„œ๋งŒ ์‚ฌ๋ผ์ง„๊ฑด์ง€ ์ •๋ง ์˜๋ฌธ ๐Ÿค” ๊ทธ๋ฆฌ๊ณ  ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›๊ณ ์„œ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ๋˜ ๋ถ€๋ถ„๋“ค์ด ์˜ˆ์ƒ์น˜ ๋ชปํ•˜๊ฒŒ ์—๋Ÿฌ๊ฐ€ ๋‚˜์„œ ์™œ ๊ทธ๋Ÿฌ๋Š”๊ฑด์ง€๋„ ๊ฐ์ด ์•ˆ์žกํžŒ๋‹ค. ์šฐ์„  ์ฃผ๋จน๊ตฌ๊ตฌ์‹์œผ๋กœ ํ•ด๊ฒฐ..

CBV์™€ ViewSet ๋ชจ๋‘ ์ฒ˜์Œ ์‚ฌ์šฉํ•ด๋ณด๋Š”๋ฐ ์ •๋ง ์‹ ์„ธ๊ณ„์˜€๋‹ค. ํŠนํžˆ ViewSet ์–ด๋–ป๊ฒŒ ์ด๋ ‡๊ฒŒ ๊ฐ„ํŽธํ•  ์ˆ˜๊ฐ€..! ๊ทผ๋ฐ ์˜คํžˆ๋ ค ์ฒ˜์Œ ๋ฐฐ์šธ ๋•Œ ViewSet์œผ๋กœ ํ–ˆ์œผ๋ฉด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”๊ฑด์ง€ ๋ชฐ๋ผ์„œ ํ—ท๊ฐˆ๋ ธ์„ ๊ฒƒ ๊ฐ™๋‹ค. filterset๋„ ์ต์ˆ™ํ•˜์ง€๊ฐ€ ์•Š์•„์„œ deleted_at์ด Null์ด ์•„๋‹Œ ๋ฐ์ดํ„ฐ๋“ค๋งŒ ๊ฐ€์ ธ์˜ค๋Š” ํ•„ํ„ฐ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ๋งŒ๋“ค๋‹ค๊ฐ€ ํฌ๊ธฐํ–ˆ๋‹ค ๐Ÿ™ƒ ์–ด์จŒ๋“  ๋„ˆ๋ฌด๋„ˆ๋ฌด ํŽธํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์•Œ๊ฒŒ ๋˜์–ด์„œ ์žฌ๋ฐŒ์—ˆ๋‹ค!


5์ฃผ์ฐจ : DRF3 - Simple JWT

๋กœ๊ทธ์ธ ์ธ์ฆ ๋ฐฉ์‹์—๋Š” ์–ด๋–ค ๊ฒƒ์ด ์žˆ์„๊นŒ?

+) ์ธ์ฆ์„ ํ•ด์•ผํ•˜๋Š” ์ด์œ 

HTTP๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ stateless, connectionlessํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋“  ์š”์ฒญ(Request)์ด ์ด์ „ ์š”์ฒญ๊ณผ ๋…๋ฆฝ์ ์œผ๋กœ ๋‹ค๋ค„์ง„๋‹ค. ์š”์ฒญ์ด ๋๋‚  ๋•Œ๋งˆ๋‹ค ์„œ๋ฒ„๋Š” ์œ ์ €์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์žŠ์–ด๋ฒ„๋ฆฌ๊ฒŒ ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์š”์ฒญ ์‹œ๋งˆ๋‹ค ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„์— ์ธ์ฆ์„ ํ•ด์•ผ ํ•œ๋‹ค.

Header

HTTP Request Header์— ์ธ์ฆ ์ˆ˜๋‹จ์ธ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ง์ ‘ ๋„ฃ๋Š” ๋ฐฉ์‹์ด๋‹ค. ๋ณดํ†ต ์„œ๋ฒ„๋กœ HTTP ์š”์ฒญ์„ ํ•  ๋•Œ ์•”ํ˜ธํ™”๋ฅผ ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์ ์œผ๋กœ ๋งค์šฐ ์น˜๋ช…์ ์ด๋‹ค. ๋งŒ์•ฝ ํ•ด์ปค๊ฐ€ HTTP ์š”์ฒญ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค๋ฉด ์‚ฌ์šฉ์ž์˜ ๊ณ„์ • ์ •๋ณด๋ฅผ ์‰ฝ๊ฒŒ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

  • ์žฅ์ 
    • ์ธ์ฆ ํ…Œ์ŠคํŠธ ๋•Œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • ๋‹จ์ 
    • ๋ณด์•ˆ ๋งค์šฐ ์ทจ์•ฝ
    • ์š”์ฒญ ์‹œ๋งˆ๋‹ค ์„œ๋ฒ„์— ID, PW ๋Œ€์กฐ ํ•„์š”

Session, Cookie

*Session: ์„œ๋ฒ„๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์ •๋ณด *Cookie: ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐœ๊ธ‰๋œ ์„ธ์…˜์„ ์—ด๊ธฐ ์œ„ํ•œ ์—ด์‡ (Session ID)

Session, Cookie ๋ฐฉ์‹์€ Session ID๋ฅผ ๋งŒ๋“œ๋Š” ์„ธ์…˜ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. Session ID๋Š” ๋กœ๊ทธ์ธ์„ ํ–ˆ์„ ๋•Œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” ๊ฒƒ์œผ๋กœ HTTP Header์— ์‹ค๋ ค ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด๋‚ด์ง„๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๋ณด๊ด€ํ•˜๊ณ  ์žˆ๋˜ ์ฟ ํ‚ค๋ฅผ ์ธ์ฆ์ด ํ•„์š”ํ•œ ์š”์ฒญ์— ๋„ฃ์–ด ๋ณด๋‚ด๊ณ  ์„œ๋ฒ„๋Š” ์„ธ์…˜ ์ €์žฅ์†Œ์—์„œ ์ฟ ํ‚ค์™€ ๊ธฐ์กด ์ •๋ณด๋ฅผ ๋น„๊ตํ•˜์—ฌ ์ธ์ฆํ•œ๋‹ค. ์„ธ์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆํ•˜์—ฌ ์ฑ…์ž„์„ ์„œ๋ฒ„๊ฐ€ ์ง€๊ฒŒ ํ•œ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.(์‚ฌ์šฉ์ž๋ณด๋‹ค๋Š” ์„œ๋ฒ„ ํ•ดํ‚น์ด ๋” ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ)

  • ์žฅ์ 
    • Header ๋ฐฉ์‹๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ HTTP ์š”์ฒญ์ด ๋…ธ์ถœ๋˜๋”๋ผ๋„ ์•ˆ์ „ํ•˜๋‹ค. ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋Š” ์„ธ์…˜ ์ €์žฅ์†Œ์— ์ €์žฅ๋˜๊ณ  HTTP ์š”์ฒญ์— ๋“ค์–ด์žˆ๋Š” ์ฟ ํ‚ค ์ž์ฒด๋Š” ์œ ์˜๋ฏธํ•œ ์ •๋ณด๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
    • ์‚ฌ์šฉ์ž๋Š” ๊ฐ๊ฐ ๊ณ ์œ ํ•œ Session ID๋ฅผ ๋ฐœ๊ธ‰ ๋ฐ›์•„ ํšŒ์› ์ •๋ณด ํ™•์ธ์ด ๋งค๋ฒˆ ํ•„์š”ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„ ์ž์›์— ์ ‘๊ทผ์ด ์šฉ์ดํ•˜๋‹ค.
  • ๋‹จ์ 
    • Session Hijacking ๊ณต๊ฒฉ ๊ฐ€๋Šฅ ์„ธ์…˜์„ ๊ฐ€๋กœ์ฑ„์„œ ๋ณ„๋„์˜ ์ธ์ฆ ์ž‘์—… ์—†์ด ์„ธ์…˜์„ ํ†ตํ•ด ํ†ต์‹ ์„ ๊ณ„์†ํ•˜๋Š” ํ–‰์œ„๋ฅผ ๋งํ•œ๋‹ค. HTTPS ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์„ธ์…˜์— ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์„ค์ •ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•˜๋‹ค.
    • ์„ธ์…˜ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ์ €์žฅ๊ณต๊ฐ„์ด ํ•„์š”ํ•˜๋‹ค.

Access Token (JWT)

  • ์žฅ์ 
    • ์„ธ์…˜ ์ฟ ํ‚ค ๋ฐฉ์‹๊ณผ ๋‹ฌ๋ฆฌ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ์ €์žฅ๊ณต๊ฐ„์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค.
    • Google, Facebook๊ณผ ๊ฐ™์€ ๋‹ค์–‘ํ•œ ํ† ํฐ ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค๋กœ ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•˜๊ธฐ ์šฉ์ดํ•˜๋‹ค
    • ์„œ๋ช…์—๋Š” ์†ก์‹ ์ž์™€ ์†ก์‹ ํ•œ ์ •๋ณด๋“ค์— ๋Œ€ํ•œ ๋‚ด์šฉ์ด ํฌํ•จ๋˜์–ด ์žˆ์–ด ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ์กฐ์ž‘ ๋ฐ ๋ณ€์กฐ ์—ฌ๋ถ€๋ฅผ ์•Œ์•„๋‚ผ ์ˆ˜ ์žˆ๋‹ค.
  • ๋‹จ์ 
    • Token์ด ๋ฐœ๊ธ‰๋˜๋ฉด ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์ „๊นŒ์ง€ ๊ณ„์† ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์„ธ์…˜ ์ฟ ํ‚ค ๋ฐฉ์‹๊ณผ ๊ฐ™์ด ํ•ด์ปค๊ฐ€ ํ† ํฐ์„ ๊ฐ€๋กœ์ฑ„์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. Refresh Token์„ ๋ฐœ๊ธ‰ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•˜๋‹ค.
    • Payload๋Š” ๋”ฐ๋กœ ์•”ํ˜ธํ™”ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋‹ด์„ ์ˆ˜ ์žˆ๋Š” ์ •๋ณด๊ฐ€ ์ œํ•œ์ ์ด๋‹ค.
    • Token์˜ ๊ธธ์ด๊ฐ€ ๊ธธ์–ด ์š”์ฒญ์ด ๋งŽ์•„์งˆ์ˆ˜๋ก ์„œ๋ฒ„์˜ ์ž์› ๋‚ญ๋น„๊ฐ€ ์ƒ๊ธด๋‹ค.

Access Token, Refresh Token

*Refresh Token: Access Token๊ณผ ๊ฐ™์€ ํ˜•ํƒœ์˜ JWT์ด๋‹ค. Access Token๋ณด๋‹ค ๊ธด ์œ ํšจ๊ธฐ๊ฐ„์„ ๊ฐ€์ง€๋ฉฐ Access Token ๋งŒ๋ฃŒ ์‹œ์— ์ƒˆ๋กœ ๋ฐœ๊ธ‰์„ ๋„์™€์ค€๋‹ค.

Refresh Token์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ž์ฃผ ๋กœ๊ทธ์ธ์„ ํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด๋‚˜ ์žฅ๊ธฐ๊ฐ„ ๋กœ๊ทธ์ธํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ๋ณด์•ˆ์  ๋ฌธ์ œ์ ๋“ค์„ ํ•ด๊ฒฐํ•˜์˜€๋‹ค.

  • ์žฅ์ 
    • ์œ ํšจ ๊ธฐ๊ฐ„์ด ๋” ์งง๊ธฐ ๋•Œ๋ฌธ์— Access Token๋งŒ ๋‹จ๋…์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋ณด๋‹ค ๋ณด์•ˆ์ ์œผ๋กœ ๋”์šฑ ์•ˆ์ „ํ•˜๋‹ค.
  • ๋‹จ์ 
    • ๊ตฌํ˜„์ด ๋ณต์žกํ•˜๋‹ค.
    • ์„œ๋ฒ„์˜ ์ž์› ๋‚ญ๋น„๊ฐ€ ์ƒ๊ธด๋‹ค.

OAuth 2.0

*OAuth 2.0(Open Authorization): ์ธ์ฆ์„ ์œ„ํ•œ ๊ฐœ๋ฐฉํ˜• ํ‘œ์ค€ ํ”„๋กœํ† ์ฝœ

  • ์žฅ์ 
    • ์ง์ ‘ ํƒ€์‚ฌ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์•ˆ์ •์ ์ด๋‹ค.
    • ํšŒ์› ์ •๋ณด๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ธฐํƒ€ API์— ๋Œ€ํ•œ ์ •๋ณด์—๋„ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ๋‹จ์ 
    • ๊ตฌํ˜„์ด ๋งค์šฐ ๋ณต์žกํ•˜๋‹ค.

์ฐธ๊ณ ๋งํฌ1

์ฐธ๊ณ ๋งํฌ2

์ฐธ๊ณ ๋งํฌ3

JWT(JSON Web Token)๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

ํ†ต์‹  ์–‘์ž๊ฐ„์˜ ์ •๋ณด๋ฅผ JSON ํ˜•์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ์•ˆ์ „ํ•˜๊ฒŒ ์ „์†กํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์ด๋‹ค. JWT๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์ธ์ฆ(Authentication)๊ณผ ๊ถŒํ•œ๋ถ€์—ฌ(Authorization)์— ์‚ฌ์šฉ๋˜๋Š”๋ฐ ์ด๋•Œ ํ•„์š”ํ•œ ์ •๋ณด๋“ค์„ ์•”ํ˜ธํ™”์‹œํ‚จ JSON ํ† ํฐ์ด๋‹ค. ์ธ์ฆ ์ ˆ์ฐจ๋ฅผ ๊ฑฐ์ณ์„œ ์„œ๋ฒ„์—์„œ JWT๋ฅผ ๋ฐœ๊ธ‰ํ•ด์ฃผ๋ฉด ์ด๋ฅผ ์ž˜ ๋ณด๊ด€ํ•˜๊ณ  ์žˆ๋˜ ํด๋ผ์ด์–ธํŠธ๊ฐ€ API ์‚ฌ์šฉ๊ณผ ๊ฐ™์„ ๋•Œ์— ์„œ๋ฒ„์— JWT๋ฅผ ์ œ์ถœํ•˜์—ฌ ์ธ๊ฐ€๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.

JSON ๋ฐ์ดํ„ฐ๋ฅผ Base64 URL-safe Encode ๋ฅผ ํ†ตํ•ด ์ธ์ฝ”๋”ฉํ•˜์—ฌ ์ง๋ ฌํ™”ํ•œ ๊ฒƒ์ด๋ฉฐ, ํ† ํฐ ๋‚ด๋ถ€์—๋Š” ์œ„๋ณ€์กฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ๊ฐœ์ธํ‚ค๋ฅผ ํ†ตํ•œ ์ „์ž์„œ๋ช…๋„ ๋“ค์–ด์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ์šฉ์ž๊ฐ€ JWT ๋ฅผ ์„œ๋ฒ„๋กœ ์ „์†กํ•˜๋ฉด ์„œ๋ฒ„๋Š” ์„œ๋ช…์„ ๊ฒ€์ฆํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์น˜๊ฒŒ ๋˜๋ฉฐ ๊ฒ€์ฆ์ด ์™„๋ฃŒ๋˜๋ฉด ์š”์ฒญํ•œ ์‘๋‹ต์„ ๋Œ๋ ค์ค€๋‹ค.

  • JWT ๊ตฌ์กฐ image

    • Header
      • alg: ์„œ๋ช… ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜(ex: HMAC SHA256, RSA)
      • typ: ํ† ํฐ ์œ ํ˜•
    • Payload ํ† ํฐ์—์„œ ์‚ฌ์šฉํ•  ์ •๋ณด์˜ ์กฐ๊ฐ๋“ค์ธ Claim์ด ๋‹ด๊ฒจ์žˆ์Œ *Claim: key-value ํ˜•์‹์œผ๋กœ ์ด๋ฃจ์–ด์ง„ ํ•œ ์Œ์˜ ์ •๋ณด
    • Signature ์‹œ๊ทธ๋‹ˆ์ฒ˜์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ํ—ค๋”์—์„œ ์ •์˜ํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ฐฉ์‹(alg)์„ ํ™œ์šฉ ์‹œ๊ทธ๋‹ˆ์ฒ˜์˜ ๊ตฌ์กฐ๋Š” (ํ—ค๋” + ํŽ˜์ด๋กœ๋“œ)์™€ ์„œ๋ฒ„๊ฐ€ ๊ฐ–๊ณ  ์žˆ๋Š” ์œ ์ผํ•œ key ๊ฐ’์„ ํ•ฉ์นœ ๊ฒƒ์„ ํ—ค๋”์—์„œ ์ •์˜ํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ์•”ํ˜ธํ™”

์ฐธ๊ณ ๋งํฌ1

์ฐธ๊ณ ๋งํฌ2

JWT ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ

  1. Custom User Model ์‚ฌ์šฉ
  # models.py
  class User(AbstractBaseUser):
      email = models.EmailField(max_length=30, unique=True)
      nickname = models.CharField(max_length=10)
      password = models.CharField(max_length=30)
      introduce = models.CharField(max_length=200)
      image = models.TextField(blank=True)
      is_public = models.BooleanField(default=False)
      search = models.BooleanField(default=False)
  
      is_active = models.BooleanField(default=True)
      is_superuser = models.BooleanField(default=False)
  
      objects = UserManager()
      USERNAME_FIELD = 'email'
  
      class Meta:
          db_table = "User"
  
      def __str__(self):
          return self.nickname
  
      @property
      def is_staff(self):
          return self.is_superuser

Django์˜ ๊ธฐ๋ณธ ์œ ์ € ๋ชจ๋ธ์—์„œ AbstractBaseUser๋ฅผ ์ƒ์†๋ฐ›์•„ ์ปค์Šคํ…€ ๋ชจ๋ธ๋กœ ๋ณ€ํ™”์‹œ์ผฐ๋‹ค. is_superuser๋กœ ๊ด€๋ฆฌ์ž ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋ฉฐ user, superuser๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋Š” UserManager์— ์ถ”๊ฐ€ํ•˜์˜€๋‹ค.

  1. ํšŒ์›๊ฐ€์ž… ๊ตฌํ˜„

    # serializers.py
    class JoinSerializer(serializers.ModelSerializer):
     email = serializers.EmailField(required=True)
     password = serializers.CharField(write_only=True, required=True)
     password2 = serializers.CharField(write_only=True, required=True)
    
     class Meta:
         model = User
         fields = ('email', 'nickname', 'password', 'password2')
    
     def validate(self, request):
         if request['password'] != request['password2']:
             raise serializers.ValidationError({"Password doesn't match."})
         return request
    
     def save(self, request):
         user = User.objects.create_user(
             email=self.validated_data['email'],
             nickname=self.validated_data['nickname'],
             password=self.validated_data['password']
         )
    
         return user
    # views.py
    class JoinView(APIView):
     serializer_class = JoinSerializer
    
     def post(self, request):
         serializer = self.serializer_class(data=request.data)
         if serializer.is_valid():
             user = serializer.save(request)
             token = TokenObtainPairSerializer.get_token(user)
             refresh_token = str(token)
             access_token = str(token.access_token)
             res = Response(
                 {
                     "email": user.email,
                     "nickname": user.nickname,
                     "message": "๊ฐ€์ž…์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ด๋ค„์กŒ์Šต๋‹ˆ๋‹ค.",
                     "token": {
                         "access": access_token,
                         "refresh": refresh_token,
                     },
                 },
                 status=status.HTTP_200_OK,
             )
             res.set_cookie("access", access_token, httponly=True)
             res.set_cookie("refresh", refresh_token, httponly=True)
             return res
    
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    image image

  2. ๋กœ๊ทธ์ธ ๊ตฌํ˜„

    # serializers.py
    class LoginSerializer(serializers.Serializer):
     email = serializers.EmailField(required=True)
     password = serializers.CharField(write_only=True, required=True)
    
     def validate(self, request):
         email = request.get('email', None)
         password = request.get('password', None)
    
         if User.objects.filter(email=email).exists():
             user = User.objects.get(email=email)
             if not user.check_password(password):
                 raise serializers.ValidationError({"Wrong Password"})
         else:
             raise serializers.ValidationError({"User doesn't exist."})
    
         token = RefreshToken.for_user(user)
         refresh = str(token)
         access = str(token.access_token)
    
         data = {
             'email': user.email,
             'refresh': refresh,
             'access': access
         }
    
         return data
    # views.py
    class LoginView(APIView):
     serializer_class = LoginSerializer
    
     def post(self, request):
         serializer = self.serializer_class(data=request.data)
    
         if serializer.is_valid(raise_exception=False):
             email = serializer.validated_data['email']
             access = serializer.validated_data['access']
             refresh = serializer.validated_data['refresh']
             # data = serializer.validated_data
             res = Response(
                 {
                     "message": "๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
                     "email": email,
                     "access": access,
                     "refresh": refresh
                 },
                 status=status.HTTP_200_OK,
             )
             return res
    
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    image image image

์—๋Ÿฌ ํ•ด๊ฒฐ

  • Password Column ๊ธธ์ด ์—๋Ÿฌ

    image

    ALTER TABLE [TABLE๋ช…] modify [COLUMN๋ช…] VARCHAR(1000);

    mysql ๋ช…๋ น์–ด๋กœ ํ•ด๋‹น ํ•„๋“œ ๊ธธ์ด ๋Š˜๋ ค์„œ ํ•ด๊ฒฐ

ํšŒ๊ณ 

๋„ˆ๋ฌด ์–ด๋ ค์› ๋‹ค..๐Ÿ˜ฉ ์–ด๋А์ •๋„ ํ•˜๊ณ  ๋‚˜์„œ ๋’ค์— ์–ด๋ ต์ง€ ์•Š๊ฒ ์ง€ํ•˜๊ณ  ์—ฌ์œ ๋กญ๊ฒŒ ํ–ˆ๋Š”๋ฐ ์ด๋ฆฌํ•ด๋„ ์ €๋ฆฌํ•ด๋„ ์•ˆ๋ผ์„œ ๋ช‡๋ฒˆ์ด๋‚˜ ๋‹ค์‹œํ•˜๊ณ  ๊ทธ๋žฌ๋‹ค. ํ•˜ํ•˜. ๋‚ด๊ฐ€ ํ˜ผ์ž ๋А๋ผ๊ธฐ์—๋„ ์ง€๊ธˆ ๋‚ด ์ฝ”๋“œ๊ฐ€ ์ƒ๋‹นํžˆ ๋น„ํšจ์œจ์ ์ด๊ณ  ๋”๋Ÿฌ์šด ๊ฒƒ ๊ฐ™์•„์„œ ๋‹ค์Œ์— ๊ผญ ๋ฆฌํŽ™ํ† ๋ง์„ ํ•˜๊ณ  ์‹ถ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ €๋ฒˆ์— viewset์ด๋‚˜ url์—์„œ router๋ฅผ ์“ฐ๋Š” ์ž‘์—…์„ ํ•˜๋ฉด์„œ ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•ด์กŒ๋Š”๋ฐ ์ด๋ฒˆ ๊ณผ์ œ์—์„œ๋Š” ๋‹ค์‹œ APIView์™€ as_view()๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋‘ ๊ฐ€์ง€ ์ฝ”๋“œ ํ˜•์‹์ด ๊ฐ™์ด ์žˆ๋Š”๊ฒŒ ๋งž๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ๋‹ค. ์šฐ์„  ๋ณด๊ธฐ์— ๊น”๋”ํ•˜์ง€๋Š” ์•Š์€ ๊ฒƒ ๊ฐ™๋‹ค. ์–ผ๋ ๋šฑ๋•… ๊ณผ์ œ ๋ ๐Ÿ˜Ž


6์ฃผ์ฐจ ๋ฏธ์…˜ : Docker ๋ฐฐํฌ ํ™˜๊ฒฝ ๊ตฌ์ถ•

๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ๋„์ปค ์‹คํ–‰

docker-compose -f docker-compose.yml up --build

ํ„ฐ๋ฏธ๋„์—์„œ ์‹คํ–‰ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €์—์„œ 127.0.0.1:8000 ์ ‘์† ํ…Œ์ŠคํŠธ

image

์ ‘์† ์„ฑ๊ณต!

์‹คํ–‰ํ–ˆ์„ ๋•Œ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์—๋Ÿฌ๊ฐ€ ๋งŽ์ด ๋ฐœ์ƒํ–ˆ๋Š”๋ฐ pip list๋กœ requirements.txt์— ์ถ”๊ฐ€๊ฐ€ ํ•„์š”ํ•œ ๋‚ด์šฉ๋“ค ์ฐพ์•„์„œ ์ˆ˜์ •ํ•˜์—ฌ ํ•ด๊ฒฐํ•˜์˜€๋‹ค.

docker-compose -f docker-compose.prod.yml down -v ์ž…๋ ฅํ•˜์—ฌ ์ข…๋ฃŒ

์‹ค ํ™˜๊ฒฝ ๋ฐฐํฌ

.env.prod ์ƒ์„ฑ

DATABASE_HOST={RDS db ์ฃผ์†Œ}
DATABASE_DB=mysql
DATABASE_NAME={RDS ๊ธฐ๋ณธ database ์ด๋ฆ„}
DATABASE_USER={RDS User ์ด๋ฆ„}
DATABASE_PASSWORD={RDS master ๋น„๋ฐ€๋ฒˆํ˜ธ}
DATABASE_PORT=3306
DEBUG=False
DJANGO_ALLOWED_HOSTS={EC2 ์„œ๋ฒ„ ip ์ฃผ์†Œ}
DJANGO_SECRET_KEY={django secret key}

ํ”„๋กœ์ ํŠธ ์ƒ๋‹จ์— ํŒŒ์ผ ์ƒ์„ฑ ํ›„ ๋‚ด๊ฐ€ ๊ตฌ์ถ•ํ•œ ์„œ๋ฒ„ ๋‚ด์šฉ ๋„ฃ๊ธฐ! โ†’ ๊ธฐ์กด .env ํŒŒ์ผ๋ช…์„ ๋ฐ”๊พธ๊ณ  ํ•ด๋‹น .env.prod๋ฅผ .env๋กœ ๋ฐ”๊ฟ”์„œ ์œ„ ๋‚ด์šฉ์ด ์—ฐ๊ฒฐ๋˜๊ฒŒ ํ•จ

github secrets ์„ค์ •

image

  • ENV_VARS: .env.prod ์ „์ฒด ๋ณต์‚ฌ ๋ถ™์—ฌ๋„ฃ๊ธฐ
  • HOST: ๋ฐฐํฌํ•  EC2 ์„œ๋ฒ„ ํผ๋ธ”๋ฆญ DNS(IPv4) ์ฃผ์†Œ
  • KEY: ๋ฐฐํฌํ•  EC2 ์„œ๋ฒ„๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ssh key ์ „๋ฌธ (.pem)

push ํ›„ Actions ํ™•์ธ

image

image

deploy.yml์— branch๋ฅผ master๋กœ ์„ค์ •ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— master branch์—์„œ pushํ–ˆ์„ ๋•Œ ์ž๋™์œผ๋กœ ๋ฐฐํฌ๋œ๋‹ค.

ํ…Œ์ŠคํŠธ API ํ™•์ธ

image

postman์—์„œ ๋ฐฐํฌ๋œ EC2 DNS ์ฃผ์†Œ๋กœ ์ ‘์†ํ•˜์—ฌ api ํ™•์ธ

image

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ณด๋ฉด ์ž˜ ์ €์žฅ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

ํšŒ๊ณ 

์ฒซ ๋ฐฐํฌ๋ฅผ ๋๋ƒˆ๋‹ค!! ๋‚ด๊ฐ€ ํ‹€๋ ค๋„ ๋ญ˜ ํ‹€๋ ธ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ๊ฐ€ ์–ด๋ ค์›Œ์„œ ์ง€๊ธˆ๊นŒ์ง€ ํ–ˆ๋˜ ๊ณผ์ œ ์ค‘์— ์•ˆ๋์„ ๋•Œ ๊ฐ€์žฅ ๋ง‰๋ง‰ํ•˜๊ณ  ํž˜๋“ค์—ˆ๋‹ค.. ์ œ๊ฐ€ ๋ชจ์ž๋ผ์„œ.. ๋ชจ์ž๋ผ์„œ ๊ทธ๋Ÿฝ๋‹ˆ๋‹ค๐Ÿงข ์ด๋Ÿฐ ๋‚˜๋ฅผ ๋๊นŒ์ง€ ๋„์™€์ฃผ์‹  ๋ฏผ์ค€๋‹˜๊ป˜ ๊ฐ์‚ฌ์˜ ๋ง์”€ ์˜ฌ๋ฆฝ๋‹ˆ๋‹ค ๊ทธ์ € ๋น›!

About

CEOS 16th Backend DRF Study

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Python 94.0%
  • Shell 3.9%
  • Dockerfile 2.1%