오늘도 개발

[2차 프로젝트 내일의 집] 카카오 api로 SNS 로그인/회원가입 리팩토링 본문

TIL & 프로젝트 회고

[2차 프로젝트 내일의 집] 카카오 api로 SNS 로그인/회원가입 리팩토링

Sueeeeeee 2022. 8. 10. 10:30

0. 리팩토링 전 코드의 문제점

글쓰기 api와 마찬가지로 로그인 api도 캡슐화, 모듈화가 되어 있지 않다는 문제가 있었다.

리팩토링 전 로그인 api는 다음 세 기능을 모두 포함했다.

 

1) 카카오에 요청해서 액세스 토큰 받기

2) 카카오에 요청해서 유저 정보 받기

3) 내일의 집 로그인/회원가입 처리하기

 

리팩토링하면서 1, 2는 클래스로 만들어 관리하고 로그인 api에는 3번 기능만 남기는 데 초점을 맞췄다.

리팩토링 전 코드는 이러했다.

class LoginView(View):
    def get(self, request):
        try:
            # 1. 프론트에서 보내준 인가코드 받기 
            code = request.META.get('HTTP_AUTHORIZATION')
            # 인가 코드 없으면 오류 반환
            if not code:
                return JsonResponse({"message" : "INVALID_AUTHORIZATION_CODE"}, status=401)
			
            # 액세스 토큰 요청 바디에 넣을 데이터 준비
            data = {
                'grant_type' : 'authorization_code',
                'client_id' : settings.KAKAO_REST_API_KEY,
                'redirect_uri' : settings.REDIRECT_URI,
                'code' : code
            }
            # 2. 카카오에 액세스 토큰 요청
            token_response = requests.post("https://kauth.kakao.com/oauth/token", data=data).json()
            access_token   = token_response.get('access_token')
          	
            # 액세스 토큰 없으면 오류 반환
            if not access_token:
                return JsonResponse({"message" : "INVALID_ACCESS_TOKEN"}, status=401)
			
            # 3. 유저 정보 요청 헤더에 넣을 데이터 준비
            headers = {
                'Authorization': f'Bearer {access_token}'
            }
            # 카카오에 유저 정보 요청
            user_data_response = requests.post("https://kapi.kakao.com/v2/user/me", headers=headers).json()
            kakao_id = user_data_response.get('id')
            
            # 유저 정보 없으면 오류 반환
            if not kakao_id:
                return JsonResponse({"message" : "INVALID_KAKAO_ID"}, status=401)
			
            # 유저 정보 가져오기
            kakao_account = user_data_response['kakao_account']
            email         = kakao_account.get('email')
            nickname      = None 
            profile_image = None
            profile       = kakao_account.get('profile')

            if profile:
                nickname      = profile.get('nickname')
                profile_image = profile.get('profile_image_url')
            
            # DB 확인해보고 존재하는 유저면 jwt 발급, 아니면 회원가입 후 jwt 발급
            user, is_created = User.objects.get_or_create(
                kakao_id = kakao_id,
                defaults = {
                    "email"        : email,
                    "nickname"     : nickname,
                    "profile_image": profile_image
                }
            )
            nhouse_token = jwt.encode({"user_id" : user.id}, settings.SECRET_KEY, settings.ALGORITHM)

            if not is_created:
                if not user.email == email:
                    user.email = email 
                    user.save()
                if not user.profile_image == profile_image:
                    user.profile_image = profile_image
                    user.save()
                if not user.nickname == nickname:
                    user.nickname = nickname
                return JsonResponse({"message" : "LOGIN_SUCCESS", "access_token" : nhouse_token}, status=200)

            return JsonResponse({"message" : "SIGNUP_SUCCESS", "access_token" : nhouse_token}, status=201)  

        except KeyError:
            return JsonResponse({"message":"KEY_ERROR"}, status=400)

 

1.  views.py 리팩토링

KakaoAPI 클래스를 만들어서, 

다음 중 1번 과정은 get_access_token 메서드가,

2번 과정은 get_kakao_user_data 메서드가 처리하도록 만들었다.

 

1) 카카오에 요청해서 액세스 토큰 받기

2) 카카오에 요청해서 유저 정보 받기

3) 내일의 집 로그인/회원가입 처리하기

 

이제 LoginView에는 3번 과정을 처리하는 코드만 있으면 된다.

class KakaoAPI:
    def __init__(self, config):
    	# 카카오 api 사용에 필요한 앱 키, redirect URI 등을 설정하는 부분이다.
        # my_settings.py에 적어둔 KAKAO_API_CONFIG 딕셔너리로 설정할 예정이다.
        self.config = config 
    
    # 카카오에 요청해서 액세스 토큰 받기
    def get_access_token(self, authorization_code):
        data = {
                'grant_type' : 'authorization_code',
                'client_id' : self.KAKAO_CONFIG['api_key'],
                'redirect_uri' : self.KAKAO_CONFIG['redirect_uri'],
                'code' : authorization_code
            }
        token_response = requests.post("https://kauth.kakao.com/oauth/token", data=data).json()
        access_token   = token_response.get('access_token')
          
        return access_token
	
    # 카카오에 요청해서 유저 정보 받기
    def get_kakao_user_data(self, access_token):
        headers = {
                'Authorization': f'Bearer {access_token}'
            }
        user_data = requests.post("https://kapi.kakao.com/v2/user/me", headers=headers).json()
        return user_data

class LoginView(View):
    def get(self, request):
        try:
            code = request.META.get('HTTP_AUTHORIZATION')
            if not code:
                return JsonResponse({"message" : "INVALID_AUTHORIZATION_CODE"}, status=401)
            
            # 카카오 api 호출 준비
            kakao = KakaoAPI(settings.KAKAO_CONFIG)
            
            # KakaoAPI 메서드로 호출
            access_token = kakao.get_access_token(code)
            if not access_token:
                return JsonResponse({"message" : "INVALID_ACCESS_TOKEN"}, status=401)
			
            # KakaoAPI 메서드로 호출
            user_data = kakao.get_kakao_user_data(access_token)
            kakao_id = user_data.get('id')
            
            if not kakao_id:
                return JsonResponse({"message" : "INVALID_KAKAO_ID"}, status=401)

            kakao_account = user_data['kakao_account']
            email         = kakao_account.get('email')
            nickname      = None 
            profile_image = None
            profile       = kakao_account.get('profile')

            if profile:
                nickname      = profile.get('nickname')
                profile_image = profile.get('profile_image_url')
                
            user, is_created = User.objects.get_or_create(
                kakao_id = kakao_id,
                defaults = {
                    "email"        : email,
                    "nickname"     : nickname,
                    "profile_image": profile_image
                }
            )
            
            nhouse_token = jwt.encode({"user_id" : user.id}, settings.SECRET_KEY, settings.ALGORITHM)

            if not is_created:
                if not user.email == email:
                    user.email = email 
                    user.save()
                if not user.profile_image == profile_image:
                    user.profile_image = profile_image
                    user.save()
                if not user.nickname == nickname:
                    user.nickname = nickname
                    user.save()
                return JsonResponse({"message" : "LOGIN_SUCCESS", "access_token" : nhouse_token}, status=200)

            return JsonResponse({"message" : "SIGNUP_SUCCESS", "access_token" : nhouse_token}, status=201)  

        except KeyError:
            return JsonResponse({"message":"KEY_ERROR"}, status=400)

 

2.  test.py 리팩토링

LoginView에 모든 기능이 들어있을 때는 side_effect를 사용해서 두 번의 외부요청을 패치했다.

KakaoAPI 클래스를 만들자 테스트코드에 patch 데코레이터를 두 번 사용해서 처리할 수 있었다.

전자보다 후자가 가독성이 좋고, TDD식의 개발에 더 적합한 것 같다.

class LoginViewTest(TestCase):
    def setUp(self):
        User.objects.create(
            id = 1,
            kakao_id = 12345,
            email = 'snoopy@gmail.com',
            nickname = 'Snoopy',
            profile_image = 'http://snoopy.com'
        )

    def tearDown(self):
        User.objects.all().delete()
    # KakaoAPI 오브젝트의 get_kakao_user_data에 test_success..함수의 두번째 인자를 넣음 
    @patch('users.views.KakaoAPI.get_kakao_user_data')
    # KakaoAPI 오브젝트의 get_access_token에 test_success..함수의 첫번째 인자를 넣음 
    @patch('users.views.KakaoAPI.get_access_token')
    def test_success_login_with_same_nickname_email_profile_image(self, mocked_access_token, mocked_user_data):
        client = Client()

        mocked_access_token.return_value = '1234'
        mocked_user_data.return_value = {
                    'id': 12345, 
                    'kakao_account': {
                        'profile': {
                            'nickname': 'snoopy', 
                            'thumbnail_image_url': 'http://snoopy.com', 
                            'profile_image_url': 'http://snoopy.com'
                            }, 
                        'email': 'snoopy@gmail.com'
                        }
                    }

        header = {'HTTP_AUTHORIZATION' : '12341234'}
        response = client.get('/users/login', content_type='applications/json', **header)
        
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json().get('message'), 'LOGIN_SUCCESS')