[SocialLogIn] Kakao API

kimlilo·2021년 12월 26일
1

소셜로그인이 끝나고 한참이 지나고서야 칭찬(?)받은,,, 박제하고 싶은 내 메모들,,, 2차도 역시 스파르타 학습가이드,,, 너무 좋다구여,,,


카카오 소셜 로그인 API를 활용해서, 기능을 구현하는데 있어서 가장 고민이 깊었던 부분은

1. 프론트와 백이 통신하는 시점

2. 백과 카카오가 통신하는 시점

3. 백에서 카카오 서버에 어떻게 request를 보내지?

4. 그리고 return과 raise와 unittests


이들과 나의 싸움이 시작됐다 흫


requests document + Kakao request url


핵심은,
back은 request에 대하여 응답만 했지 보낼수도 있었고!
처음 kakao document를 읽었을 때, 이 질문에 대하여 끊임없는 답답함의 연속, 그것이 엄청났던 것이 떠오른다.
(그래서 requests 쓰면 되는거냐는 질문에 그 누구도 대답을 해주지 않았음 흫)


MH님과 리퀘스트도큐먼트대탐험카카오도큐먼트대탐험하면서,
고통스러웠지만,

너무너무 재밌었다

이곳에 도착했을 때,
진짜 세상 너무 재밌고 너무 좋았던!
그 방에서,,, 우릴 보는 멘토님들,,, 우리 얼마나 욱겨보였을까,,, 그렇게 좋아했는데,,,


그렇게 리퀘스트유알엘 만들기 대장정이 끝나면 이제 뷰만 쓰면 되지?
되,,,,,,지,,,,,???
일단 의식의 흐름대로
쓴다. 한글로. 뷰를

class SignInView(View):
    def get(self, request):
        try:
            #1. front로 부터 토큰 받기 > headers
            #2. 카카오로 user 정보 요청하기
            #3. 정보받기(kakao id, email, 이름)
            #   받아올 정보 kakao_account 추가 ()
            #4. user id 가져오기 kakao_id = result["id"]
            #5. user table에 확인 > get or create / > user table에 저장
            #6. 있는 user라면 jwt token 발급
            #7. front 전달
            #8. error 처리
            access_token  = request.headers.get('Authorization', None)
            # print(access_token)
            headers       = {"Authorization" : f"Bearer {access_token}"}
            url           = "https://kapi.kakao.com/v2/user/me?secure_resource=true&property_keys=%5B%22properties.nickname%22%2C%22kakao_account.email%22%5D" 
            result        = requests.get(url, headers = headers)
            # print(result)
            kakao_data    = result.json()

            if result.status_code != 200:
                return JsonResponse({'message' : result.json()}, status = result.status_code)
            
            # result.json()

            kakao_data    = result.json()
            kakao_id      = kakao_data['id']
            # print(kakao_id)
            email         = kakao_data['kakao_account']["email"]
            # print(email)
            name          = kakao_data['properties']["nickname"]
            # print(name)
    
            user, created      = User.objects.get_or_create(
                kakao_id       = kakao_id,
                defaults       = {
                    'name'     : name,
                    'email'    : email,
                }
            )

            token = jwt.encode({'id': user.id}, SECRET_KEY, ALGORITHM)
            return JsonResponse({'message' : 'SUCCESS', 'token' : token}, status = 201)

        except KeyError:
            return JsonResponse({'message' : 'UNAUTHORIZED'}, status = 401) 

        
class LogOutView(View):
    def post(self, request):
       
        access_token  = request.headers.get('Authorization', None)
        headers       = {"Authorization" : f"Bearer {access_token}"}
        url           = "https://kapi.kakao.com/v1/user/logout" 
        result        = requests.post(url, headers = headers)

        if result.status_code != 200:
            return JsonResponse({'message' : result.json()}, status = result.status_code)
            
        return JsonResponse({'kakao_id' : result.json()}, status = 200)

리뷰 & 리뷰 & 리뷰

  • 카카오에서 request로 받은 json메세지 그래로 활용하여 에러처리
  • 카카오 API class
  • unit tests

A. 카카오 API 관련된 로직을 class로 그룹핑
  • Kakao API 파트는 core > utils 파일로 이동시키고, KakaoLoginView에서는 Kakao API를 호출하여 사용하도록 함
  • Kakao API 관련된 로직을 하나의 class에서 관리, 추후 KakaoAPI 호출부의 수정일 있을 경우, KakaoLoginView를 수정할 필요 없이 KakaoAPI 클래스만 수정하도록 함

B. Kakao에서 request로 받은 json 메세지 활용하여 1차 에러처리

  • 에러 처리 : KakaoAPI 함수에서 에러코드를 1차로 처리 및 카카오에서 보낸 에러 메세지와 status code를 그대로 사용하도록 구현 > status_code가 200이 아닌 경우, 함수를 끝내고 이후 과정을 진행하지 않도록 하는 로직을 구현하고자 함

    1) return : 에러가 발생하더라도, 그대로 에러 메세지를 담아 KakaoLoginView를 실행하면서 결과적으로 error 발생

    또한 이 경우, view > (module) > db > View로 다시 돌아와 종료 되는 것이 아닌 모듈에서 json response를 반환해버리고 종료 되는 것은 정상 루트가 아니라고 할 수 있는데 그 개념이 뭐였더라,,,,,;;;;

    2) raise : 해당 에러를 반환해주는 error를 exception으로 발생시키고 KakaoLoginView에서 에러 메세지를 출력(e)해줄 수 있지만, 어떤 에러인지 정확하게 파악할 수 없음

    또한 이 경우, unittest를 진행하였을 때 아래와 같은 에러 발생
    TypeError: catching classes that do not inherit from BaseException is not allowed

  • 결과 : KakaoAPI에서의 에러 처리를 하지 않고, 모든 response를 KakaoLoginView로 그대로 반환하고, 1차 에러 처리함(return)

C. unittests

AttributeError: 'MockedResponse' object has no attribute 'status_code'

  • B-2)의 결과처럼 테스트를 실행하게 되는 경우, 정상적인 response를 반환할 때, 아래의 에러 발생
    if not response.status_code == -401:
    AttributeError: 'MockedResponse' object has no attribute 'status_code'
    During handling of the above exception, another exception occurred
  • 테스트 파일의 MockedResponse의 데이터가 정상 값을 반환할 때, status_code를 반환하지 않아서, 결과를 비교할 수 없음
    이 때,
    .__dir__()를 찍어보면???
    ['__module__', 'json', '__dict__', '__weakref__', '__doc__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']

    당연히 없겠지???

    따라서, if kakao_response.get('code') == -401: 과 같이
    response 객체({'msg': 'this access token does not exist', 'code': -401})의 'code'의 값을 불러 오는 코드로 수정

또는!

class SignInTest(TestCase):
    @patch("core.utils.KakaoAPI.requests")
    def test_kakao_signin_new_user_success(self, mocked_requests):
        client = Client()

        class MockedResponse:
            status_code = 200
            
            def json(self):
                return {
                    "id": 2033314461,
                    "connected_at": "2021-12-14T09:03:16Z",
                    "properties": {
                        "nickname": "김은혜"
                    },
                    "kakao_account": {
                        "has_email": True,
                        "email_needs_agreement": False,
                        "is_email_valid": True,
                        "is_email_verified": True,
                        "email": "jino63@naver.com"
                    }
                }       
                
        mocked_requests.get = mock.MagicMock(return_value = MockedResponse())
        headers             = {"HTTP_Authorization" : "access_token"}
        response            = client.post("/users/signin", **headers)

        self.assertEqual(response.status_code, 201)

status_code = 200 라고 선언을 해주고,
print(MockedResponse().__dir__())를 찍어보면, 놀랍게도

'__module__', 'status_code', 'json', '__dict__', '__weakref__', '__doc__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__'

status_code가 생겨있지,

해결? 아직,

D. object(객체)와 dictionary / status_code

  • 클래스를 인스턴스로 할당하여 카카오 서버를 호출하는 함수의 response는 딕셔너리로 받음(reponse.json())
  • 딕셔너리에는 status_code를 사용할 수 없음 (값이 전달 되지 않음)
    따라서, response.status_code 형태로 사용 가능함

결론

return으로 jsonresponse를 반환하는 것은, validator때도 이미 여러차례, 데코레이터 선생님을 붙잡고 공부를 했지만, try 안의 함수(class) 호출 부분 이후 부터의 코드를 그대로 실행하기 때문에, 의도한 로직에 의하면 맞지 않다.

결국 raise로 error를 발생시키고 이후 함수는 종료 시켜야 하는데, unittests를 통과하지 않으며(typeerror???), 메세지(e)를 준다고 해도 무슨 에러인지 정확하게 반환하지 않음.

그럼 어짜피 view로 돌아와 종료해야 함과 의도했던 로직(카카오에서 돌아온 response가 200이 아니면 종료)을 실행하기 위해서 모듈에서 response를 (200이든 -400이든 간에) view로 전달하고, 판단은 view에서 하는 것,,,
여기까지가 맞다고 인정이 되고,

class에 status_code 얹고 + raise 추가에 + unittests 돌리기


결국 이 삼위일체 찐막으로 리팩토링 하지 못한 것이 끝끝내 신경쓰이는데 조만간
한다. 래이즈. 한다. 유닛테스트.

하 이게 이렇게 된 데는 총 3명의 멘토님의 리뷰가 있었음인데 너무 썰 풀고 싶네 :-)
이프낫이백이 불러온 파급효과,,,,,

profile
킴릴로

0개의 댓글