Dajngo Trouble Shooting

손성수·2023년 7월 11일
0

DRF Vaildation

문제의 발생

  1. 비슷한 유효성을 검증하는 코드가 반복된다.
    예를 들어 비밀 번호의 경우
    회원 가입, 회원 정보 수정, 비밀 번호 찾기(재설정) 기능에서
    유효성 검증 단계를 거쳐야 하므로 여러 곳에서 똑같은 비밀번호 유효성 검증을 해야 하는 과정이 필요 하다.
views.py API 로직 처리
    def put(self, request):
        """
        비밀 번호 정보 수정
        """

        user = get_object_or_404(User, pk=request.user.pk)
        serializer = UserUpdatePasswordSerializer(user, data=request.data)
        
        if request data의 비밀번호 유효성 검사 과정:
        	return 비밀 번호가 올바르지 않아요 !!
            
        elif serializer.is_valid():
            ... 생략


문제의 발생 2

  • 때때로 API에서는 비밀번호 뿐만 아니라 다른 데이터의 유효성을
    검증해야 한다.
    예를 들어서 나의 프로젝트인 경우 이메일 인증 코드에 관한
    유효성을 검사 할때
    인증 시간은 유효한지, 인증 코드는 올바른지
    또 인증 코드는 발급 받았는지, 소셜 계정은 아닌지 등등 판단해야할 요소가 많아졌다.

  • 사용자에게 어떤 유효성 검사가 실패했는지 안내해줄 필요가 있었다.
    예를 들어서
    인증 코드 발급을 받지 않아서 유효성 검사가 실패한 거라면
    사용자에게 먼저 인증 코드를 발급 받으라 안내하고
    인증 코드 유효 기간이 만료 되었으면
    인증 코드를 다시 발급 받으라고 안내를 해야 했다.

  • 따라서 각각의 분기문에 status_code를 일일히 적어서 보내줘야 했다.
... 문제의 분기 처리들
if 어떤 유효성 검사1:
  return status_code = 4XX
elif 어떤 유효성 검사2:
  return status_code = 4XX
elif 어떤 유효성 검사3:
  return status_code = 4XX
elif 어떤 유효성 검사4:
  return status_code = 4XX
...생략

문제의 발생 3

Views.py에서 API처리 과정과 vaildation, 데이터 암·복호화 와
기타 처리 과정들이 섞이다보니 중복되는 코드도 발생하고
코드의 유지보수성도 낮아지고 가독성도 나빠졌다.



해결 방안 1

  • Serializer의 Vaildate 오버 라이딩
  1. serializer.is_valid()는 유효성을 검증하는 과정이다.
    필드의 유니크 속성은 유효한지,
    또는 max값은 유효한지 등 이미 편리한 유효성 검증 과정을
    대신해서 처리해 주고 있었다.
  2. ModelSerializer를 상속 받아 validate를 오버라이딩 하여 데이터 유효성을 검증 한다.
views.py

    def put(self, request):
        """
        비밀 번호 정보 수정
        """

        user = get_object_or_404(User, pk=request.user.pk)
        serializer = UserUpdatePasswordSerializer(user, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(
                {"msg": "Success"}, status=status.HTTP_200_OK
            )
            ....생략
serializer.py
class UserUpdatePasswordSerializer(serializers.ModelSerializer):
    ... 생략

    def validate(self, attrs):

        ...생략
        if 유효성 검증:
            raise ValidationError(결과 값 반환)
        return attrs

해결 방안 2

  • staticmethod와 classmethod 데코레이터를 활용한 모듈 관리
class Validate:
...생략
    @staticmethod
    def validated_password(password):
        """
        비밀 번호 검증
        """

        check = [
            lambda element: all(
                x.isdigit() or x.islower() or x.isupper() or (x in ['!', '@', '#', '$', '%', '^', '&', '*', '_']) for x
                in element),
            # 요소 하나 하나를 순환하며 숫자,소문자,대문자,지정된 특수문자 제외한 요소가 있을경우 False
			....생략
        ]
        for i in check:
            if not i(password):
                return False
        return True

vaildate.py
    @classmethod
    def user_password_update_validation(cls, instance, attrs):
        """
        비밀 번호 수정 유효성 검사
        현재 활용 범위 : 비밀 번호 재 설정
        """

        password = attrs.get('password')
        new_password = attrs.get('new_password')
        if instance.login_type != "normal":
            # 소셜 계정 사용자일 경우 False
            return [False, '소셜 계정 입니다.']
        elif not check_password(password, instance.password):
            # 데이터 베이스에 저장된 비밀번호와, 사용자가 입력한 비밀번호가 다를 경우
            return [False, '비밀번호가 일치하지 않습니다.']
        elif not cls.validated_password(new_password):
            # 변경하고자 하는 비밀번호가 유효성 검사를 통과하지 못할 경우
            return [False, '비밀 번호가 올바르지 않습니다.']
        else:
            return True


serializer.py
    def validate(self, attrs):
        """
        비밀번호 데이터 검증
        """

        verification_result = ValidatedData.user_password_update_validation(self.instance, attrs)
        if verification_result is not True:
            raise ValidationError(verification_result[1])
        attrs['password'] = attrs['new_password']
        return attrs


새로운 문제점

너무 자세한 response value
위의 vaildate.py를 보면 return값으로 어떤점이 잘 못된 점인지
너무나 자세하게 반환하고 있다.
따라서 프론트와 약속된 번호를 발송하여
어떠한 api에서 받은 번호는 어떤 에러인지 구분할 수 있도록 처리 했다.

Backend Server

    @classmethod
    def user_password_update_validation(cls, instance, attrs):
        """
        비밀 번호 수정 유효성 검사
        현재 활용 범위 : 비밀 번호 재 설정
        """

        password = attrs.get('password')
        new_password = attrs.get('new_password')
        if instance.login_type != "normal":
            # 소셜 계정 사용자일 경우 False
            return [False, '0']
        elif not check_password(password, instance.password):
            # 데이터 베이스에 저장된 비밀번호와, 사용자가 입력한 비밀번호가 다를 경우
            return [False, '1']
        elif not cls.validated_password(new_password):
            # 변경하고자 하는 비밀번호가 유효성 검사를 통과하지 못할 경우
            return [False, '2']
        else:
            return True

... Front Server

const response_json = await response.json()
const message_list = [
	'소셜 계정 사용자는 비밀번호 정보를 변경할 수 없습니다.',
	'입력하신 비밀번호가 사용자의 비밀번호와 일치하지 않습니다.',
	'비밀번호는 영문자,숫자,특수문자로 길이 5이상의 조건이 충족되어야 합니다.',
]
alert(message_list[Number(response_json.non_field_errors)])


궁금증

과연 올바르게 사용한 것일까?

  1. Serializer의 vaildate를 오버라이딩 하여 데이터 유효성 검증을 구현

  2. 이때 여러곳에서 똑같은 유효성 검증을 하게 되는 상황이 발생하여
    중복된 코드들이 여러 발생

  3. 따라서 vaildated.py라는 임의의 파일을 생성하고,
    객체 지향성을 위해 각각의 기능 별로 class를 나누어
    이메일 발송 기능을 하는 class,
    휴대폰 인증 기능을 하는 class,
    데이터 유효성을 검증하는 class를 구현

  4. 참조 방법은 staticmethod와, classmethod를 사용하여
    정적,추상 메서드를 구현하게 되었고
    각각 용도에 맞게 메서드를 호출하도록 구현

  5. 돌이켜 바라보니
    모듈로 빼지 않아도 될 것 같을 것 같은 메서드가 있었고
    모듈로 빼는게 좋을 것 같다는 생각이 드는 메서드가 발생
    예시 사진

    해당 메서드를 사용하는 활용 범위가 여러개인 메서드가 있는 방면
    해당 메서드를 한 곳에서만 호출하는 메서드가 있다.

  6. 과연 유효성을 검증하는 모든 코드는
    모듈로써 관리하는게 적합한지, 또는 상황에 따라서 유연하게
    대처하는것이 적합한지 궁금합니다.

  7. serializer의 validate를 오버라이딩 하여 구현하여서
    vaildate 안에서 구현한 유효성 검증에 실패할 경우
    동일한 status_code를 반환하게 되었습니다.
    제 프로젝트의 경우에는 400 bad request를 반환하도록 구현 되어 있습니다.

  8. 사용자에게 어떤 유효성 검증이 실패했느냐에 관해 안내해 드릴 필요가 있었기
    때문에 프론트와 백엔드의 약속 된 msg 번홀르 지정하게 되었습니다.
    예시) 1 : 비밀번호 유효성 검사 실패, 2 : 닉네임 유효성 검사 실패 ..

  9. 과연 response value에 임의의 숫자를 지정하여
    발송하고 프론트와 약속하는것이 올바른 구현 방법일까?
    유지 보수성과 가독성에 문제가 있지는 않을까?
    별도의 설명이 없다면 다른 사람이 보기에는 코드를 이해할 수 없는데
    그만큼 보완성이 강화된것이니 장점이 된것일까?

나의 결론

  • vaildate.py는 말 그대로 데이터를 검증하기 위한 객체들의 집합을
    class로 묶은 것이므로, 하나 또는 여러개를 데이터 검증하더라도
    사용 자체는 문제가 되지 않는다.
    다만 주의할점은 내가 구현한 class는 데이터 검증을 위한 모듈의 집합이므로
    이와 별개로 다른 용도로 쓰이는 메서드가 섞이는 것은 안될 일 이다.

  • status를 msg로 보내는건 결국, 자바스크립트 파일을 열람한다면 공격자 입장에서 어떤 용도로 쓰이는지 추론이 가능하다.
    하지만 유추하기 어렵도록 난독화 한것은 적합하다.

  • 그러나 너무나 자세한 정보를 전달하는것은 여전히 적합하지 않으니
    중요한 정보에 관해서는 범위성을 넓혀서 status_code를 반환하는것이 적합하지 싶다.
profile
더 노력하겠습니다

0개의 댓글