Django

Login의 View 작성시 피해야 할 코드 작성법

Bruno-Jang 2021. 12. 23. 17:04

 

제가 Login view 부분을 작성할 때 했던 실수들이 많이 담겨있어서 어떤 식으로 고치면 좋을지 공유하고자 합니다.

 

class LogInView(View):
    def post(self, request):
        data  = json.loads(request.body)
        email = data['email']
        password = data['password']

        try:
            user    = User.objects.get(email=email)
            user_id = user.user_id
            
            if not User.objects.filter(email=email).exists() or not User.objects.filter(password=password).exists():
                return JsonResponse({'message': 'INVALID_USER'}, status=401)
            
            if User.objects.filter(email=email, password=password).count() == 1:
                return JsonResponse({'message': f"SUCCESS! user_id : '{user_id}' successfully logged in"}, status=200)
            
            else:
                return JsonResponse({'message': '이메일과 비번이 맞지 않습니다.'}, status=400)
                
        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)

 

1. user 존재 여부 판단 이전에 get을 통해 값을 가져오려는 경우의 문제점

try 구문 아래에 DB의 User 테이블에서 email 값이 유저가 입력한 email 값과 같은 것을 user 라는 변수에 저장해달라고 했습니다.

제가 이 부분을 작성하면서 했던 생각은 조건에 일치하는 데이터의 유무에 대해서는 생각하지 않고 밑에서 계속 user를 사용할거니까 "미리 정의해두자"라고 생각했습니다.

 

그러나 존재하지 않는 이메일을 쓰자 아래와 같은 에러가 발생했습니다.

 

Internal Server Error : products.models.User.DoesNotExist (해당 유저에 매칭되는 쿼리가 없음)

 

objects.get

 - 조건에 만족하는 하나의 객체만을 반환

 - 조건에 만족하는 객체가 없거나 2개 이상이면 에러 발생 (DoesNotExist / MultipleObjectsReturned)

 - 객체 하나만을 반환하므로 objects.filter와 다르게 for문을 사용할 필요 X

 

Unauthorized : MultipleObjectsReturned (중복되는 값 리턴)

코드를 수정해가면서 테스트하던 중에 뜬 에러메세지입니다.
코드 전체적으로 중복된 부분도 보이시겠지만 지금은 에러메세지에만 집중하시면 좋겠습니다.

아래의 코드와 에러메세지를 보시고 어떤 부분에서 문제가 발생하고 있고 어떻게 바꾸면 좋을지 생각하신 후에

읽으신다면 더 좋을것 같습니다. 

 

'get'과 'exists'에 대해 찾아보자.

 

'get'은 위에서 설명했듯이 조건에 맞는 하나의 객체만을 가져옵니다. 만약 없거나 둘 이상이라면 에러가 발생합니다.

'exists'는 'QuerySet'의 메서드입니다.

그렇기 때문에 'User' 객체가 'exists' 속성을 가지고 있지 않다고 에러메세지를 보여주고 있습니다.

'filter'와 'exists'가 어울리는 짝이라고 생각하시면 되겠습니다.

 

그래서 위의 상황에서는

if not User.objects.get(email=email):

이런 식으로 수정하고

User 테이블에 해당 이메일에 맞는 데이터가 없을 경우에 뜰 에러에 대비해서

except User.DoesNotExists:

    return JsonResponse({'message': 'INVALID_USER'}, status=404)

이렇게 추가하는 것이 맞다고 생각합니다.

 

# 그리고 55번째 줄에 token 부분은 제가 checkpw와 token을 혼동해서 잘못 쓴 부분이므로 참고하시지 않기를 바랍니다!

 

아래 사이트의 답변을 참고하신다면 도움이 많이 될겁니다!

https://stackoverflow.com/questions/11093087/object-has-no-attribute-exists

 

object has no attribute 'exists'

I am using Django 1.3 and trying to use .exists() on a model entry but get the error listed below. Exists() was included in Django 1.2 so I should have access to it. I verified my version using d...

stackoverflow.com

 

objects.filter

 - 조건에 만족하는 객체의 개수는 1개 이상이면 OK

 - 조건에 만족하는 객체의 QuerySet 반환

 - QuerySet으로 반환되기에 for문 사용하여 원하는 값 추출

try:
    user = User.objects.filter(user_name=user_name)
    result = []
    for i in user:
        result.append(
        	{
                    'user_name' : i.user_name,
                    'email'     : i.email,
        	}
        )
    return JsonResponse({'result': result}, status=200)  
except KeyError:
    return JsonResponse({'message': 'KEY_ERROR'}, status=400)

QuerySet - for문 사용하여 return

 

2. 중첩 삭제하여 가독성 높이기

if not User.objects.filter(email=email).exists() or not User.objects.filter(password=password).exists():

첫 번째 코드 작성 부분에서 일부를 가져왔습니다.

이메일과 비밀번호를 입력해서 로그인하고자 할 때 DB에 이에 맞는 데이터가 없을 경우 INVALID_USER 라는 메세지를 리턴하고자 했습니다.

그런데 filter의 특성을 모른채 작성해서 이런 가독성 떨어지는 코드가 작성되는 상황이 벌어졌습니다.

아래처럼 작성하는 것이 가독성과 효율성 모두 높일 수 있습니다.

if not User.objects.filter(email=email, password=password).exists():

 

3. 불필요한 else 삭제 (전체적인 흐름 이해할 것)

data = json.loads(request.body)를 try 구문 밖에 썼을 때의 문제점은 데이터가 json 형태로 오지 않았을 때입니다. 아래처럼 request.body 부분이 공백으로 된 상태로 전달되거나 파일과 같은 타입이어서 json 타입이 아닌 경우입니다.

그래서 이런 경우들을 방지하고자 data = json.loads(request.body)를 try 구문 안에 쓰고 except에 json.JSONDecodeError을 해줘야 합니다. 지금 이 상태에서는 당장 필요하지 않을수도 있으나 어떤 식으로 쓰는지 보여드리고자 추가했으니 참고 부탁드립니다~!

JSONDecodeError 발생시키는 예시
JSONDecodeError 예시

 

아래의 코드를 통해서 이 부분을 설명하고자 합니다.

이메일과 비밀번호가 맞지 않는 경우 "INVALID_USER" 메세지를 리턴하게 하고
그 이외의 경우인 이메일과 비밀번호가 맞는 경우에는 get을 통해서 user 데이터를 가져와서
원하는 값을 return하는 것으로 바꿨습니다.

class LogInView(View):
    def post(self, request):
        try:
            data     = json.loads(request.body)
            email    = data['email']
            password = data['password']
            if not User.objects.filter(email=email, password=password).exists():
                return JsonResponse({'message': 'INVALID_USER'}, status=401)
            
            user = User.objects.get(email=email)
            result = {
                'id'      : user.id,
                'user_id' : user.user_id,
                'name'    : user.user_name,
            }
            return JsonResponse({'result': result}, status=200)  
        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)
        except json.JSONDecodeError:
            return JsonResponse({"MESSAGE": "JSONDecodeError"}, status=404)

 

훨씬 간결해지고 가독성도 높아진걸 느낄 수 있습니다.

이런 실수와 에러 메세지에 맞닥뜨리셨을 때 도움이 되었으면 합니다.

 

전에는 그 의미를 몰랐지만 에러메세지를 띄워줄 때마다 오히려 다행이라는 말의 의미를 이제는 알겠습니다.

에러들과 실수들을 통해서 계속 성장하고 있음을 느낍니다.

모두 힘내서 에러메세지들을 정복해 갑시다!!