소프트웨어 마에스트로/BackEnd(Django)

[Django] api 제한기 2 + admin login 관련 문제 해결

alpakaka 2024. 9. 28. 21:10

오늘은 진짜 진짜 api 제한을 적용해볼 예정이다.

 

오늘은 멘토님께 피드백 받은 사항이 있어서 일단 내가 이해할 수 있으며, 우리 프로젝트에 적용할 수 있는 것들을 간략히 적어본다.

LLM 추천 : Cursor + v0

코드 테스트 작성에 용의하도록 코드를 작성하는 경우도 있다.

코드 루틴 : 빠르게 구현 -> 테스트코드 작성 -> 리팩토링 후 테스트 코드로 검증 하는 방식도 있음

쿼리 튜닝 하는 것도 괜찮을 듯 (IndexHint)

DB sharding 하는 방법도 있음

인프라 그리기 (변경사항이 있을 경우)

 

일단 백로그에 넣어두고 나중에 열어보는 방법이 있을 듯 보인다.

 

api 제한부터 진행해본다! 

저번에 작성했던 모델이다.

그리고 serializer 를 작성해주었다.

이제 recommend 에서 작성한 경우, 업데이트 하는 식으로 작성되어야한다.

 

대략적인 로직은 다음과 같이 진행해보려고한다.

1. recommend 를 실행하는 경우 get을 한다.

2. get을 했는데 없는 경우 recommend api를 실행시키고 create 를 통해서 만든다.

3. get 을 했는데 있는 경우 시간을 바탕으로 recommend api 를 실행시키거나, 429 Too Many Request 을 반환해준다.

 

지금 당장은 premium 을 넣을 수 있는 수단이 없기 때문에 아주 라이트하게 만들어본다.

이런식으로 작성했다.

간단하게 설명해보자면 get_or_create 를 한다. 그 후에

create가 된경우 무조건 True,

get을 했을 때 last_used_at에서 10초보다 적을 때 요청을 했다면 False, 

10초 후라면 True,

다른 무언가의 에러가 발생했다면 False 와 에러를 불렀다.

recommend 에 이런식으로 체크하는 방식을 만들어줬다.

여기까지 작성하다가 생각이 나버렸는데.. db를 수정하거나 생성하는 로직이라 utils 가 아니라 model 의 함수로 넣는 편이 적절해보인다.

따라서 model에 넣어서 작동하도록 바꿔보았다.

그리고 serializer 를 따로 사용하지 않길래, 지워버렸다.

 

그리고 테스트를 해봤는데 계속 에러가 발생해서 보니..

어째서인지 user_id 가 숫자가 아니라 이런 모델로 들어온다..

왜인지 모르겠다.

그리고 계속 2개의 인자를 받는 함수인데 3개씩 들어온다고 에러가 떴다.

copilot 에게 물어보니까 이런식으로 바꾸라고 해서 바꿨더니 해결되었다.

 

이유를 찾아보니 대략 이런 이유인 것 같았다.

https://wikidocs.net/16074

 

44. class 정리 - 정적메소드 @classmethod와 @staticmethod의 정리

## 1. 정적메소드(@classmethod와 @staticmethod) - 정적메소드라 함은 클래스에서 직접 접근할 수 있는 메소드입니다. - 파이썬에서는 클래스에서 직접 접…

wikidocs.net

첫번째 인자는 모델이 들어가는데, 코파일럿이 말한대로 cls 를 넣지 않아서 오류가 난 듯 했다.

그리고 이건 클래스 함수보다는 static 함수가 더 맞는 것 같다. 그래서 바꿔주고 개선사항을 확인해보았다.

코파일럿이 다시 클래스함수를 쓰라고 하는데 이유를 잘 모르겠어서 찾아보았다.

찾아보니.. 클래스 자체를 사용중이라서 클래스메소드를 사용하란 것 같다. 상속도 딱히 사용할 일이 없어서.. 사실 둘이 딱히 큰 차이는 없는 것 같아서 이대로 사용해보려고한다.

이제 테스트 코드를 만들어 볼 예정이다.

안에 llm 이 껴있어서 mock 을 사용하거나... 결과값을 나와도 체크하지 않는 방향으로 가는 것이 있을 텐데..

음 mock 이 제일 베스트로 생각된다.

따라서 테스트 코드를 작성해본다!

 

@pytest.fixture
def llm():
    mock_response = Mock()
    mock_response.choices = [
        Mock(
            message=Mock(
                content=(
                    '{"id": 1, "content": "subtask", "start_date": "2024-09-01", '  # noqa
                    '"end_date": "2024-09-24", "category_id": 1, "order": 1, '
                    '"is_completed": false, "children": []}'
                )
            )
        )
    ]

    return mock_response

이런식으로 llm mock 을 만들어줬다.

저기 뷰 자체에서 부르는게 골치가 아팠다... 

어쨌든 이런식으로 llm 을 만들고 테스트코드는 다음과 같이 작성해주었다.

 

import time
from unittest.mock import patch

from django.urls import reverse
from rest_framework import status


@patch("todos.views.client.chat.completions.create")
def test_rate_limit_exceeded(mock_llm, authenticated_client, create_todo, llm):
    mock_llm.return_value = llm
    url = reverse("recommend")
    response = authenticated_client.get(url, {"todo_id": create_todo.id})
    assert response.status_code == status.HTTP_200_OK

    response = authenticated_client.get(url, {"todo_id": create_todo.id})
    assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS
    assert response.data["error"] == "Rate limit exceeded"


@patch("todos.views.client.chat.completions.create")
def test_rate_limit_passed(mock_llm, authenticated_client, create_todo, llm):
    url = reverse("recommend")
    mock_llm.return_value = llm

    response = authenticated_client.get(url, {"todo_id": create_todo.id})
    assert response.status_code == status.HTTP_200_OK

    time.sleep(10)

    response = authenticated_client.get(url, {"todo_id": create_todo.id})
    assert response.status_code == status.HTTP_200_OK

 

하나는 10초 이내에 호출했을 떄이고 하나는 10초 후에 호출했을 때이다. 

통과하였다!

추가적으로 premium 인 유저인 경우를 처리했다.

@patch("todos.views.client.chat.completions.create")
def test_rate_limit_premium(
    mock_llm, authenticated_client, create_user, create_todo, llm
):
    create_user.is_premium = True
    create_user.save()

    url = reverse("recommend")
    mock_llm.return_value = llm

    response = authenticated_client.get(url, {"todo_id": create_todo.id})
    assert response.status_code == status.HTTP_200_OK

    response = authenticated_client.get(url, {"todo_id": create_todo.id})
    assert response.status_code == status.HTTP_200_OK

이런식으로 테스트코드를 작성했다.

로직은 간단했는데

if user.is_premium:
                user_last_usage.last_used_at = timezone.now()
                user_last_usage.save(update_fields=["last_used_at"])
                return True, "Premium user"

유저가 프리미엄인지만 확인하고 last_used_at 만 업데이트 해주도록 작성했다.

일단 이정도로 이건 마무리 지으면 좋을 듯 하다.

 

이제 나머지 할일은 리팩토링이다.

리팩토링 할 사항

header 에서 user_id 를 가져오기, model 이름 변경하기, sentry log 넣기, admin 에서 안뜨는 이유 찾기

일단 마지막의 django admin 에서 계속 로그인 오류가 떴는데,

localhost 에선 괜찮은데 이상하게 dev 나 prod 에서는 에러가 발생했다.

저번에 멘토님의 피드백으로는 아마 속성 몇개가 부족해서 로그인이 안되는 것 같다는 의견을 들었다.

그래서 한번 이쪽 방면으로 찾아보는 편이 좋아 보인다.

 

일단 찾아보니 이런 문제가 있는 것 같았다.

https://stackoverflow.com/questions/29573163/django-admin-login-suddenly-demanding-csrf-token

 

django admin login suddenly demanding csrf token

I was logging into my django admin console easily a few minutes ago. I must have changed something somewhere that caused this error when logging in as superuser: Forbidden (403) CSRF verification ...

stackoverflow.com

일단 적용해본다..!

오 적용이 완료되었다...!

오...

생각보다 간단한 이유였구나.. CSRF 문제가 맞았었다.. 

settings 에 이 코드를 추가해줬다.

CSRF_TRUSTED_ORIGINS = ["https://*.stepby.one"]

 

그러면 이제 남은 리펙토링을 진행하면 될 듯 하다.

리팩토링 할 사항

header 에서 user_id 를 가져오기, model 이름 변경하기, sentry log 넣기

인데, 일단 제일 만만한 header 에서 가져오는 걸 먼저 해보겠다!

 

아 그전에 먼저.. 방치해놓은 LLM 부터 수정해야할 것 같다.

오랫동안.. 방치되어버린 프롬프트를 ... rebase 를 실행해본다...