Django DRF - 프로젝트 - User앱 관련 (1) :: models.py :: @classmethod 사용법.

권수민·2023년 10월 30일
0

먼저 이번 프로젝츠에서는 깃관련 컨벤션에 대한 부분을 많이 배운거같다.
Api문서 작성에 관련해서도 뭔가 더 자세히 어떻게 써야하는지 배운거 같은데 이부분에 관련해서는 추후에 작성을 진행하도록 하겠다.

먼저 처음에 serializer의 유효성 체크검사의 부분이 어느정도까지 커버해주는지 확실치 않아서 내가 직접 커버해주는 여러 메소드들을 구현하게되었는데 겹치는 부분들은 추후 다시 지워버렸다.

내가 맡은 부분은 회원수정 및 페이지네이션, 그리고 내가 좋아요한 게시물 불러오기였다. 그리고 AWS서버 배포를 담당하였는데 프론트엔드가 index.html안에 있는 script를 불러오지 못하여.. 완벽하게 서버배포에 성공하지는 못했다.

일단 먼저 어떤 부분들을 serializer가 해준는 지 몰라 구현한 부분들을 체크해보겠다.

models.py

from datetime import datetime
from django.conf import settings
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.shortcuts import get_object_or_404

먼저 커스터 마이징하는 유저모델을 사용할 경우 유저메니저를 따로 만들어줘야한다.
여기서 해주는 작업은 유저가 저장하려고 할때 이메일 형식 및 비밀번호 해싱화 부분에 대한 추가기능을 넣어주었다.

class UserManager(BaseUserManager):

    """사용자 모델을 생성하고 관리하는 클래스입니다."""

    def create_user(self, email, nickname, password):
        """일반 사용자를 생성하는 메서드입니다."""
        if not email:
            raise ValueError("유효하지 않은 이메일 형식입니다.")

        user = self.model(
            email=self.normalize_email(email),
            password=password,
            nickname=nickname,
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, nickname, password=None):
        """관리자를 생성하는 메서드입니다."""
        if not email:
            raise ValueError("유효하지 않은 이메일 형식입니다.")

        user = self.create_user(
            email,
            password=password,
            nickname=nickname,
        )

        user.is_admin = True
        user.save(using=self._db)
        return user

밑의 유저 모델링 부분은 당연히 데이터베이스에 저장될 필드 및 속성값을 정해주는 모델 클라스가 되겠다.

기본적으로 주어지는 속성 메소드들은 구현이 되어있는 상태이고, 내가 추가해준 부분은

"""클래스 변수에(user필드) 접급해야하여 클래스 메소드 사용"""

@classmethod
def is_email_duplicated(cls, email, user_id=None):
    print(f"models: {email}")
    origin_email = cls.objects.filter(email=email)
    print(origin_email)
    if user_id:
        origin_email = origin_email.exclude(id=user_id)
    return origin_email.exists()

@classmethod
def is_nickname_duplicated(cls, nickname, user_id):
    origin_nickname = cls.objects.filter(nickname=nickname)
    if user_id:
        origin_nickname = origin_nickname.exclude(id=user_id)
    return origin_nickname.exists()

@classmethod
def is_password_same_as_previous(cls, password, user_id):
    user = get_object_or_404(User, id=user_id)
    if user.check_password(password):
        return True
    else:
        return False
        
        

이 부분인데, 여기서 먼저 classmethod가 뭔지 집고 넘어가면

@classmethod

@classmethod가 붙은 메서드는 첫 번째 인자로 클래스 자체를 받게 된다 : 이뜻이 뭐냐하면, 일반적인 인스턴스 메서드에서는 self를 통해 해당 인스턴스의 속성이나 상태를 참조하거나 변경할 수 있지만, 클래스 메서드에서는 cls를 통해 클래스 레벨의 속성이나 메서드만을 참조하거나 변경할 수 있다는 것이다.
클라스 변수처럼 인스턴스 생성에 의해 값이 변동되거나 할일이없다는 것이다.

인스턴스의 상태와는 독립적이고, 의존하지않고 클라스 메소드를 시키므로서 모든 인스턴스에 영향을 준게된다.

더 상세한 예)

class CoffeeShop:
    total_coffee_sold = 0

    def __init__(self, name, price):
        self.name = name
        self.price = price
        self.sold = 0

    def sell_coffee(self, count):
        self.sold += count
        CoffeeShop.total_coffee_sold += count

    @classmethod
    def total_sales(cls):
        return cls.total_coffee_sold

    @classmethod
    def set_discount(cls, percentage):
        cls.discount = percentage / 100

    def get_discounted_price(self):
        return self.price * (1 - getattr(self, 'discount', 0))

espresso = CoffeeShop("Espresso", 3000)
latte = CoffeeShop("Latte", 4000)

espresso.sell_coffee(5)
latte.sell_coffee(3)

print(f"Total coffee sold: {CoffeeShop.total_sales()} cups")  # 출력: Total coffee sold: 8 cups

CoffeeShop.set_discount(10)  # 클래스 메서드를 통해 전체 커피에 10% 할인 적용

print(f"Discounted price of Espresso: {espresso.get_discounted_price()} won")  # 출력: Discounted price of Espresso: 2700.0 won
print(f"Discounted price of Latte: {latte.get_discounted_price()} won")  # 출력: Discounted price of Latte: 3600.0 won

total_coffee_sold는 클래스 변수로써 모든 CoffeeShop 인스턴스에 의해 공유됩니다.
sell_coffee는 인스턴스 메서드로써 특정 커피 상품의 판매량을 증가시킵니다.
그런데 이 메서드 안에서 클래스 변수 total_coffee_sold도 증가시키는데, 이는 모든 인스턴스가 공유하므로 전체 커피 판매량에 영향을 줍니다.

(개별적인 부분을 볼수있는게, 메소드 내 따른 메소드를 불러내어 클라스메소드화시킨것.)
total_sales는 클래스 메서드로써 현재까지 판매된 전체 커피의 수를 반환합니다.
=> 이 메서드는 인스턴스의 상태에는 의존하지 않습니다.
set_discount는 또 다른 클래스 메서드로써 모든 커피에 대한 할인률을 설정합니다.
=> 이는 클래스 레벨에서 할인률을 설정하므로 모든 인스턴스에 영향을 줍니다.

이 예제를 통해 볼 수 있듯이, 클래스 메서드는 인스턴스의 상태와 독립적으로 클래스 레벨에서 작동하며 클래스 변수나 다른 클래스 메서드와의 상호 작용에 주로 사용됩니다.


그렇다면 다시 돌아와서 내가 추가해준부분이 밑에 부분인데,

@classmethod를 사용하는 이유는 다음과 같다.

인스턴스 독립성:

이 메서드는 특정 인스턴스의 상태에 의존하지 않습니다. 즉, 특정 사용자 객체의 상태나 속성에 의존하지 않고 전체 User 모델 데이터에서 이메일 닉네임 등 중복 여부를 확인합니다.
따라서, 인스턴스에 종속적이지 않은, 클래스 레벨에서의 연산이 필요하기 때문에 클래스 메서드를 사용합니다.

장고 QuerySet API 접근:
cls.objects.filter()와 같은 형식은 장고의 QuerySet API를 사용하여 데이터베이스 쿼리를 수행합니다. 이는 모델 클래스 레벨에서 수행되는 작업이므로, 인스턴스 메서드보다는 클래스 메서드가 더 적합합니다.

재사용성:
클래스 메서드를 사용하면, 이 메서드는 인스턴스를 생성하지 않고도 언제든지 이메일 중복 검사를 수행할 수 있습니다.
예를 들어, 사용자 등록 과정에서 중복 이메일 검사를 수행하기 위해 User.is_email_duplicated(email)와 같은 방식으로 간편하게 호출할 수 있습니다.

요약하면, is_email_duplicated 메서드는 특정 User 인스턴스와는 독립적으로 작동하며,
장고의 QuerySet API를 통해 데이터베이스 연산을 수행하기 때문에 @classmethod를 사용하는 것이 적절하다.

  1. 시리얼라이저에서 이메일 부분 중복확인 체크하고 update()부분에서 이미 excluse시켜주는 걸 모르고 추가적으로 기능을 부여했다.
    그렇기에 마지막final코드에는 불필요한 코드이다 보니 삭제.
    @classmethod
    def is_email_duplicated(cls, email, user_id=None):
    print(f"models: {email}")
    origin_email = cls.objects.filter(email=email)
    print(origin_email)
    if user_id:
    origin_email = origin_email.exclude(id=user_id)
    return origin_email.exists()
  2. 닉네임 중복여부 확인 -> 불필요하여 삭제
    @classmethod
    def is_nickname_duplicated(cls, nickname, user_id):
    origin_nickname = cls.objects.filter(nickname=nickname)
    if user_id:
    origin_nickname = origin_nickname.exclude(id=user_id)
    return origin_nickname.exists()
  3. 이전 패스워드가 현재바꿀것과 동일한지 아닌지 확인 -> 추후 이건 common_utils.py로 옮김
    @classmethod
    def is_password_same_as_previous(cls, password, user_id):
    user = get_object_or_404(User, id=user_id)
    if user.check_password(password):
    return True
    else:
    return False
class User(AbstractBaseUser):
    """
    사용자 모델을 정의하는 클래스입니다.

    - email(필수) : 로그인 시 사용할 사용자의 이메일 주소입니다.
        - 다른 사용자의 이메일과 중복되지 않도록 설정합니다. (Unique)
    - password(필수) : 사용자의 비밀번호입니다.
    - nickname(필수) : 사용자의 활동 아이디입니다.
        - 다른 사용자의 닉네임과 중복되지 않도록 설정합니다. (Unique)
    - intro : 사용자의 소개글입니다.
    - subscribe : 사용자 간 구독(팔로우) 관계입니다.
    - is_admin : 관리자 권한 여부입니다.
        - True 혹은 False를 저장할 수 있으며, 기본값으로 False를 저장하도록 설정합니다.
    """

    email = models.EmailField("이메일", max_length=255, unique=True)
    password = models.CharField("비밀번호", max_length=255)
    nickname = models.CharField("활동 아이디", max_length=30, unique=True)
    intro = models.CharField("소개글", max_length=500, null=True, blank=True)
    subscribe = models.ManyToManyField(
        "self",
        verbose_name="구독",
        symmetrical=False,
        related_name="subscribers",
        blank=True,
    )
    is_admin = models.BooleanField("관리자 여부", default=False)

    objects = UserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = [
        "nickname",
    ]

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        return True

    def has_module_perms(self, app_label):
        return True

    @property
    def is_staff(self):
        return self.is_admin

    """클래스 변수에(user필드) 접급해야하여 클래스 메소드 사용"""

    @classmethod
    def is_email_duplicated(cls, email, user_id=None):
        print(f"models: {email}")
        origin_email = cls.objects.filter(email=email)
        print(origin_email)
        if user_id:
            origin_email = origin_email.exclude(id=user_id)
        return origin_email.exists()

    @classmethod
    def is_nickname_duplicated(cls, nickname, user_id):
        origin_nickname = cls.objects.filter(nickname=nickname)
        if user_id:
            origin_nickname = origin_nickname.exclude(id=user_id)
        return origin_nickname.exists()

    @classmethod
    def is_password_same_as_previous(cls, password, user_id):
        user = get_object_or_404(User, id=user_id)
        if user.check_password(password):
            return True
        else:
            return False

    class Meta:
        db_table = "user"
profile
초보개발자

1개의 댓글

comment-user-thumbnail
2023년 10월 31일

상세한 비유와 예시로 클래스매서드가 무엇인지 설명하고 왜 사용했는지 알수 있게 잘 정리해 주셨네요!

답글 달기