프로젝트 경험기/MSA 경험기

[MSA 경험기] 모아 구독 (7) 메일 서비스 비동기 전환 (by. FastAPI, Celery, Rabbitmq)

yubi5050 2022. 11. 29. 20:55

지난 글

이전 글에서는 검색 히스토리 서비스를 구현하며, DB 선택 과정과 Singleton 패턴을 적용한 경험에 대해 작성하였다.
https://yubi5050.tistory.com/225

 

[MSA 경험기] 모아 구독 (6) 검색 히스토리 서비스 (by. NoSQL, Singleton)

지난 글 이전 글 에서는 Query Profiling을 통한 Query 시간 최적화에 대해 작성하였었다. https://yubi5050.tistory.com/223 [MSA 경험기] 모아 구독 (5) Query Profiling을 통한 최적화 지난 글 이전 글 에서는 조회수

yubi5050.tistory.com

 

이번 글 에서는 기존에 동기로 이루어지던 메일 서비스를 비동기로 전환하면서 고민하였던 부분들과 과정에 대해 작성해 보려고 한다.

 

주요 고민 지점

  • 메일 서비스를 독립적인 서비스로 (seller, consumer 모두 호출)
  • 서비스 구현 Framework 선택 (Django vs FastAPI 중) 
  • Worker의 분리 여부 (FastAPI 내부모듈 vs Celery)
  • 메시지 Broker 선택 (Redis vs RabbitMQ)

 

메일 서비스를 독립적인 서비스로 분리 할 것인가?

이 부분은 사실 메일 서비스를 독립적으로 분리하는 쪽으로 쉽게 결정이 나긴 했지만, 

프로젝트를 성공적으로 마치기 위해선, 전체 규모 (인원) 대비 Over-engineering 하는 것은 아닌지 의구심을 가지며, 해당 서비스가 분리했을 때 이점이 큰지에 대해 고민하였다.

 

최종적으로는 기존에 타 서비스들에서의 다음과 같이 메일 서비스를 활용하는 다양한 소요가 있었고

  • 판매자(Seller) 서비스에서 구독 상품 약관 변경시 이메일 전송
  • 결제(Payment) 서비스에서 결제 완료시 이메일 전송

 

Mail 서비스를 독립적으로 분리하여 결합도를 낮추기로 최종 결정하였다.

 

서비스 구현 Framework 선택 (Django vs FastAPI)

이 부분이 사실 주니어 입장으로서 가장 선택하기 어려웠는데, 최대한 이해할 수 있는 정보를 활용하여 결정하였다.

 

ASGI 방식의 FastAPI가 더 요청을 빠르게 처리할 수 있다.

  • WSGI 방식의 Django는 동기적으로 요청을 처리하여, 여러 서비스에서 호출시 성능에 한계가 존재
  • ASGI 방식인 FastAPI는 비동기적으로 요청을 처리가능하고, '모아구독'의 메일 서비스는 다양한 서비스 (SellerService, PaymentService) 에서 호출되므로 더 적절하다고 판단
  • 또한 해당 문서의 Performance TEST 결과에 따르면 FastAPI가 Django보다 최초 Call이 더 빠르다.

 

최종 FastAPI를 사용하여 구현하기로 결정하였다.

 

FastAPI BackgroundTasks vs Celery

FastAPI는 내부적으로 BackgroundTasks 모듈을 제공하여 Thread를 활용해 메일서비스를 구현 할 수도 있고, Celery와 Broker를 활용해 Worker로서 메일을 보낼 수 있다.

 

BackgroundTasks (POC 소스코드 링크)

  • Web Applcation의 자원을 같이 활용하여 내부적인 Thread로 작동
  • 서버가 장애발생시 메시지가 유실 될 우려가 존재 (요청을 받았으나 정상적으로 수행 못함)

 

Celery (+Broker)

  • Web Application 외의 자원을 사용
  • Broker를 통해 Web Application이 가 장애가 발생해도 메시지가 Broker에서 보관되어 유실 가능성이 적음

 

'모아 구독'의 메일 서비스는 구독 상품에 대한 '약관 정보 변경' 이나 '구매 상품 정보 전달' 등에 메일이 활용되므로, 메시지의 전달이 어느정도 보장되어야 하는 서비스이다.

 

따라서 메시지 유실 가능성이 비교적 적은 Celery + Broker로 구현하기로 결정하였다.

 

메시지 Broker 선택 (RabbitMQ vs Redis)

WebApplication(Producer)에서 생산된 Message를 Worker(Consumer)로 전달해주는 중간 Broker가 필요하다.

Broker에는 MQ(ex. RabbitMQ)를 이용하는 방법과 Pub/Sub(ex. Redis)를 이용하는 방법이 있고,

 

해당 Broker의 비교는 아래 글에서 정리해보았다.

https://yubi5050.tistory.com/224

 

[MQ] 메시지 지향 미들웨어 (by. RabbitMQ, Redis)

메시지 지향 미들웨어(Message Oriented Middleware, MOM) 란? 메시지 지향 미들웨어(Message Oriented Middleware, MOM)이란, 응용 프로세스 간 비동기 방식의 데이터 통신을 통해, 메시지를 전달 해주는 시스템을

yubi5050.tistory.com

 

위 고민과 마찬가지로 '모아구독' 메일 서비스는 안정성이 좀더 요구되는 서비스에 가까워, 최종 속도적인 측면 보단 안정성 측면에서 더 강한 RabbitMQ를 최종 사용하기로 하였다.

 

코드 비교 - 동기 메일 서비스 코드 구현 (Django)

처음 메일 시스템 구현시 Django의 mail 라이브러리와 Gmail 서버를 이용해 구현하였다.

아래 코드를 확인해보면 POST 요청이 들어오면 django.core.mail의 send_mail을 활용해 request 정보에 맞춰 메일을 전송한다. 해당 방법의 큰 단점은 메일 전송이 완료될 때 까지 기다려야 한다는 점이다.

 

 

자세한 구현 코드 예시는 아래 링크에 정리하였다.

https://yubi5050.tistory.com/216

 

[Django] 간단한 메일 전송 기능 구현 (by. Gmail )

1. Google 계정 보안 설정하기 (1) 구글 계정 - 보안 접속 (2) 2단계 인증 설정 (3) 앱 비밀번호 생성 구글 뿐만 아니라 네이버로 전송시에도 다음과 같이 앱 비밀번호를 생성하는 절차를 거쳐야 한다.

yubi5050.tistory.com

 

코드 비교 - 비동기 메일 서비스 구현 (FastAPI + RabbitMQ + Celery)

FastAPI의 mail server 라이브러리와 Gmail 서버를 이용해 기본적인 메일 전송 시스템을 구현하였다.

 

POST 요청에 맞춰 celery의 task로 등록한 함수를 비동기적으로 실행시키고 바로 사용자에게는 Response를 준다.

이후 해당 MessageSchema에 맞춰 메일 발송이 진행되고, 생성된 Celery의 오브젝트에 맞춰 실행시킨다. (해당 과정에서 async하게 들어오는 요청을 sync하게 만들기 위해 asgiref 라이브러리의 async_to_sync를 사용한다.)

 

메일 전송 완료를 기다릴 필요 없이, 사용자에게 바로 응답이 가고 Celery는 독립적으로 mail 전송을 진행한다.