ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Django] Login(로그인), Comment(댓글) 관련 간단한 endpoint 만들어보기 - 2
    Software, Computer Science/Django, Flask 2020. 2. 10. 10:24

    바로 직전에 이어서 LoginComment 기능을 수정하고자 한다. 멘토님의 조언을 받아 해당 api를 만드는데 있어 아래와 같은 기준을 적용하고자 한다.

    1. 기능과 성격의 연관성을 고려하여 app을 만들자(ex) account 관련 app과 comment 관련 app은 분리해서 구현)

    2. 처음부터 너무 여러가지 경우의 수를 한번에 고려하지 않고 가장 단순하고 최소의 기능만 먼저 완벽하게 구현해보자. 그러나 2~3가지 이상의 복잡한 조건에 대한 기능을 한번에 구현하려면 구현중에 많은 에러와 버그를 만날 가능성이 높아져 효율적인 기능 구현이 어려워 지기 때문에 일단 단일 유형의 계정을 하나 정하고 해당 계정과 비밀번호를 잘 받아오는지를 판단하게끔 구현하는 것으로 방향을 잡는다.

    3. 계정 앱 views.py에는 SignInView, SignUpView를 구현하여 간단히 가입 기능도 추가하여 테스트

    4. 에러처리는 status 유형에 맞춰서 응답만 주는 식으로 하고 성공 및 실패여부 경우의 메세지는 최대한 간단하게(ex> 성공 : 'SUCCESS', 실패 : 'Fail' 등)

     

    기존 app 정리 및 새롭게 app 구성

     

    App 설계를 새로 하게 되어서 project 내의 app을 먼저 정리했다. 그리고 account와 comment 앱을 새로 생성했다.

    또한 git이 설정된 프로젝트이고 app 구조를 전체적으로 수정하여 아래와 같은 과정을 거친 뒤 account와 comment 앱을 새로 만들었다.

     

    1. settings.pyINSTALLED_APP 부분에서 기존에 사용했던 app을 제거
    2. <project-name>/urls.py에서 기존에 사용했던 url을 모두 지워준다
    3. db.sqlite3에 들어가서 사용했던 db테이블을 제거해준다

     

    account
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py
    
    comment
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py
    

     

    새롭게 구성한 앱의 tree구조는 위와 같고 해당 app에 맞춰서 urls.py를 수정해주고 각 app에서도 URLConf 설정을 진행한다.

    # westargram_api/urls.py
    
    from django.urls import path, include
    
    urlpatterns = [
        path('account', include('account.urls')),
        path('comment', include('comment.urls'))
    ]
    
    # westargram_api/settings.py 중에서
    
    INSTALLED_APPS = [
        # 'django.contrib.admin',
        # 'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'account',
        'comment',
    ]
    

     

    먼저 account에서 models.py를 아래와 같이 구성한다. 실제 GET, POST 사용법에 맞춰서 아래와 같이 구현할 수 있다.

     

    # account/models.py
    from django.db import models
    
    class Account(models.Model):
        user_account    = models.CharField(max_length=50),
        password        = models.CharField(max_length=300)
        created_at      = models.DateTimeField(auto_now_add=True),
        updated_at      = models.DateTimeField(auto_now=True)
    
        class Meta:
            db_table = 'user_account'
    
    # account/urls.py
    from django.urls    import path
    from .views         import SignUpView, SignInView
    
    urlpatterns = [
        path('/sign-up', SignUpView.as_view()),
        path('/sign-in', SignInView.as_view())    
    ]
    

     

     

    # comment/models.py
    from django.db import models
    
    class Comment(models.Model):
        user_account    = models.CharField(max_length=50),
        comments        = models.CharField(max_length=700)
        created_at      = models.DateTimeField(auto_now_add=True),
        updated_at      = models.DateTimeField(auto_now=True)
    
        class Meta:
            db_table = 'user_comment'
    
    # comment/urls.py
    from django.urls    import path
    from .views         import CommentView
    
    urlpatterns = [
        path('', CommentView.as_view()),
    ]
    

     

     

    다음으로 views.py를 마저 완성시켜 준다. 기존에 작성했던 api의 로직과는 크게 차이가 없고 app을 나누고 예외처리를 해줄 부분을 해주게 되면 아래와 같다.

     

    # account/views.py
    
    import json
    from django.views   import View
    from django.http    import HttpResponse, JsonResponse
    from .models        import Account
    
    class SignInView(View):
        def post():
            account_data = json.loads(request.body)
    
            try:
                if Account.objects.filter(user_account=account_data['user_account']).exists():
                    account = Account.objects.get(user_account=account_data['user_account'])
    
                    if account.password == account_data['password']:
                        return JsonResponse({'message':'Welcome back!'}, status=200)
                    return HttpResponse(status=401)
                
                return HttpResponse(status=400)
            
            except KeyError:
                return HttpResponse(status=400)
    
    
    class SignUpView(View):
        def post(self, request):
            account_data = json.loads(request.body)
    
            try:
                if not Account.objects.filter(user_account=account_data['user_account']).exists():
                    Account(
                        user_account=account_data['user_account'],
                        password=account_data['password']
                    ).save()
                else:
                    return HttpResponse(status=409)
            except KeyError:
                return HttpResponse(status=400)
    
            return JsonResponse({'message':'SUCCESS'}, status=200)
    

     

    • sign-in : 기존에 user 등록 부분을 적당한 queryset을 활용하여 DB에 데이터가 있는지 확인한 다음 해당 계정 정보가 있다면 password 정보를 활용해서 response를 하게 되어 있으며 그 외의 경우는 status에 맞춰 예외처리를 하였다.
    • sign-up : user가 이미 등록이 되었는지와 제대로 된 회원 가입 정보를 입력했는지 확인한 다음(계정충돌이 일어나면 409 에러를 발생시킴) 없다면 새로 모델 객체를 생성하여 DB 테이블에 저장하게끔 구성했다.

     

    실제로 테스트를 진행하기 위해 python manage.py sqlmigrate 명령어를 실행하여 아래와 같은 작업을 진행

    >>> Account.objects.create(user_account='abc', password=1234)
    >>> Account.objects.all().values()
    <QuerySet [{'id': 1, 'user_account': 'abc', 'password': '1234', 'created_at': datetime.datetime(2020, 2, 8, 11, 20, 11, 842314, tzinfo=<UTC>), 'updated_at': datetime.datetime(2020, 2, 8, 11, 20, 11, 842371, tzinfo=<UTC>)}]>
    >>> Account(user_account='bcd', password=4567).save()
    >>> Account.objects.all().values()
    <QuerySet [{'id': 1, 'user_account': 'abc', 'password': '1234', 'created_at': datetime.datetime(2020, 2, 8, 11, 20, 11, 842314, tzinfo=<UTC>), 'updated_at': datetime.datetime(2020, 2, 8, 11, 20, 11, 842371, tzinfo=<UTC>)}, {'id': 2, 'user_account': 'bcd', 'password': '4567', 'created_at': datetime.datetime(2020, 2, 8, 11, 22, 13, 846972, tzinfo=<UTC>), 'updated_at': datetime.datetime(2020, 2, 8, 11, 22, 13, 847010, tzinfo=<UTC>)}]>
    

     

    * project 경로에 있는 urls.py 설정에 유의 ! <url이름>/ or <url 이름>으로 할 것

    그렇지 않으면 404 NOT Found가 뜨면서 테스트 자체가 안되었다. urlpatterns 명시에 주의. project 상단에서 app까지 어떻게 이어지는지 잘 생각해서 패턴을 명시할 것.

    # comment/views.py
    
    import json
    from django.views   import View
    from django.http    import HttpResponse, JsonResponse
    from .models        import Comment
    
    class CommentView(View):
        def post(self, request):
            comments_data = json.loads(request.body)
            Comment(
                user_account=comments_data['user_account'],
                comments=comments_data['comments']
            ).save()
    
            return JsonResponse({'message':'SUCCESS'}, status=200)
    
        def get(self, request):
            return JsonResponse({'comments':list(Comment.objects.values())}, status=200)
    

    comment 앱 부분도 다음과 같이 명시해 주고 테스트들을 하면 다음과 같은 결과를 얻을 수 있었다.

     

    실제 작동여부에 대한 테스트를 진행

     

    Account 앱 테스트

    $ http -v http://127.0.0.1:8000/account/sign-in user_account=abc password=1234
    POST /account/sign-in HTTP/1.1
    Accept: application/json, */*
    Accept-Encoding: gzip, deflate
    Connection: keep-alive
    Content-Length: 43
    Content-Type: application/json
    Host: 127.0.0.1:8000
    User-Agent: HTTPie/2.0.0
    
    {
        "password": "1234",
        "user_account": "abc"
    }
    
    HTTP/1.1 200 OK
    Content-Length: 28
    Content-Type: application/json
    Date: Sat, 08 Feb 2020 12:18:13 GMT
    Server: WSGIServer/0.2 CPython/3.8.1
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    
    {
        "message": "Welcome back!"
    }
    
    $ http -v http://127.0.0.1:8000/account/sign-in user_account=bcd password=45
    POST /account/sign-in HTTP/1.1
    Accept: application/json, */*
    Accept-Encoding: gzip, deflate
    Connection: keep-alive
    Content-Length: 41
    Content-Type: application/json
    Host: 127.0.0.1:8000
    User-Agent: HTTPie/2.0.0
    
    {
        "password": "45",
        "user_account": "bcd"
    }
    
    HTTP/1.1 401 Unauthorized
    Content-Length: 0
    Content-Type: text/html; charset=utf-8
    Date: Sat, 08 Feb 2020 12:20:35 GMT
    Server: WSGIServer/0.2 CPython/3.8.1
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    
    $ http -v http://127.0.0.1:8000/account/sign-up user_account=abc password=5678
    POST /account/sign-up HTTP/1.1
    Accept: application/json, */*
    Accept-Encoding: gzip, deflate
    Connection: keep-alive
    Content-Length: 43
    Content-Type: application/json
    Host: 127.0.0.1:8000
    User-Agent: HTTPie/2.0.0
    
    {
        "password": "5678",
        "user_account": "abc"
    }
    
    HTTP/1.1 409 Conflict
    Content-Length: 0
    Content-Type: text/html; charset=utf-8
    Date: Sat, 08 Feb 2020 12:21:39 GMT
    Server: WSGIServer/0.2 CPython/3.8.1
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    
    $ http -v http://127.0.0.1:8000/account/sign-up user_account=efg password=5678
    POST /account/sign-up HTTP/1.1
    Accept: application/json, */*
    Accept-Encoding: gzip, deflate
    Connection: keep-alive
    Content-Length: 43
    Content-Type: application/json
    Host: 127.0.0.1:8000
    User-Agent: HTTPie/2.0.0
    
    {
        "password": "5678",
        "user_account": "efg"
    }
    
    HTTP/1.1 200 OK
    Content-Length: 22
    Content-Type: application/json
    Date: Sat, 08 Feb 2020 12:21:53 GMT
    Server: WSGIServer/0.2 CPython/3.8.1
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    
    {
        "message": "SUCCESS"
    }
    

     

    Comment 앱 테스트

    $ http -v http://127.0.0.1:8000/comment user_account=def comments=hajdhklfjasdklfjldsk
    POST /comment HTTP/1.1
    Accept: application/json, */*
    Accept-Encoding: gzip, deflate
    Connection: keep-alive
    Content-Length: 59
    Content-Type: application/json
    Host: 127.0.0.1:8000
    User-Agent: HTTPie/2.0.0
    
    {
        "comments": "hajdhklfjasdklfjldsk",
        "user_account": "def"
    }
    
    HTTP/1.1 200 OK
    Content-Length: 22
    Content-Type: application/json
    Date: Sat, 08 Feb 2020 12:15:46 GMT
    Server: WSGIServer/0.2 CPython/3.8.1
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    
    {
        "message": "SUCCESS"
    }
    
    $ http -v http://127.0.0.1:8000/comment user_account=dgdgd comments=gotohell
    POST /comment HTTP/1.1
    Accept: application/json, */*
    Accept-Encoding: gzip, deflate
    Connection: keep-alive
    Content-Length: 49
    Content-Type: application/json
    Host: 127.0.0.1:8000
    User-Agent: HTTPie/2.0.0
    
    {
        "comments": "gotohome",
        "user_account": "dgdgd"
    }
    
    HTTP/1.1 200 OK
    Content-Length: 22
    Content-Type: application/json
    Date: Sat, 08 Feb 2020 12:16:25 GMT
    Server: WSGIServer/0.2 CPython/3.8.1
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    
    {
        "message": "SUCCESS"
    }
    
    http -v http://127.0.0.1:8000/comment
    GET /comment HTTP/1.1
    Accept: */*
    Accept-Encoding: gzip, deflate
    Connection: keep-alive
    Host: 127.0.0.1:8000
    User-Agent: HTTPie/2.0.0
    
    
    
    HTTP/1.1 200 OK
    Content-Length: 312
    Content-Type: application/json
    Date: Sat, 08 Feb 2020 12:16:28 GMT
    Server: WSGIServer/0.2 CPython/3.8.1
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    
    {
        "comments": [
            {
                "comments": "hajdhklfjasdklfjldsk",
                "created_at": "2020-02-08T12:15:46.264Z",
                "id": 1,
                "updated_at": "2020-02-08T12:15:46.264Z",
                "user_account": "def"
            },
            {
                "comments": "gotohome",
                "created_at": "2020-02-08T12:16:25.028Z",
                "id": 2,
                "updated_at": "2020-02-08T12:16:25.028Z",
                "user_account": "dgdgd"
            }
        ]
    }
    

     

    Reference

     

    댓글

Designed by Tistory.