Golang과 Lambda를 활용한 알림 서비스 개발기

평소에 Festa 라는 이벤트 플랫폼을 가끔씩 이용하는 편이다. 이 서비스에는 내가 관심있는 IT 관련 세미나 혹은 해커톤과 같은 밋업이 종종 올라오곤 한다. 보통은 페이스북 그룹에 공유된 글을 통해서 혹은 지인들을 통해서 알게됬는데 그럼에도 놓치는 것들이 있었다. 이번에 하던일이 끝나고 시간이 좀 남아서 Festa에 최신 이벤트에 대해서 메일로 알림을 해주는 서비스를 개발해보았다.

이 글에선 Golang과 AWS Lambda를 활용해서 알림 서비스를 개발한 것 에대해서 정리해보려고 한다.

구상

처음에 했던 작업은 알림에 대한 아이디어 구상이였다. Festa에서는 이벤트에 대한 카테고리나 태그에 대해서 제공하지 않았다. 새로은 이벤트에 대해서 모두 알림을 받는 방법도 생각해봤지만 생각보다 개발과 관련없는 이벤트도 많이 올라왔다. 고민중에 주변에서 피드백을 받아 키워드를 통해서 필터링하는 방법을 선택했다. 아래 그림을 보면 이벤트에는 제목과 주최한 Host에 대한 정보가 있다. 특정 키워드를 등록해놓고 해당 키워드에 매칭된다면 메일로 알림을 해주는 방식이다.

키워드 Exmaple : GDG, 해커톤

(Festa 사이트 events 페이지 참고)

키워드를 사용하는 방법도 완벽한것 같지는 않다. 다만 지금은 이것말고 딱히 다른방법이 떠오르지 않아서 일단 구현하기로 했다. GDG에서 여는 이벤트가 많이 올라와서 이것만 등록해놔도 크게 문제는 없을것 같았다.

기술 선정

Golang

Golang을 공부하고 있어서 프로젝트 목적의 반은 Golang에 좀 익숙해지는데에 있었다. 다른 익숙한 언어를 사용했다면 좀 더 수월하게 개발할 수 있었지만 진행하면서 Golang에 대한 학습도 되고 괜찮았던 것 같다.

Google Cloud Firestore

구독자 메일과, 키워드를 저장해놓을 데이터베이스가 필요했다. 선정 기준은 무료여야했고 얼마 되지않고 단순한 데이터를 저장하는 용도라 RDB보단 NoSQL을 선호했다. Firebase가 생각나서 정말 오랜만에 들어갔는데 Realtime Database를 말고 Firestore라는 서비스를 제공하고 있었다. Realtime Database보다는 Firestore가 목적에 좀 더 적합해서 이걸 사용하게 되었다.

두 서비스의 차이점 비교
https://firebase.google.com/docs/firestore/rtdb-vs-firestore?hl=ko

AWS Lambda

Schedule 기능이 필요해서 CircleCI Schedule 기능을 알아보다가 람다도 CloudWatch를 이용해서 Schedule 기능을 구현할 수 있다는 걸 알게되었다. Function 별로 Schedule을 설정할 수 있어서 다른 Schedule을 추가하기에도 용이했고 가격도 무료에 가까웠다. 사용해본적도 있어서 람다를 이용하게 되었다.

AWS SES

메일을 보내는 기능은 알림을 위해서 필수적이였다. 메일 전송에 대한 부분은 선택지가 꽤 많은 편이다. mailgun, SendGrid 등 무료와 유료 플랜 모두 지원하는 서비스가 여러개 있고 개인 Gmail을 활용하는 방법도 있었다. 처음엔 귀찮아서 내 Gmail 계정을 활용해서 테스트 하다가 AWS SES를 이용하게 됬다. 도메인만 가지고 있으면 손쉽게 Outbound메일 서버를 구축할 수 있고 비용도 사용량이 적은경우 거이 나가지 않는 듯 했다. 자세히 읽어보지는 않아서 다음달 청구서를 봐야할 것 같다.

CircleCI

CircleCI는 단순히 Serverless Deploy 용도를 위해서 사용하고 있다. Local에서 Deploy해도 충분하지만 최종 커밋이 항상 깨지지 않는 상태를 유지하고 싶었다. Travis만 사용해봐서 CircleCI는 어떤가 사용해보려는 목적도 있었다.

구현

전체적인 Flow에 대해서 먼저 생각해보면 아래와 같다.

  1. Festa에서 최신 Event 정보를 받아온다.
  2. Firestore에서 구독자 목록을 받아온다.
  3. 구독자마다 Event목록 중 알림대상이 있는지 확인한다.
  4. 알림대상이 있다면 메일을 전송한다.

위 Flow를 하나의 람다로 구성하고 일정시간마다 호출해주면 된다.

Festa Event

먼저 Festa 최신 Event를 받아오는 부분이다. REST API가 있었고 로그인을 하지 않아도 최신 Event를 확인할 수 있었다. 이 부분을 Go로 구현해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package festa

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
)

// Festa struct is invoke Festa API
type Festa struct{}

// EventResponse Festa API response
type EventResponse struct {
Page string `json:"page"`
PageSize string `json:"pageSize"`
Total int `json:"total"`
Rows []Event `json:"rows"`
}

// New returning Festa API instance
func New() *Festa {
return &Festa{}
}

const apiEndpoint = ...

func toQueryString(params map[string]string) string {
arr := []string{}
for k, v := range params {
arr = append(arr, fmt.Sprintf("%s=%s", k, v))
}

return strings.Join(arr, "&")
}

// GetEvents return recent festa events
func (f *Festa) GetEvents() (events []Event) {

var eventResponse EventResponse
queryParam := map[string]string{
...
}
resp, _ := http.Get(fmt.Sprintf("%s?%s", apiEndpoint, toQueryString(queryParam)))

responseBytes, _ := ioutil.ReadAll(resp.Body)

json.Unmarshal(responseBytes, &eventResponse)

return eventResponse.Rows
}

Go는 JSON String을 Unmarshaling할때 Struct 형태로 변환이 가능하다. JSON 필드를 일일이 Struct로 만드는 작업은 매우 손이 많이가고 번거롭기 때문에 다른 툴을 이용해서 변환했다. JSON Response를 input으로 넣으면 Go Struct로 변환해주는 툴이다.
https://mholt.github.io/json-to-go/

Firestore

Firebase 콘솔 에서 프로젝트를 생성하면 바로 Firestore 혹은 Realtime Database를 사용할 수 있다.

SDK를 통해서 Firestore에 접근하려면 비공개 키가 필요하다. Firebase 프로젝트 설정에서 비공개 키를 만들고 JSON 형식의 파일로 다운받을 수 있다. Lambda에선 파일을 사용하기 어렵기때문에 파일내용을 JSON으로 환경변수에 넣어놓고 시작시에 Unmarshal 해서 사용하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

// Package db provides firestore client
package db

import (
"cloud.google.com/go/firestore"
firebase "firebase.google.com/go"
"fmt"
"golang.org/x/net/context"
"google.golang.org/api/option"
"os"
)

var client *firestore.Client

func getOption() option.ClientOption {
rawString := os.Getenv("SERVICE_ACCOUNT_KEY")
return option.WithCredentialsJSON([]byte(rawString))
}

// GetClient return signletone firestore instance
func GetClient() (*firestore.Client, error) {
if client == nil {

ctx := context.Background()
conf := &firebase.Config{ProjectID: "festa-notify"}

app, err := firebase.NewApp(ctx, conf, getOption())

if err != nil {
return nil, fmt.Errorf("error initializing firebase app: %v", err)
}

store, err := app.Firestore(ctx)
if err != nil {
return nil, fmt.Errorf("error initializing firestore: %v", err)
}

client = store
}
return client, nil
}

Firestore에 대한 instance를 Signletone 형태로 구성해놓고 도메인 별로 Service를 만드는 방식을 사용했다.
Collection에 대한 Query, Update 부분은 https://cloud.google.com/firestore/docs/ 이곳에 자세하게 나와있다.

AWS SES

메일링을 위해서 SES를 셋업해야 한다. 생각보다 SES를 사용하는 방법은 간단하다.
SES 페이지에 접속 후 Verify a New Domain 버튼을 눌러서 도메인을 검증하면 Sandbox 모드에서 사용할 수 있게된다. 실제 사용을 위해서는 Sandbox 모드 해제를 신청해야 한다. Sandbox 모드에서는 미리 인증해놓은 이메일에 한해서만 메일 전송이 가능하다.

도메인은 Freemon 에서 무료 도메인을 만들어서 사용했다.

실제로 메일을 전송할 때는 등록해놓은 도메인으로 전송이 가능하다. 지금은 메일 발송시 noreply@festa-notify.cf 이런식으로 사용하고 있다.

Go 에서 SES 메일 발송을 위해선 이 예제만 참고하면 무리없이 구현할 수 있다.

Serverless & Go

Lambda 배포를 위해 Serverless 프레임워크를 사용하였다. Go를 Serverless를 통해서 Lambda에 배포하는건 Node.js에 비해 간단한 느낌을 받았다. 단순히 Go func를 만들고 Labmda 라이브러리에 해당 핸들러를 넘겨주면 된다.

Serverless Blog에서 간단한 Example에 대해서 소개한다.
https://serverless.com/blog/framework-example-golang-lambda-support/

위 예제에서는 go dep를 이용하는 방법에 대해 나오는데 나는 Go modules를 사용하고 있어서 의존성 관리 부분을 제외하고는 예제를 따라서 구현했다. 이미 개발중인 프로젝트에 적용시에는 Makefile, serverless.yml 설정만 옮겨서 프로젝트에 맞게 설정해주면 쉽게 AWS Lambda로 배포할 수 있다.

CloudWatch Event

CloudWatch는 보통 AWS 관련로그를 보는 용도로 사용하는데 여기에 이벤트라는 기능이 있다. 이 이벤트를 이용하면 특정시간 혹은 주기적으로 어떤 람다를 호출해줄 수 있다.

CloudWatch에 Event 탭으로 이동해서 규칙을 생성해주기만 하면 된다. rate 혹은 crontab 표현식으로 등록할 수 있다.

메일 알림

https://user-images.githubusercontent.com/2585676/63648974-9f0ae000-c772-11e9-8785-672b5efa224b.png

Github

이 프로젝트의 코드는 아래 Repo에 모두 공개되어 있다.
https://github.com/y0c/festa-notify

마치며

  • Go가 아직 익숙하지 않아서 생각보다 오래걸렸지만 언어 자체는 간편하고 장점이 많아 보인다.
  • SES를 사용한 다른 글을 보면 Sandbox 모드 해제는 하루정도면 되는 것 같은데 주말때문인지 오래걸리는 케이스도 있는 것 같다.

작은 프로젝트지만 자세히 적으려니 생각보다 내용이 많아서 전체적인 맥락에 대해서만 간단히 정리해보았다.