Python

DRF로 로그인 & CRUD 만들어보기

320Hwany 2025. 5. 6. 18:16

이번 글에서는 DRF (Django REST Framework)로 회원 로그인 및 간단한 CRUD를 만들어보려고 합니다.

회사를 다니게 된 이후로 go, python을 사용하게 되었는데 python의 경우 잘 동작하던 프로젝트에서 개발을 했지

처음부터 만들어본 경험은 없는 것 같아서 글을 써보게 되었습니다.

 

📕 요청의 흐름

사용자의 요청이 들어오면 urls.py 를 통해 매핑되는 view를 확인합니다.

그 다음으로 views.py 에서 사용자의 요청/응답을 처리합니다.

이때 비즈니스 관련 로직은 service를 통해 처리하고 service는 DB 관련 로직이 들어있는 repository를 참조합니다.

views -> service -> repository 순으로 참조를 하는 코드라고 봐주시면 될 것 같습니다.

 

📘 코드 살펴보기

urls.py

urlpatterns = [
    path("members", MemberView.as_view(), name="member-view"),
    path("login", MemberLoginView.as_view(), name="login"),
]

 

http://127.0.0.1:8000/crud-app/members 로 들어온 요청은 MemberView 와 매핑되고

http://127.0.0.1:8000/crud-app/login 로 들어온 요청은 MemberLoginView와 매핑이 됩니다.

 

views.py

class MemberView(APIView):

    def __init__(self, **kwargs: Any):
        super().__init__(**kwargs)
        self.member_service = container.member_service

    @handle_exception
    def post(self, request: Request) -> Response:
        member_create_request: MemberCreateRequest = MemberCreateRequest(**request.data)
        self.member_service.create_member(member_create_request)
        return Response("OK", status=201)

    @handle_exception
    @authentication
    def get(self, request: Request, member_id: int) -> Response:
        member_response: MemberResponse = self.member_service.get_member(member_id)
        return Response(asdict(member_response), status=200)

    @handle_exception
    @authentication
    def patch(self, request: Request, member_id: int) -> Response:
        member_update_request: MemberUpdateRequest = MemberUpdateRequest(**request.data)
        self.member_service.update_member(member_id, member_update_request)
        return Response("OK", status=200)

    @handle_exception
    @authentication
    def delete(self, request: Request, member_id: int) -> Response:
        self.member_service.delete_member(member_id)
        return Response("OK", status=200)

 

MemberView는 APIView를 상속하여 생성하였습니다.

APIView는 DRF에서 제공하는 RESTful API를 구현하기 위한 뷰의 기본 동작(HTTP 메소드, Request/Response 객체)을 제공합니다.

특이한 것은 메소드의 이름이 HTTP method와 매핑된다는 것입니다.

 

또한 예외에 대한 공통 처리를 위해 handle_exception 데코레이터를 이용하였습니다.

아래와 같이 내부 로직에서 예외가 발생하면 처리할 수 있도록 하였습니다.

def handle_exception(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except ValueError as e:
            return Response({"message": str(e)}, status=400)
        except AuthenticationFailed as e:
            return Response({"message": str(e)}, status=401)
        except Exception as e:
            logger.error(f"An error occurred: {e}")
            return Response({"message": "An error occurred"}, status=500)
    return wrapper

 

인증 관련 authentication 데코레이터도 만들었는데 jwt token을 바탕으로 회원 id 를 반환하는 로직입니다.

def authentication(func):
    @wraps(func)
    def wrapper(self, request: Request, *args, **kwargs):
        secret_key: str = "GZ8x8-Dl2FZAClDee7aOZ5rHnPDO1BtYK8wH6P5wf_k"
        algorithm: str = "HS256"

        access_token: str = request.headers.get("AccessToken")
        refresh_token: str = request.headers.get("RefreshToken")

        try:
            access_payload = jwt.decode(access_token, secret_key, algorithms=[algorithm])
            member_id = access_payload.get("member_id")
        except jwt.ExpiredSignatureError:
            try:
                refresh_payload = jwt.decode(refresh_token, secret_key, algorithms=[algorithm])
                member_id = refresh_payload.get("member_id")
                member_response: MemberResponse = self.member_service.get_member(member_id)

                access_payload = {
                    "member_id": member_response.member_id,
                    "name": member_response.name,
                    "email": member_response.email,
                    "age": member_response.age,
                    "exp": datetime.utcnow() + timedelta(hours=1)  # access_token 만료 시간 (1시간)
                }

                access_token: str = jwt.encode(access_payload, secret_key, algorithm=[algorithm])
                request.headers.__setattr__("AccessToken", access_token)

            except jwt.ExpiredSignatureError:
                raise AuthenticationFailed("Refresh token has expired")
            except jwt.InvalidTokenError:
                raise AuthenticationFailed("Invalid refresh token")
        except jwt.InvalidTokenError:
            raise AuthenticationFailed("Invalid access token")

        return func(self, request, member_id=member_id, *args, **kwargs)
    return wrapper

 

 

확실히 어떤 언어든 단순 반복되는 코드들을 줄이기 위한 방법은 존재하는 것 같습니다.

요청에 대한 예외 처리, 인증 관련 부분에서 Python 데코레이터를 이용하여 해결해보았습니다.

 

그리고 또 주목해볼 점이 있다면 MemberView를 생성할 때 container를 통해 member_service를 주입 받도록 하였습니다.

def __init__(self, **kwargs: Any):
    super().__init__(**kwargs)
    self.member_service = container.member_service

 

depedency_container.py

Java Spring 진영에서는 각 객체들을 스프링 컨테이너를 통해 의존 관계 주입을 해줍니다.

하지만 Python에서도 일부 관리해주는 프레임워크가 있는 것 같지만 이번에는 직접 관리를 해주도록 하였습니다.

중요한건 컴포넌트니 bean 주입과 같은 것이 아니고 Stateless한 싱글톤으로 관리되는 객체에 대한 의존 관계 주입이라고 생각합니다.

class MetaSingleton(type):
    _instances: dict = {}

    def __call__(cls, *args: Any, **kwargs: Any) -> object:
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class DependencyContainer(metaclass=MetaSingleton):
    def __init__(self):
        self._member_repository = MemberRepository()
        self._member_service = MemberService(self._member_repository)

    @property
    def member_repository(self) -> MemberRepository:
        return self._member_repository

    @property
    def member_service(self) -> MemberService:
        return self._member_service

container = DependencyContainer()

 

member_service.py

class MemberService:

    def __init__(self, member_repository: MemberRepository):
        self.member_repository = member_repository

    def create_member(self, dto: MemberCreateRequest) -> None:
        self.member_repository.create_member(dto)

    def login(self, dto: MemberLoginRequest) -> JwtToken:
        return self.member_repository.login(dto)

    def get_member(self, member_id: int) -> MemberResponse:
        member: Member = self.member_repository.get_member(member_id)
        return MemberResponse.from_model(member)

    def update_member(self, member_id: int, dto: MemberUpdateRequest) -> None:
        self.member_repository.update_member(member_id, dto)

    def delete_member(self, member_id: int) -> None:
        self.member_repository.delete_member(member_id)

 

member_repository.py

Django ORM을 통해 직접 쿼리를 작성하지 않고 get, update_or_create, save, delete 를 통해
간단한 CRUD 로직을 수행할 수 있습니다.

class MemberRepository:

    @transaction.atomic
    def create_member(self, dto: MemberCreateRequest) -> Member:
        member = dto.to_model()
        member.save()
        return member

    @transaction.atomic
    def login(self, dto: MemberLoginRequest) -> JwtToken:
        try:
            member = Member.objects.get(email=dto.email, password=dto.password)

            access_payload = {
                "member_id": member.member_id,
                "name": member.name,
                "email": member.email,
                "age": member.age,
                "exp": datetime.utcnow() + timedelta(hours=1)  # access_token 만료 시간 (1시간)
            }
            refresh_payload = {
                "member_id": member.member_id,
                "exp": datetime.utcnow() + timedelta(hours=24)  # refresh_token 만료 시간 (24시간)
            }
            secret_key: str = "GZ8x8-Dl2FZAClDee7aOZ5rHnPDO1BtYK8wH6P5wf_k"
            access_token: str = jwt.encode(access_payload, secret_key, algorithm="HS256")
            refresh_token: str = jwt.encode(refresh_payload, secret_key, algorithm="HS256")

            JwtRefreshToken.objects.update_or_create(
                member_id=member.member_id,
                defaults={"refresh_token": refresh_token}
            )

            return JwtToken(access_token=access_token, refresh_token=refresh_token)

        except Member.DoesNotExist:
            raise ValueError("회원 정보가 존재하지 않습니다.")

    @transaction.atomic
    def get_member(self, member_id: int) -> Member:
        try :
            return Member.objects.get(member_id=member_id)
        except Member.DoesNotExist:
            raise ValueError(f"회원 id가 {member_id}인 회원이 존재하지 않습니다.")

    @transaction.atomic
    def update_member(self, member_id: int, dto: MemberUpdateRequest) -> None:
        try:
            member = Member.objects.get(member_id=member_id)
            for field, value in asdict(dto).items():
                setattr(member, field, value)
            member.save()
        except Member.DoesNotExist:
            raise ValueError(f"회원 id가 {member_id}인 회원이 존재하지 않아 수정할 수 없습니다.")

    @transaction.atomic
    def delete_member(self, member_id) -> None:
        try:
            member = Member.objects.get(member_id=member_id)
            member.delete()
        except Member.DoesNotExist:
            raise ValueError(f"회원 id가 {member_id}인 회원이 존재하지 않아 삭제할 수 없습니다.")

 

models.py

django.db 의 models 를 상속 받아 Member, Order, JwtRefreshToken을 생성해주었습니다.

이때 models는 DB 엔티티로 DB의 테이블과 매핑됩니다.

class Member(models.Model):
    member_id: models.BigAutoField = models.BigAutoField(primary_key=True)
    name: models.CharField = models.CharField(max_length=100)
    email: models.EmailField = models.EmailField()
    password: models.CharField = models.CharField(max_length=100)
    age: models.IntegerField = models.IntegerField()

    created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True)
    updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'members'


class Order(models.Model):
    order_id: models.BigAutoField = models.BigAutoField(primary_key=True)
    member_id: models.BigIntegerField = models.BigIntegerField()
    price: models.IntegerField = models.IntegerField()

    created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True)
    updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'orders'


class JwtRefreshToken(models.Model):
    jwt_refresh_token_id: models.BigAutoField = models.BigAutoField(primary_key=True)
    member_id: models.BigIntegerField = models.BigIntegerField()
    refresh_token: models.CharField = models.CharField(max_length=500)
    created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True)
    updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'jwt_refresh_token'

 

📚 정리하기

Java만 하던 대학생이 입사를 한 후 회사에서 Go, Python을 사용하게 되었는데

철학은 조금 다른 것 같기는 하지만 각 언어마다 부딪히는 문제는 비슷하고 각각의 해결책도 비슷한 것 같습니다.

특히 의존 관계 주입을 하는 방법, 공통 예외 처리, 인증/인가 관련 로직 등등..

 

코드 보러가기

 

GitHub - 320Hwany/django-crud-practice

Contribute to 320Hwany/django-crud-practice development by creating an account on GitHub.

github.com