[Django] api 제한기 2 + admin login 관련 문제 해결
오늘은 진짜 진짜 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 에게 물어보니까 이런식으로 바꾸라고 해서 바꿨더니 해결되었다.
이유를 찾아보니 대략 이런 이유인 것 같았다.
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 를 실행해본다...