Project/[Project] Red Horse

[Backend] 정리 - 2

bonevillain 2023. 6. 14. 23:56

Django User Model

이 부분은 사람마다 의견이 다를 것 같네요.

보통 장고에서 User Model 계획할 때, 3가지 방법을 사용합니다. 

 

  • 기존 User를 Foreign Key로 연결 (OneToOneField 이용)
  • User 모델을 완전 새로 정의 (AbstractBaseUser 상속)
  • User 모델에 필드 추가 (AbstractUser 상속)

제가 일하면서 들었던 조언 중 하나는

장고 프레임워크를 너무 변형시킬 경우, 다른 패키지(3rd party)를 이용할 때 문제 발생할 가능성이 높아진다.
최대한 프레임워크 기초를 건들지말고 있는 그대로 사용하는 것이 좋다.

라는 것이었습니다. 그래서 이 프로젝트도 OneToOneField를 이용하여 Profile 테이블을 만들었습니다.

이 부분에 대해서 무조건 따르는 것보단 득과 실을 잘 따져서 코딩하는 것이 좋을 것 같습니다.

저는 단순히 모델만 봤을 땐, User 모델 안에 Profile 모델의 필드들이 있으면 좋았을 것 같지만 뭐.. 이건 각자의 경험이나 프로젝트에 따라 달라질 것 같네요.



Custom Authentication - Simple JWT

위 내용에 연장선 상에 있는 내용일 수 있겠네요. request 안에 로그인한 이용자를 식별할 때,

class CountingLike(APIView):
    def get(self, request):
        ...
        user_id = request.user.id
        ...

request.user로 접근합니다. 그런데 request.user로 접근할 때, 아래와 같이 User 쿼리가 발생합니다. 

 

request.user 사용 시, User 쿼리 수행

그리고 프로젝트에서 프로필 정보가 같이 필요한 경우가 많은데 request.user.profile로 데이터를 가져오면 추가로 한 번 더 쿼리가 발생합니다.

request.user.profile 사용 시, User + Profile 각각 쿼리 수행

request에서 profile 정보까지 가져오려면 무조건 총 두 번의 쿼리가 발생합니다.

 

이 부분에서 두 번이 아닌 최소한 한 번의 쿼리로 User와 Profile 정보를 동시에 가져오고 싶다면 

  • JWTStatelessUserAuthentication 사용
  • JWTAuthentication의 get_user 메소드 오버라이딩
  • 아싸리 User 모델을 AbstractUser를 상속받아 Profile 모델과 결합

3가지가 생각나는데 다른 더 좋은 방법이 있는지 당장은 기억이 안나네요.

 

두 번째 방법을 사용한다면

from rest_framework_simplejwt.authentication import JWTAuthentication

class CustomJWTAuthentication(JWTAuthentication):
    def get_user(self, validated_token):
        …
        try:
            user = self.user_model.objects.select_related("profile").get(
                **{api_settings.USER_ID_FIELD: user_id}
            )
        …
        return user

request.user할 때, User 정보를 가져오는 코드는 rest_framework_simplejwt.authentication에서 JWTAuthentication 안에 get_user라는 메소드에서 가져옵니다. 그에 따라 JWTAuthentication 상속받아서 get_user를 오버라이딩하면 Profile을 같이 가져올 수 있습니다.

그러나 문제는 Profile 정보가 필요없는 곳에서도 항상 User + Profile 셋트로 가져오다보니 과연 좋은 방법인지는 따져봐야할 것 같네요.

 

이 프로젝트에서는 JWTStatelessUserAuthentication을 이용하기 때문에 기본적으로 사용자를 식별하는데 DB조회는 발생하지는 않습니다. (Profile이 필요한 경우 제외) 하지만 기본 JWTAuthentication을 이용한다면 최적화하는데 이 부분을 고려할 필요가 있을 것 같습니다.

 

 

 JSONField 사용

이전 포스트에서 Profile 모델의 여러 취미(Passions) 데이터를 저장하는데 ManyToMany를 쓰려다가 그냥 통째로 넣기로 하였었습니다.

 

JSON 구조는 프레임워크에서 검사해줘서 문제는 안되는데 JSON 구조 안에서 어떤 형식(schema)으로 저장할 것인지는 별로도 지정해야합니다. 여기서는 jsonschema라는 라이브러리를 이용하여 JSON schema를 작성하고 Validator를 만들었습니다.

 

처음에는 JSON 구조에 맞춰서

 

{ "data" : [ "취미 1", "취미 2", "취미 3"] } 

 

위와 같은 형태로 스키마를 작성하면 아래와 같이 됩니다.

PASSIONS_JSON_FIELD_SCHEMA = {
    "type": "object",
    "properties": {
        "data": {
            "type": "array",
            "items": {"type": "string"},
            "minItems": 0,
            "maxItems": settings.MAX_PASSION_NUM,
            "uniqueItems": True,
        },
    },
}

 

BaseValidator를 상속받아 새로운 validator를 만들어 모델에 적용시켜줍니다.

 

validators.py

from django.core.validators import BaseValidator

class PassionsValidator(BaseValidator):
    def compare(self, value, schema):
        try:
            jsonschema.validate(value, schema)
        except jsonschema.exceptions.ValidationError:
            raise ValidationError("JSON schema check is failed.")

 

models.py - Profile 모델 일부분

passions = models.JSONField(
    max_length=100,
    null=True,
    blank=True,
    validators=[PassionsValidator(limit_value=PASSIONS_JSON_FIELD_SCHEMA)],
)

 

근데 만들다보니 Profile의 Passions를 신규 또는 수정할 때, 매번

{
    …
    "passions" : {"data" : ["축구", "야구", "농구"]}
    …
}

"data" 키를 포함하는 위와 같은 형식으로 넣어줘야합니다.

"data"를 빼고 리스트만 넣어주도록 스키마 형태를 조금 변경하였습니다.

PASSIONS_JSON_FIELD_SCHEMA = {
    "type": "array",
    "items": {
        "type": "string",
    },
    "minItems": 0,
    "maxItems": settings.MAX_PASSION_NUM,
    "uniqueItems": True,
}

이렇게하면 이제 아래 같은 형태로 깔끔하게 넣을 수 있습니다.

{
    …
    "passions" : ["축구", "야구", "농구"]
    …
}

 

근데 JSON 형식은 아닌데 과연 정말로 깔끔한 것인가?

라는 질문에는 의문이긴 하네요.

'Project > [Project] Red Horse' 카테고리의 다른 글

[Backend] 정리 - 3  (0) 2023.06.15
[Backend] 정리 - 1  (0) 2023.06.13
[Backend] env 환경변수  (0) 2023.06.12
[Backend] 프로젝트 환경셋팅  (0) 2023.06.12
[Backend] API 설계  (0) 2023.06.09