DRF Celery를 사용한 이메일 인증 서비스 구현

강태원·2024년 5월 24일
0

개발중인 서비스에서 이메일 인증 서비스를 한 번 도입해보기로 했다.
널리 쓰이는 Gmail을 통해서 보내볼거고, 꽤나 오래 걸리는 작업이므로 Celery에 Task를 보내 백그라운드에서 처리할 것이다.

Gmail 설정하기

장고와 같은 외부 앱에서 Gmail을 사용하려면 앱 비밀번호가 필요하다
비밀번호 설정하기 <= 이 링크를 타고 들어가서 비밀번호를 생성해주도록 하자.


필자가 테스트용으로 만들어본 앱비밀번호 16자리이다.
이 화면에서 벗어나면 다시 확인할 수가 없으니 확인 버튼을 누르기 전에
꼭 미리 저장해놔야 한다.

위 비밀번호는 외부에 반출되면 안되므로 프로젝트의 env 파일에 잘 숨겨두자.

이메일 인증 로직

이메일을 보내기 전에 구현해볼 인증 로직을 먼저 설명해보도록 하겠다.

  1. 유저에게서 이메일을 입력받고 주소로 랜덤 코드를 발송한다.
  2. 서버에서는 redis에 이메일:랜덤 코드 쌍으로 5분의 유효기간을 주고 저장한다.
  3. 유저가 코드 확인 후 검증 API로 확인한 코드를 전송한다.
  4. 서버에서 코드의 일치 여부를 확인하고 일치한다면 이 코드의 유효기간을 삭제한다.
  5. 코드를 클라이언트 측으로 반환해준다.
  6. 회원가입 시 클라이언트는 유저정보와 함께 이 코드를 같이 서버로 보내 이메일 확인이 되었음을 증명한다.
  7. 회원가입 성공 시 서버에서는 이메일:랜덤 코드를 redis에서 제거한다.

Django에서 이메일 보내기

장고에서는 이메일을 보내는 클래스가 이미 존재한다.
(역시 batteries included 철학답다)

#project_name/settings.py

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = env("EMAIL_ID")
EMAIL_HOST_PASSWORD = env("EMAIL_PW")
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

프로젝트의 settings.py에 위 내용을 추가해주도록 하자.
env 파일에 이메일을 보내는 주체(EMAIL_ID), 위에서 생성한 비밀번호(EMAIL_PW)가 존재한다는 가정 하에 작성된 내용이다.

env 파일을 읽어오는 방법은 사용자에 따라 가지각색이므로 어떻게 불러와도 상관없다.


위 이메일 인증 로직을 따라가면서 구현해보자.

로직 1번

먼저 랜덤 코드를 생성해 이메일로 발송해야한다.

#project_app/utils.py NEW

import strings
import secrets

class SendEmailHelper:
    def make_random_code_for_register():
        digit_and_alpha = string.ascii_letters + string.digits
        return "".join(secrets.choice(digit_and_alpha) for _ in range(6))
        
sendEmailHelper = SendEmailHelper()

이메일을 보내는 앱에 utils.py를 생성해 랜덤한 6자리의 코드를 생성해보자.
위 코드를 사용하면 알파벳+숫자로 이루어진 코드가 생성된다.

#project_app/views.py

from django.core.mail import EmailMessage
from .utils import sendEmailHelper

class EmailVerifyView(APIView):

	def post(self, request, *args, **kwargs):
    	email = request.data.get("email")
        code = sendEmailHelper.make_random_code_for_register()
        message = code
        subject = "EMAIL 제목"
        to = [email]
        mail = EmailMessage(subject=subject, body=message, to=to)
        mail.content_subtype = "html" # html형태로 템플릿을 만들었을 때 필요함
        mail.send()
        
        return Response({"detail": "Success to send Email"}, status=status.HTTP_202_ACCEPTED)
        

간단하게 유저로부터 이메일을 입력 받아 주소로 랜덤 코드 6자리를 보내는 코드이다.

message에는 이메일의 내용이, subject에는 제목이, to에는 받는 주소가 들어가야 한다.
(to에는 반드시 튜플이나 리스트의 형태의 데이터가 들어가야 함을 주의하자)

포스트맨으로 테스트를 한 번 해보자.

오! 이메일이 잘 들어왔고, 포스트맨을 보면?

응답도 잘 들어와있으나 시간을 한 번 보시라. 4초에 가까운 시간이 걸리는데 이를 일반적인 HTTP 통신 상에서 처리하려고 하면 안 된다.

이를 지난 포스트에서 다루었던 Celery를 이용해 백그라운드로 넘겨서 진행해주자.

위 링크를 통해 기본적인 Celery 세팅은 다 되었다고 판단하고 진행하겠다.

#project_app/tasks.py NEW

from django.core.mail import EmailMessage

from wtnt.celery import app
from .utils import sendEmailHelper


@app.task
def send_email(email):
    code = sendEmailHelper.make_random_code_for_register()
    message = sendEmailHelper.get_template(code)
    subject = "%s" % "[WTNT] 이메일 인증 코드 안내"
    to = [email]
    mail = EmailMessage(subject=subject, body=message, to=to)
    mail.content_subtype = "html"
    mail.send()
    return "Success to send email"

tasks.py에 이메일 보내는 함수를 따로 빼내어 작성했다.
필자는 sendEmailHelper에 이메일의 템플릿을 따로 작성해두어 이메일의 내용에 약간의 구색을 갖춰두었다.

#project_app/views.py

from .tasks import send_email

class EmailVerifyView(APIView):

    def post(self, request, *args, **kwargs):
        email = request.data.get("email")
        try:
            send_email.delay(email)
            return Response({"detail": "Success to send Email"}, status=status.HTTP_202_ACCEPTED)
        except Exception as e:
            return Response({"error": e}, status=status.HTTP_400_BAD_REQUEST)

Celery에 task를 할당하려면 delay 함수를 사용하면 된다. 좀 더 자세히 알아보고 싶다면 공식 문서를 참고해보는게 좋다.

celery -A project worker -l INFO

Celery를 켜주고 같은 API에 요청을 보내보자.

Celery에 요청이 received되고 앞에서 포스트맨을 사용했을 때와 비슷한 시간이 걸려서 Task를 완수했다.

이메일도 예쁘게 도착한 모습.

그리고 포스트맨에서는? 125ms 밖에 걸리지 않았다. 야호!

이제 로직의 1번을 달성했다.

로직 2번

redis에 이메일:랜덤 코드 쌍을 유효기간 5분을 두고 저장한다.
이는 매우 간단하다.

from django_redis import get_redis_connection
from django.core.mail import EmailMessage

from wtnt.celery import app
from .utils import sendEmailHelper

client = get_redis_connection("default")


@app.task
def send_email(email):
    code = sendEmailHelper.make_random_code_for_register()
    client.set(email, code, ex=300)
    message = sendEmailHelper.get_template(code)
    subject = "%s" % "[WTNT] 이메일 인증 코드 안내"
    to = [email]
    mail = EmailMessage(subject=subject, body=message, to=to)
    mail.content_subtype = "html"
    mail.send()
    return "Success to send email"

tasks.py를 위와 같이 변경해주었다.

client.set(email, code, ex=3000)

이 코드가 함수 안에 추가된 것인데, 이는 email 키에 code값을 300초(5분)동안 저장하겠다는 것이다. ex 값을 60으로 바꿔놓고 확인해보면 1분 뒤에 키가 자동으로 삭제되어있을 것이다.

이메일을 보낸 뒤 redis-cli로 확인해보면 이렇게 이메일 키 값이 생겨져있다.

아무런 작업을 하지않고 ex만큼 시간이 지난 뒤 다시 확인해보면 키가 없어진 것을 볼 수 있다.

로직 3,4,5번

유저가 보낸 코드가 일치하는지 확인하고 redis 내부의 값을 변경해줘야한다.

#project_app/views.py

from django_redis import get_redis_connection

client = get_redis_connection()

def patch(self, request, *args, **kwargs):
        code = request.data.get("code")
        email = request.data.get("email")
        answer = client.get(email)
        if code == answer:
            client.set(email, code)
            return Response({"code": code}, status=status.HTTP_200_OK)
        else:
            return Response({"error": "Code Not Matched"}, status=status.HTTP_400_BAD_REQUEST)

위에서 작성한 EmailVerifyView에 patch 메소드를 추가해줬다.
유저로부터 코드를 받아 일치하는지 확인하는 API다.

코드가 일치하면 다시 이메일:랜덤 코드로 set해주는데 이번에는 ex옵션이 없다.
이렇게 하면 유효기간을 없앨 수 있다.

이 후에 클라이언트 쪽으로 유저가 입력한 코드를 다시 보내준다.

로직 6,7번

#project_app/views.py

def post(self, request):
        code = request.data.get("code")
        email = request.data.get("email")
        if code != client.get(email):
            return Response({"error": "Code Not Matched"}, status=status.HTTP_400_BAD_REQUEST)
            
        # ...
        
        client.delete(email)

회원가입을 담당하는 뷰에서 code와 email을 추가로 입력받고 redis에서 일치하는지 확인해 유저가 이메일 인증 작업을 완료했는지 검증한다.

회원가입 로직이 완료되면 이메일 키를 삭제해준다.

profile
가치를 창출하는 개발자! 가 목표입니다

0개의 댓글