본문 바로가기
Project

Watchapedia 백엔드 개발

by yongmin.Lee 2020. 10. 25.

Description  

  • 왓챠피디아(Watchapedia)는 영화평가를 기반으로 사용자의 취향을 분석하고, 이에 따라 추천까지 해주는 서비스입니다.
  • 팀원: 프론트개발 3명, 백엔드개발 2명
  • 개발기간: 2020.08.18 ~ 2020.08.28 (10일)
  • Tech stack : Django, Selenium, Mysql, AWS EC2, AWS RDS
  • Backend Github : github.com/eymin1259/11-WeCha-backend

Demo Video

What I did

  • 정규화를 통한 데이터 모델링
  • Selenium 을 통해 영화 데이터 크롤링
  • Bcrypt를 활용하여 비밀번호 암호화 및 회원가입 로직 구현
  • JWT를 활용하여 access token 생성 및 로그인 로직 구현
  • Access token과 Authorization decorator를 이용하여 영화 리스트 조회 API 구현
  • 마이페이지 API 구현
  • Django ORM을 통해 리뷰 CRUD API 구현
  • AWS를 이용한 클라우드 환경의 서버 구현

Modeling

What I learned

1. Authentication(인증), Authorization(인가) 로직

 

인증(Authentication) 
Authentication은 유저의 identification을 확인하는 절차로써, 회원가입, 로그인이 대표적이며, 우리 서비스를 누가, 어떻게 사용하는지 추적 가능하도록 하기 위해 필요하다.

 

인증(Authentication) 프로세스

  1. 회원가입 : 유저 아이디와 비밀번호 생성.
  2. 유저 비밀번호를 "암호화" 하여 DB에 저장.
  3. 로그인 : 아이디와 비밀번호 입력.
  4. 유저가 입력한 비밀번호를 암호화 한후 암호화되서 DB에 저장되어있는 암호화 비밀번호와 비교.
  5. 일치하면 로그인 성공.
  6. 로그인 성공하면 access token을 클라이언트에게 전송.
  7. 유저는 로그인 성공후 다음부터는 access token을 첨부해서 request를 서버에 전송함으로서 매번 로그인 해도 되지 않도록 함.

Bcrypt
대표적인 암호 인증 메커니즘으로 Bcrypt가 많이 사용된다.
단방향 해쉬 함수로 대의 취약점들을 보안하기 위해 일반적으로 2가지 보완점들이 같이 사용된다.
Salting: 실제 비밀번호 이외에 추가적으로 랜덤 데이터를 더해서 해시값을 계산하는 방법.
Key Stretching: 단방향 해쉬값을 계산한 후 그 해쉬값을 또 해쉬 하고, 이를 반복하는것.

 

회원가입 로직 구현코드

class SignUp(View):
    def post(self, request):
        try:
            data = json.loads(request.body)
            signup_email = data['email'] 
            signup_pw    = data['password']
            signup_name  = data['name']
            # duplicate email validation
            if User.objects.filter(email=signup_email).exists() : # duplicate email exists
                return JsonResponse( {"message": "DUPLICATE_EMAIL_ERROR"}, status=401)
            #password validation
            if not password_validation(signup_pw):
                return JsonResponse({"message": "PASSWORD_VALIDATION_ERROR"}, status=401)
            # 비밀번호 암호화
            encrypted_pw = bcrypt.hashpw(signup_pw.encode('utf-8'), bcrypt.gensalt())
            # 회원가입이 성공하면 {"message": "SUCCESS"}, status code 200을 반환합니다.
            User(
                email          = signup_email,
                password       = encrypted_pw.decode('utf-8'),
                name           = signup_name,
                face_image_url = default_image
            ).save()
            return JsonResponse({'message':'SIGNUP_SUCCESS'}, status=200)
        except JSONDecodeError:
            return JsonResponse({"message":"JSONDecodeError"}, status=401)
        except KeyError:
            return JsonResponse({"message": "KEY_ERROR"}, status=401)

 

인가(Authorization)
Authorization은 유저가 요청하는 request를 실행할 수 있는 권한이 있는 유저인가를 확인하는 절차이다.
access token을 통해 유저 정보를 얻고, 해당 유저가 가지고 있는 권한(permission)도 확인한다.

 

인가(Authorization) 절차

  1. 인증(Authentication) 절차를 통해 access token을 생성. access token에는 유저 정보를 확인할 수 있는 정보가 들어가 있음. (예를 들어 user id)
  2. 유저가 request를 보낼때 access token을 첨부해서 송신.
  3. 서버에서는 유저가 보낸 access token을 복호화 하여 유저 정보를 조회.  (예를 들어 user id)
  4. user id를 이용하여 database에서 해당 유저의 권한(permission)을 확인.
  5. 유저가 충분한 권한을 가지고 있으면 해당 요청을 처리.
  6. 유저가 권한을 가지고 있지 않으면 다른 로직을 실행.

JWT, JSON Web Tokens
JWT는 웹표준으로서 서버와 클라이언트 사이에서 JSON 객체를 사용하여 가볍고 필요한 모든 정보를 자체적으로 담고있는 방식으로, 유저가 서버에 요청을 할 때 마다 JWT를 포함하여 전달한다. 서버가 클라이언트에게서 요청을 받을때 마다, 해당 토큰이 유효하고 인증됐는지 검증을 하고, 유저가 요청한 작업에 권한이 있는지 확인하여 작업을 처리한다.

서버측에서는 유저의 세션을 관리할 필요 없어서 서버 자원을 아낄수있다.

 

Authorization 로직 구현코드

def token_authorization(func):
    def wrapper(self, request, *args, **kwargs) :
        try:
            token        = request.headers.get('Authorization', None)         
            payload      = jwt.decode(token, SECRET_KEY, algorithm=TOKEN_ALGORITHM)  
            user_info    = User.objects.get(id=payload['user_id'])
            request.user = user_info # user 정보를 request에 저장하여 이후 활용

        except jwt.exceptions.DecodeError: #토큰이 없거나 토큰 형태가 유효하지 않는 경우                                    
            request.user = None
        except User.DoesNotExist:          # 해당 토큰에 대한 유저정보가 없는 경우
            request.user = None
        return func(self, request, *args, **kwargs)
    return wrapper 

 

2. select_related() & prefetch_related()를 통한 DB 최적화

select_related 와 prefetch_related 는 하나의 QuerySet을 가져올 때, 미리 related objects들까지 다 불러와주는 함수이다. 비록 query를 복잡하게 만들긴 하지만, 그렇게 불러온 data들은 모두 cache에 남아있게 되므로 DB에 다시 접근해야 하는 수고를 덜어줄 수 있다. 이렇게 두 함수 모두 DB에 접근하는 수를 줄여, performance를 향상시켜준다는 측면에서는 공통점이 있지만, 그 방식에는 차이점이 있다.
 
select_related
Database에서 JOIN 쿼리를 수행하여 두 테이블의 정보를 가져와서 캐싱한다. select_related 은 SQL의 JOIN을 사용하는 특성상 many-to-many 관계에서 사용하는 경우 result set이 너무 거대해질 수 있는 문제가 존재하므로 foreign-key, one-to-one 와 같은 single-valued relationships에서 주로 사용된다.
 
prefetch_related
select_related 와 달리 별도의 2개의 쿼리를 수행후에 파이썬에서 조인한다. 성능은 select 보다 느리지만,  many-to-many 관계에서 사용하거나, 역참조시 사용하여 DB 연산을 최적화 할 수 있다.

 

select_related 이용하여 최적화

class UserInfo(View):
    @token_authorization
    def get(self, request):
        user_info = request.user            
        if not user_info: # 유저 정보가 없는 경우
            return JsonResponse({"message": "INVALIDE_USER"}, status=400) 
        user_name    = user_info.name
        user_profile = user_info.face_image_url
        MAXIMUM_NUMBER_REVIEWS = 10
        # 유저가 평가한 영화들
        reviewed_films = Review.objects.filter(user = user_info).select_related('film')[0:MAXIMUM_NUMBER_REVIEWS]
        user_review_films = [
            {   "title"      : reviewed_film.film.korean_title,
                "rating"     : reviewed_film.score,
                "poster_url" : reviewed_film.film.poster_url
            } for reviewed_film in reviewed_films
        ]
        response = {
            "user_name"         : user_name,
            "user_profile"      : user_profile,
            "user_review_films" : user_review_films
        }
        return JsonResponse(response, status=200)