Notice
Recent Posts
Recent Comments
Link
오늘도 개발
[2차 프로젝트 내일의 집] 카카오 api로 SNS 로그인/회원가입 리팩토링 본문
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')'TIL & 프로젝트 회고' 카테고리의 다른 글
| [2차 프로젝트 내일의 집] 프로젝트 최종 회고 (0) | 2022.08.13 |
|---|---|
| [2차 프로젝트 내일의 집] AWS s3로 글쓰기 api 구현하기 - 3. 리팩토링 (0) | 2022.08.09 |
| [2차 프로젝트 내일의 집] AWS s3로 글쓰기 api 구현하기 - 2. Django 세팅(boto3)과 코드 작성 (0) | 2022.08.08 |
| [2차 프로젝트 내일의 집] AWS s3로 글쓰기 api 구현하기 - 1. AWS 세팅(IAM 설정, S3 설정) (0) | 2022.08.08 |
| [2차 프로젝트 내일의 집] 상품 상세 api - Django ORM 최적화하기 심화 (0) | 2022.08.08 |