PROJECT/개발일지

[회고] 출근길에 금융 포스트들을 보내주는 서비스 제작기

Ray123 2024. 6. 6. 21:32

들어가며

작년 말 운좋게 금융권 인턴 취업에 성공하고, 올해 무사히 정직원 전환에 성공했습니다. 금융권에 재직 중이고, 돈을 벌기 시작하면서 금융과 경제에 대한 관심이 자연스럽게 많아지고 있는데요. 문득 출근길 지하철에서 금융 포스트들을 하나씩이라도 읽으면 좋겠다는 생각이 들었고, 매일 아침 출근길마다 읽기 좋은 금융 포스트들의 링크를 보내주는 서비스들을 만들면 어떨까라는 생각으로 이어졌습니다.

 

그와 동시에 실제로 이 서비스를 간단히 운영해보고 싶다는 생각이 들었고, 운영 과정에서 발생하는 비용을 최소화하는 걸 목표로 삼았는데요. 이 "출근길 금융 한 잔"이라는 간단한 서비스를 개발한 과정을 소개해드리려고 합니다.

 

 

1. 어떤 플랫폼의 포스트들을 보낼까?

첫 번째로는 어떤 글들을 보낼 것인가를 고민했습니다. 아무래도 스스로를 위한 서비스인만큼 제가 잘 읽힌다고 생각되는 플랫폼들을 선정했는데요. 고민 끝에, 카카오뱅크와 토스뱅크에서 운영하는 플랫폼의 글들을 보내주기로 결정했습니다.

 

https://brunch.co.kr/@kakaobank#articles

 

카카오뱅크의 브런치스토리

이미 모두의 은행. 카카오뱅크 공식 브런치입니다.

brunch.co.kr

https://blog.toss.im/

 

금융이 알고 싶을 때, 토스피드

콘텐츠도 토스가 만들면 다릅니다

blog.toss.im

 

 

 

2. 이 플랫폼의 포스트들을 어떻게 가져올까?

처음에는 크롤링을 통해 포스트들을 감싸고 있는 <ul>태그나 <ol태그>를 찾은 뒤, 그 안에 있는 목록들을 가져오는 방법을 구상했습니다. 그러다가 문득 "카카오뱅크나 토스뱅크도 처음에 블로그에 들어갈 때 특정 API를 통해서 포스트 목록들을 가져오지 않을까?"라는 생각이 들더라구요. 개발자 도구 등을 사용해서 그 API를 제가 딸수만 있다면 크롤링보다 훨씬 적은 공수로 포스트들을 가져와서 활용할 수 있겠다는 생각에 바로 개발자 도구를 켰습니다. 

 

 

Network 탭을 까본 결과, 카카오뱅크와 토스뱅크가 사용하는 API들을 어렵지 않게(?) 따올 수 있었습니다. 그러면 그 API의 응답값을 토대로 특정 포스트의 no값이나 key값을 딸수가 있었는데요, 카카오뱅크와 토스뱅크의 블로그들은 그 값들을 이용해서 다음과 같은 URL로 접근하면 해당 글들을 볼 수 있는 것까지 살펴볼 수 있었습니다.

 

카카오뱅크 : https://brunch.co.kr/@kakaobank/{no} 

토스뱅크 : https://blog.toss.im/article/{key} 

 

 

3. 포스트들의 링크를 어떤 매체로 전달할까?

포스트들의 링크를 얻는 방법을 알았으니, 이 링크를 어떤 매체로 보낼지가 고민됐습니다. 가장 대중적인 매체인 "카카오톡"으로 보내보자는 생각이 들었고, 바로 채널을 개설했습니다.

 

나름 채널 프로필도 포토샵으로 만들어봤습니다
채널 개설 후 테스트 메시지 발송도 해봤답니다

 

하지만, 알람톡을 보내려면 일반 채널이 아닌 비즈니스 채널로 만들어야 한다는 것을 알게 됐습니다. 비즈니스 채널 개설을 위해선 사업자 등록증이 필요한데, 이걸 발급받고 이것저것 하는 것은 공수가 많이 들 것 같다는 생각이 들었습니다.

 

결국 카카오톡 채널을 통한 발송은 포기하고, 다른 방법을 알아봤습니다. 눈에 들어온 방법은 SMS 전송이었는데요, 다음과 같이 SMS 전송을 지원하는 4개의 플랫폼들을 비교했습니다.

 

  가비아 CoolSMS AWS SNS 네이버 클라우드 Notification
비용 최소 충전 20,500원
(건당 20.5원)
최소 충전 10,000원
(건당 20원)
건당 약 30원
(24.05.18 기준)
사업자만 가능
API 지원 O O O
기타 발신번호 등록 필요 발신번호 등록 필요 발신번호 등록 필요 X

 

SMS 발송에 필요한 발신번호는 "아톡" 등의 플랫폼에서 발급받을 수 있지만 월 2,000원이라는 비용이 발생합니다. 하지만 저는 이 서비스를 어차피 저(아니면 주변 사람들 몇 명만 더해서)만 사용할 것이기 때문에 발신번호는 제 010번호를 쓰면 되므로, 발신번호 필요 여부는 그리 큰 문제가 되지 않겠다는 생각이 들었습니다.

 

그러면 비용만 비교하면 됐었는데요. 최소 충전 비용이 있다보니 1년도 채 안 지나서 서비스 운영을 종료할 예정이라면 AWS SNS를 쓰는게 이득이지만, 앵간하면 이 서비스는 제가 계속 쓸 것 같다는 생각이 들었습니다. 금융 관련 글은 읽으면 읽을수록 플러스지 마이너스는 안 될 거라고 생각하거든요. 결국, 건당 비용이 가장 낮은 CoolSMS를 사용하기로 결정했습니다.

 

그렇다면, 실제로 CoolSMS를 통해 SMS를 보낼 수 있다는 걸 검증해야겠죠. 제공되는 API 문서를 통해 간단히 테스트했고, 제가 원하는 내용을 보낼 수 있음을 다음과 같이 검증할 수 있었습니다.

성공적인 테스트 결과

 

참고 : 빠른 속도를 위해 Python을 사용해서 개발 및 테스트했습니다

 

4.  상세 로직은 어떻게 구성할까?

API를 통해 SMS를 보낼 수 있다는 것도 검증했으니, 이제 실질적으로 어떤 과정을 통해 포스트들의 링크를 SMS로 전달할지를 구상할 차례인데요. 우선 다음 3가지의 데이터는 가지고 있어야 한다고 생각했습니다.

 

1) SMS를 받을 사람(구독자들)의 휴대폰 번호

2) 메시지 내용 템플릿

3) 가장 최근에 보낸 포스트의 정보(카카오뱅크는 no, 토스뱅크는 key)

 

이를 바탕으로, 초기에는 다음과 같은 로직을 구상했습니다.

 

 

2번 과정에서 마지막(가장 최근)에 보낸 포스트들의 정보를 가져오고, 이를 토대로 어떤 포스트들을 보내야할지를 4번 과정에서 추린 다음 SMS를 전송하는 로직입니다. 근데 이렇게 되면, 네트워크 문제 등으로 6번 과정(SMS 전송)에 실패해도 이미 실행된 5번 과정에 의해 전송된 포스트 정보가 갱신된다는 문제가 있었습니다. 즉 특정 포스트는 건너 뛰게 되는 현상이 발생할 수 있었죠. 따라서 5번과 6번의 순서를 바꿔서, 다음과 같은 로직으로 구성했습니다.

 

 

 

5. 어떤 DB를 사용할까?

이제 데이터들을 담을 DB를 골라야 했습니다. 클라우드 서비스들을 알아봤는데요. 제 AWS 계정은 프리 티어를 사용할 수 있는 시기가 이미 지났기 때문에 AWS가 아닌 다른 서비스들을 찾아봤고, 서치를 통해 Supabase라는 BaaS(Backend as a Service)를 알게 됐습니다. 완전 무료형 서비스는 아니고 500MB만큼의 DB 용량을 무료로 사용 가능한데, 어차피 저는 DB에 insert가 아닌 update만 해줄 거라 용량제한을 넘길 일은 없고, 끽해야 하루에 한 번 read/write를 하기 때문에 이 녀석을 쓰는게 딱이겠다는 생각이 들었습니다. 

 

역시나 테스트를 해봐야겠죠? Supabase 콘솔에서 테이블들을 세팅해주고, 다음 코드들을 통해 select(휴대폰 번호 테이블에서 번호들 가져오기)와 update(가장 최근에 보낸 포스트 정보 갱신)가 정상 수행되는 걸 확인했습니다.

 

# SMS를 받을 사람들의 핸드폰 번호 가져오기
subscriber_phone_numbers = supabase.table(os.environ.get("PHONE_NUMBER"))\
    			.select("*").execute().data

print(subscriber_phone_numbers)

# [
#   {"phone_number": "010XXXXXXXX},
#   {"phone_number": "010XXXXXXXX},
#   {"phone_number": "010XXXXXXXX}
# ]


# 가장 최근에 보낸 포스트의 정보 갱신(토스뱅크)
supabase.table(os.environ.get("TOSS_BANK_POST"))\
                .update({"toss_last_send_key": key}).eq("id", id).execute()

 

 

6. 서버는 어디에 띄울까?

코드 작성은 끝났고, 이제 가장 중요한 단계죠. 서버를 어디에 띄울지 고민했습니다. 아무래도 매일 아침에만 코드가 실행되면 되게 때문에 서버리스를 도입하는게 궁극적인 목적인 "비용 절감"에 가장 적합하다고 생각했는데요. 그렇다고 처음부터 서버리스만 고려하기 보다는 EC2에 호스팅하는 방법을 포함해서 다음과 같이 4가지 방법을 조사했습니다.

 

  AWS EC2 + Scheduler AWS Lambda + EventBridge Supabase Edge Fuction + pg_cron 스크립트 페이지 호스팅 + cron_job.org
비용 1,586원/월
(스팟 인스턴스, t3.nano 기준)
3.55원/월
(0.5GB 메모리, 10초 실행 기준)
무료 무료
비고 1) EC2를 Public Subnet에 두면 보안 취약

2) EC2를 Private Subnet에 두면 NAT Gateway 비용 추가 발생
  TypeScript로만 작성 가능 다른 사람이 해당 페이지를 호출함으로써 스크립트 실행 가능

 

 

1) AWS EC2 + Scheduler

EC2 상에 서버를 띄우고, 자체적으로 스케쥴러(스프링의 @Scheduled 등)를 설정해 일마다 수행하는 방법입니다. EC2는 사용한 만큼 지불하는 On-demand 옵션 외에도 할인된 금액으로 선납입을 하는 예약 인스턴스, 가장 저렴한 금액으로 사용 가능하나 서비스가 중단될 가능성이 있는 스팟 인스턴스 등의 유형이 있는데요. 본 서비스는 서비스가 중단되도 큰 영향이 없기 때문에(다음날 수행되면 어차피 전날 안 보내진 것도 일괄 전송되므로) 스팟 인스턴스를 사용해도 무방하다고 판단했습니다.

 

 

24년 6월 4일 기준으로, t3.nano를 사용할 때 시간당 0.0016 달러를 내는데요. 24를 곱하고 30을 곱하면, 한달에 1.152달러 = 1,586원이 발생됩니다.

※ 참고 : On-demand의 경우, 이 케이스에선 4배 정도 더 비쌈

 

그리고 EC2는 최초 생성 시 Public Subnet에 둘 건지 Private Subnet에 둘 건지를 정해야 합니다. 해당 EC2는 외부로부터의 선접근이 없으므로 Private Subnet에 두는 게 보안상 안전하나, 카뱅 & 토뱅으로부터 최신 포스트 목록을 가져오는 과정, supabase에 접근해 데이터를 가져오는 과정 등에서 외부와의 통신이 필요하므로 NAT 게이트웨이 등을 별도 설치해야 하고, 이는 추가 비용을 야기합니다. 그렇다고 Public Subnet에 두자니, 외부로부터의 접근에 노출될 우려가 있습니다. 따라서 EC2와 Scheduler 조합을 선택할 경우, 이 부분을 좀 더 고려해서 선택해야 할 필요가 있었습니다.

 

 

2) AWS Lambda + EventBridge

AWS의 서버리스 서비스인 Lambda Function을 등록하고, EventBridge를 통해 일마다 Lambda를 호출하는 방법입니다. 제 로컬을 기준으로 1회 실행 시 3.2초 정도가 수행됐는데요, 아무래도 보수적으로 1회 실행에 10초가 걸린다고 잡고 512MB짜리 메모리를 쓴다고 가정했습니다. 

 

 

일마다 실행하므로 한 달에 30번만 실행되기 때문에, 30 x 10(10초) x 0.5(512MB = 0.5GB) x 0.0000166667 = 0.0025달러 = 3.5원 정도의 비용이 한 달마다 발생되게 됩니다.

 

그리고 EventBridge에 대한 비용도 고려해야 하는데요. 

 

 

100만건의 스케줄러 호출 당 1.15달러의 비용이 월마다 발생하는데요. 우리는 월 30번만 호출하니 0.0000345달러 = 약 0.05원의 비용이 달마다 발생합니다.

 

즉, 정리하면 AWS Lambda + EventBridge 사용 시, 3.55원의 비용이 월마다 발생합니다.

 

 

3) Supabase Edge Function + pg_cron

Lambda와 같은 서버리스 서비스인 Supabase Edge Function을 등록하고, pg_cron이라는 Supabase Extension을 통해 일마다 Edge Function을 호출하는 방법입니다. 24년 6월 4일을 기준으로 Edge Function은 TypeScript만으로만 작성가능하고, HTTP 요청을 통해 Edge Function을 호출하는 방식으로 동작합니다. 따라서 Edge Function과 + pg_cron을 선택하는 경우 HTTP 요청에 대한 별도의 인증을 구성하지 않는다면 제 3자의 Edge Function 호출로 SMS 발송이 야기되어 과금이 발생할 수 있으니 꼭 인증을 구성할 필요가 있고, TypeScript로 다시 코드를 작성해야 하는 공수가 발생함을 고려해야 합니다.

 

 

4) 스크립트 페이지 호스팅 + cron_job.org

우비나 닷홈 등의 무료 호스팅 서비스를 이용해 스크립트 페이지를 만들고, cron_job.org라는 무료 스케쥴러 사이트를 활용해 일마다 스크립트 페이지로 HTTP Request를 보내게 하는 방법입니다. 완전 무료로 사용 가능하지만, 호스팅된 스크립트 페이지를 다른 사람도 호출할 수가 있는데요. 이 경우 제가 원치 않는 타이밍에 SMS가 발송되고, 이것들이 모두 과금으로 이어질 수 있다는 치명적인 단점이 있습니다.

 

 

 

정리하면, EC2 사용은 월 1,500원 이상의 비용이 발생하니 고려 대상에서 제외했고, 스크립트 페이지 호스팅 & cron_job.org 사용은 제 3자의 스크립트 페이지를 요청으로 인한 과금 폭탄이 발생 가능해 제외했습니다. AWS Lambda와 Supabase Edge Function이 남았는데, Lambda 사용 시 발생하는 월 3원 가량의 비용은 사실상 없는 수준이라고 생각했고 Supabase Edge Function은 제가 사용해본 적 없는 TypeScript로 코드를 재구성해야 하는 점 등의 공수가 걸렸습니다. 따라서 최종적으로 Lambda와 EventBridge를 사용하는 것으로 결정하게 됐습니다.

 

 

7. AWS Lambda 함수 생성 & EventBridge 설정 (with. 트러블 슈팅..)

Lambda 함수 생성시, 파이썬 버전은 로컬에서와 동일하게 3.11버전으로 설정해줬습니다.

 

그 뒤 함수에서 사용될 환경변수들을 세팅해줬습니다.

값은 가렸습니다

 

그리고 Lambda 함수가 사용할 외부 라이브러리들을 세팅해줘야 했는데요. "레이어"를 통해 이것이 가능하고, 쉽게 설명하면 제가 세팅해준 레이어 위에서 Lambda함수가 돌아가는 개념입니다.(주로 이렇게 외부 라이브러리들을 미리 세팅할 용도로 활용한다고 합니다) 로컬에서 쓴 라이브러리들을 pip freeze >를 통해 requirements.txt로 빼준 뒤, pip install -t를 통해서 해당 라이브러리들을 따로 제가 지정한 폴더에 설치한 후 해당 폴더를 압축해서 레이어 생성에 활용할 수 있었습니다.

 

 

그 다음, 이렇게 생성한 레이어를 아까 만들어둔 Lambda 함수에 추가해줬습니다.

 

 

그리고 만들어둔 코드를 Lambda 코드에 복붙한 뒤 테스트 실행을 눌렀는데, 다음과 같이 pydantic_core라는 모듈을 찾을 수 없다는 오류를 만났습니다.

 

Response
{
  "errorMessage": "Unable to import module 'lambda_function': No module named 'pydantic_core._pydantic_core'",
  "errorType": "Runtime.ImportModuleError",
  "requestId": "Blah Blah ~",
  "stackTrace": []
}

 

관련된 스택 오버플로 글 

https://stackoverflow.com/questions/76650856/no-module-named-pydantic-core-pydantic-core-in-aws-lambda-though-library-is-i

 

No module named 'pydantic_core._pydantic_core' in AWS Lambda though library is installed for fast api based code

AWS lambda deployment of FastAPI gives the following error: [ERROR] Runtime.ImportModuleError: Unable to import module 'users_crud': No module named 'pydantic_core._pydantic_core' Traceback (most r...

stackoverflow.com

 

요약하면 Lambda는 특정 아키텍쳐에 맞는 패키지들을 필요로 하는데, 제가 레이어로 세팅해준 라이브러리들이 Lambda가 구동되는 아키텍쳐와 맞지 않아서 발생되는 문제였습니다. 실제로 제 로컬에서 requirements.txt에 적힌 라이브러리들에 대해 pip install을 해보면,

 

 

이렇게 특정 라이브러리들이 제 로컬 머신의 아키텍쳐(macos)에 맞는 버전으로 설치되는 걸 볼 수 있었습니다.(pip install을 할 때 라이브러리들이 머신의 아키텍쳐 버전에 맞춰 설치되는 걸 처음 알았네요... 모든 라이브러리가 이렇게 아키텍쳐별로 버전을 다르게 제공하는 건 아닙니다) 하지만 저는 Lambda함수를 생성할 때 아키텍쳐를 x86_64로 설정했으니 오류가 발생하는 거였죠.

 

따라서 다음 명령어를 통해 라이브러리들을 x86_64 아키텍쳐에 맞는 버전으로 설치하게끔 하고, 이걸 다시 압축해서 새로 레이어를 세팅해줬습니다.

 

pip install -r requirements.txt --platform manylinux2014_x86_64 --target ./python --only-binary=:all:

 

그 결과,

 

 

Lambda를 통해 제가 짰던 코드가 정상적으로 실행되게 할 수 있었습니다.

 

 

이제 EventBridge를 통해 생성한 Lambda함수를 스케줄링할 차례입니다. 이건 매우 간단했는데요. 생성된 Lambda 함수 콘솔에서 트리거 추가를 통해 설정해줄 수 있었습니다.

 

 

 

해당 cron 표현식은 월-금요일 매일 12시에 실행한다는 의미입니다. 다만 UTC가 기준이기 때문에, 한국 시각으로는 21시에 실행됩니다(KST가 UTC보다 9시간 앞서 있기 때문). 따라서 저렇게 트리거를 추가한뒤, 실제로 21시에 람다가 실행되는지를 봤습니다. 그 결과,

 

 

실제로 21시에 Lambda가 수행되어, SMS가 정상 전송된 것을 확인할 수 있었습니다! 

이제 매일 아침 08시에 람다가 실행되도록 EventBridge 규칙의 cron 표현식을 다음과 같이 변경해주고 마무리 지었습니다.

 

cron(0 23 ? * SUN-THU *)

 

 

마무리...

되게 간단한 서비스라고 생각했는데, 이것저것 고민하는 과정들이 많았습니다.. 만들었다 하기도 뭐하고 운영이라 하기도 뭐하지만, 그래도 생각해보니 제가 만든 서비스를 이렇게 운영 레벨로 옮겨온 건 처음이더라구요. 옛날에 이두희님이 코딩으로 자기가 만들고 싶은 것들을 뚝딱뚝딱 만드는 걸 보며 되게 멋있다고 생각했는데, 저도 제가 나한테 필요하겠다 싶은 서비스를 이렇게 중간에 멈추지 않고 처음 목표대로 실제 운영 단계까지 오는데 성공해서 굉장히 뿌듯...>< 합니다. 우선 저랑 여자친구한테만 이 글들이 가게 하고, 나중에 친구들 몇 명 해서 추가할 것 같네요. 일련의 과정들을 정리하고 공유하고 싶어 글을 이렇게 써봤는데, 난잡한 글 끝까지 봐주셔서 감사합니다.