DRF로 로그인 & CRUD 만들어보기
이번 글에서는 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